image.png

遇到的坑

1、WebMvcConfigurationAdapter

随便使用此注解可能会导致MVC静态资源自动配置失效,因为WebMvcConfigurationSupport如下:

  1. @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
  2. @ConditionalOnMissingBean({WebMvcConfigurationSupport.class})⭐这里⭐
  3. @AutoConfigureOrder(-2147483638)
  4. @AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
  5. public class WebMvcAutoConfiguration {...

配置了直接失效;
但是如果自己配置了静态资源路径,mvc静态资源自动配置就不会失效,因为就是WebMvcConfigurationSupport类中配置了静态资源自动配置;

  1. ...
  2. this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(),
  3. (registration) -> {
  4. registration.addResourceLocations(this.resourceProperties.getStaticLocations());
  5. ...//⭐静态资源的配置就在getStaticLocations()方法中⭐
  1. private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{
  2. "classpath:/META-INF/resources/",
  3. "classpath:/resources/",
  4. "classpath:/static/",
  5. "classpath:/public/"};
  6. private String[] staticLocations;
  7. 。。。
  8. public Resources() {
  9. this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
  10. 。。。
  11. public String[] getStaticLocations() {
  12. return this.staticLocations;⭐上面就是调用了此方法⭐
  13. }
  14. 。。。

我们可以用WebMvcConfigurer接口代替之;

2、雪花算法生成id

由于雪花算法生成的idlong类型的,19位,而javascriptlong类型的数据处理时会产生精度丢失,最多只能保证16位的精度,因此此处要扩展mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行java对象到json数据的转换。

  1. package com.mcr.reggie.common;
  2. import com.fasterxml.jackson.databind.DeserializationFeature;
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import com.fasterxml.jackson.databind.module.SimpleModule;
  5. import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
  6. import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
  7. import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
  8. import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
  9. import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
  10. import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
  11. import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
  12. import java.math.BigInteger;
  13. import java.time.LocalDate;
  14. import java.time.LocalDateTime;
  15. import java.time.LocalTime;
  16. import java.time.format.DateTimeFormatter;
  17. import static com.fasterxml.jackson.databind.DeserializationFeature
  18. .FAIL_ON_UNKNOWN_PROPERTIES;
  19. /**对象映射器:
  20. * 基于jackson将Java对象转为json,或者将json转为Java对象
  21. * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
  22. * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
  23. */
  24. public class JacksonObjectMapper extends ObjectMapper {
  25. public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
  26. public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
  27. public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
  28. public JacksonObjectMapper() {
  29. super();
  30. //收到未知属性时不报异常
  31. this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
  32. //反序列化时,属性不存在的兼容处理
  33. this.getDeserializationConfig().withoutFeatures(
  34. DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
  35. SimpleModule simpleModule = new SimpleModule()
  36. .addDeserializer(LocalDateTime.class,
  37. new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
  38. .addDeserializer(LocalDate.class,
  39. new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
  40. .addDeserializer(LocalTime.class,
  41. new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
  42. .addSerializer(BigInteger.class,
  43. ToStringSerializer.instance)
  44. .addSerializer(Long.class,
  45. ToStringSerializer.instance)
  46. .addSerializer(LocalDateTime.class,
  47. new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
  48. .addSerializer(LocalDate.class,
  49. new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
  50. .addSerializer(LocalTime.class,
  51. new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
  52. //注册功能模块 例如,可以添加自定义序列化器和反序列化器
  53. this.registerModule(simpleModule);
  54. }
  55. }
  1. package com.mcr.reggie.config;
  2. import com.mcr.reggie.common.JacksonObjectMapper;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.http.converter.HttpMessageConverter;
  5. import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
  6. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  7. import java.util.List;
  8. @Configuration
  9. public class WebMvcConfig implements WebMvcConfigurer {
  10. /**
  11. *扩展mvc框架消息转换器
  12. */
  13. @Override
  14. public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
  15. MappingJackson2HttpMessageConverter messageConverter=
  16. new MappingJackson2HttpMessageConverter();
  17. //创建消息转换器对象
  18. messageConverter.setObjectMapper(new JacksonObjectMapper());
  19. //设置对象转换器,底层用jackson将Java-->json
  20. converters.add(0,messageConverter);
  21. //将上面的消息转换器对象追加到mvc框架的转换器集合中,索引为0,放在最前面优先使用
  22. }
  23. }

3、全局异常处理

判断用户名是否重复时,以前都是在数据库里面进行查询咋进行判断,这样多次查询比较耗费性能,由于数据库用户名设置了唯一约束,我们可以直接把数据插入进入,如果重复就会报错,我们就可以创建一个全局异常处理:

package com.mcr.reggie.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;
/**全局异常处理
 *1.@ControllerAdvice,是Spring3.2提供的新注解,它是一个Controller增强器,
 *  可对controller中被@RequestMapping注解的方法加一些逻辑处理。最常用的就是异常处理
 *2.ControllerAdvice本质上是一个Component;
 *3.ControllerAdvice 提供了多种指定Advice规则的定义方式,默认什么都不写,
                     则是Advice所有Controller;
 *4.@ControllerAdvice 配合 @ExceptionHandler 实现全局异常处理;
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
//拦截加RestController, Controller
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException es){
        String msg="未知错误,添加失败";
        if(es.getMessage().contains("Duplicate entry")){
            //判断异常信息是否包含”Duplicate entry“
            String[] s = es.getMessage().split(" ");
            msg="用户名:"+s[2]+"重复,添加失败";
        }
        return R.error(msg);
    }
}

4、@Value()注解

import org.springframework.beans.factory.annotation.Value;
我们可以用@Value()注解来获取yaml配置文件中配置的某项值,但是注意下面这些情况下@Value()注解得不到值:

  1. @Value()得到的值赋予的变量不能由staticfinal修饰;
  2. @Value()的使用类上必须标注**@Component**注解,其中@Controller,@Configuration(自己看源码)这些注解都包含**@Component**注解,所以也是可以的;
  3. 使用@Value的类在调用的时候,不能直接通过new操作符进行调用,需要使用@Autowired进行注入;

    5、关于Long类型

  • 首先明确Long类型是long类型的封装类,Long类型是引用类型,long类型是原始类型;
  • 所以Long类型可以用null来判断是否未被赋值;
  • 但是引用类型都知道直接用==比较的话比较的是两者的地址,但是观察源码发现:

    public static Long valueOf(long l) {
      final int offset = 128;
      if (l >= -128 && l <= 127) { // will cache
          return LongCache.cache[(int)l + offset];
      }
      return new Long(l);
    }
    
  • 对于-128127的每一个数字都提前做了缓存,当我们比较两个Long类型数据时就是比较两个对象的引用,而当数据大小位于该区间时,返回的引用类型便是已经存在cache的引用,也就是说相同的数字,而当数据大小位于该区间之外时,比较就需要这样比较(采用.longValue()直接比较值)

    Long a=xxx;
    Long b=xxx;
    boolean t=a.longValue()==b.longValue();
    

    引申:同样同Long类型一样,Integer类型也是一样,-128**-**127范围内比较直接用==即可,范围之外比较用:

    Integer c=255;
    Integer d=255;
    int i=c.compareTo(d);
    /*当c>d时i=1;
    当c<d时i=-1;
    当c==d时i=0;*/
    

    新知识

    1、mybatis-plus公共字段自动填充

    当多张表中有相同字段时,我们可以使用mybatis-plus公共字段自动填充功能来将这些公共字段在某个地方进行统一处理,来简化开发;
    Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
    实现步骤∶

  1. 在实体类的公共字段的属性上加入@TableField注解,指定自动填充的策略;
  2. 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObiectHandler接口;
    ...//在公共属性上添加此属性
    @TableField(fill = FieldFill.INSERT)//插入时填充字段
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)//插入和更新时填充字段
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    ...
    
    MetaObjectHandler实现类里面无法获取httpsession也就无法获取需修改或者添加的人员的**id**此时我们可以用ThreadLocal这个类(jdk提供的类);
    在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都居于相同的一个线程∶
  • LoginCheckFilterdoFilter方法;
  • EmployeeControllerupdate方法;
  • MyMetaObjectHandlerupdateFill方法;

什么是ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法∶
public void set(T value):设置当前线程的线程局部变量的值
public T get():返回当前线程所对应的线程局部变量的值
我们可以在LoginCheckFilterdoFilter方法中获取当前登录用户id,并调用ThreadLocalset方法来设置当前线程的线程局部变量的值(用户id),然后在MetaObjectHandler实现类的xxxFill方法中调用ThreadLocalget方法来获得当前情(用白id);

package com.mcr.reggie.utils;
/**
 * 基于ThreadLocal封装的工具类
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal=new InheritableThreadLocal<>();
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }
    public static Long get(){
        return threadLocal.get();
    }
}
...
BaseContext.setCurrentId((Long) request.getSession().getAttribute("emp"));
...//注意一定要在“放行代码”前面执行
filterChain.doFilter(request,response);
...

因为:
image.png

package com.mcr.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.mcr.reggie.utils.BaseContext;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
/**
 * 自定义元数据对象处理器
 */
@Configuration
public class MyMetaObjecthandler implements MetaObjectHandler {
    @Override//插入操作
    public void insertFill(MetaObject metaObject) {
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", BaseContext.get());
        metaObject.setValue("updateUser", BaseContext.get());
    }

    @Override//修改操作
    public void updateFill(MetaObject metaObject) {
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", BaseContext.get());
    }
}

之后当每次插入数据或者修改数据时就不需要对这两个属性进行操作了,这里自动操作了这两个属性;

2、DTO

DTO,全称为Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输。

3、springboot使用事务

  • 使用事务的方法上添加注解@Transactional
  • 主启动类上添加注解@EnableTransactionManagement开启注解;

    4、公共文件上传下载类

    用来实现实现图片上传存储并展示

    package com.mcr.reggie.controller;
    import com.mcr.reggie.common.R;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    import javax.servlet.ServletOutputStream;
    import javax.servlet.http.HttpServletResponse;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.util.UUID;
    /**
    * 文件上传与下载
    */
    @Slf4j
    @RestController
    @RequestMapping("/common")
    public class CommonController {
      @Value("${reggie.path}")
      private String BasePath;//自定义路径类
      /**
       *文件上传
       */
      @PostMapping(value = "/upload")
      public R<String> upload(MultipartFile file){
          //参数名必须和前端上传的name属性值一致
          log.info("上传文件信息"+file.toString());
          String filename = file.getOriginalFilename();
          String suffix = filename.substring(filename.lastIndexOf("."));
          filename=UUID.randomUUID().toString()+suffix;
          File dir=new File(BasePath);
          if(!dir.exists()){
              dir.mkdir();
          }
          try {
              file.transferTo(new File(BasePath+filename));
          } catch (IOException ioException) {
              ioException.printStackTrace();
          }
          return R.success(filename);
      }
      /*文件下载
       */
      @GetMapping(value = "/download")
      public void download(String name, HttpServletResponse response){
          try {
              //输入流,通过输入流读取文件内容
             FileInputStream fileInputStream=new FileInputStream(new File(BasePath+name));
              //输出流,通过输出流将文件写回浏览器,在浏览器展示图片
              ServletOutputStream outputStream = response.getOutputStream();
              response.setContentType("image/jpeg");
              //response.setContentType(xxx)作用是使客户端浏览器,区分不同种类的数据,
              //并根据不同的种类调用浏览器内不同的程序嵌入模块来处理相应的数据。
              int len=0;
              byte[] bytes=new byte[1024];
              while ((len=fileInputStream.read(bytes))!=-1){
                  outputStream.write(bytes,0,len);
                  outputStream.flush();
              }
              outputStream.close();
              fileInputStream.close();
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
    }
    

    5、stream流

    //假设有2个类
    @Data
    class pig{int id;int age;String name;}
    @Data
    class egg{int id;int age;String name;}
    //并有这两个类的list集合
    List<pig> pp=...
    List<egg> ee=new Arrsylist<>();  
    //则使用stream流来将pp里面的值都拷贝到ee中,其中pp中的name属性不拷贝,重新进行设置;
    ee=pp.stream().map((item)->{//这里是lambda表达式
      pig pp1=new pig();
      //使用BeanUtils工具进行对象拷贝
      BeanUtils.copyProperties(item,pp1,"name");
      //(将第一个参数的值拷贝到第二个属性)
      pp1.setName("xxx");
      return pp1;//一定记得要返回
    }).collect(Collectors.toList());
    

    6、AtomicInteger

    AtomicInteger amount=new AtomicInteger(0);//初始值设置为0
    

    AtomicInteger保持线程运算的原子性:

  • 支持原子操作的Integer类;

  • 主要用于在高并发环境下的高效程序处理。使用非阻塞算法来实现并发控制;

    AtomicInteger的常用的方法

  • get()方法,获取当前value的值,无锁:amount.get()

  • set()方法:设置指定值,无锁,多线程使用会出问题,一般用于初始化数值;
  • getAndSet()方法:获取旧的值(当前值),重新设置新的值;
  • addAndGet()方法:先加上指定值再获取当前值;
  • BigDecimal.multiply(BigDecimal).multiply就是乘的意思;
  • 具体见别人博客:—->链接

    7、项目部署

    手动部署

    直接java -jar xxx部署springboot项目那种属于是霸屏部署,页面一关服务也就停止了,线上程序不会采用这种方法,并且线上程序不会把日志输出在控制台,而是单独输出到日志文件中,方便运维查询,做法如下:
    **nohup**命令∶英文全称no hang up(不挂起),用于不挂断地运行指定命令,退出终端不会影响程序的运行

  • 语法格式∶ **nohup Command命令 [ Arg..][&]**

  • 参数说明∶
    • Command**命令**∶要执行的一些命令;
    • &∶让命令在后台运行;
  • 举例∶后台运行jave-jar命令,并将日志输出到hello.log文件(用的相对路径,即在当前文件目录下):
    • **nohup java-jar boot工程名.jar &> hello.log &**

如果遇到日志报错nohup: failed to run command ‘java-jar’: No such file or directory的话,解决方法如下:

  1. **source /etc/profile;** **nohup java-jar boot工程名.jar &> hello.log &**
  2. 配置配置文件vim ~/.bash_profile,之后重新导入source ~/.bash_profile,如下:
    1. image.png
    2. 根据prohup安装位置进行配置:image.png

停掉的话就按照杀进程的方式:kill -9 进程号

shell脚本自动部署

1、安装git:
查看安装的版本:
image.png
安装此版本git:yum install git
2、安装maven;
3、编写脚本(不会写┭┮﹏┭┮)
之后直接在linux上把项目拉进来直接进行部署;

8、修改RedisTemplate默认的序列化器

RedisTemplate默认的序列化器是_JdkSerializationRedisSerializer_我们要将_key_的序列化器(value不改)改为StringRedisSerializer,添加配置类如下:

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(
        RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setKeySerializer(new StringRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}
  • 当我们的数据存储到Redis的时候,我们的键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的。RedisTemplate默认使用的是JdkSerializationRedisSerializerStringRedisTemplate默认使用的是StringRedisSerializer
  • _JdkSerializationRedisSerializer_:使用JDK提供的序列化功能。 优点是反序列化时不需要提供类型信息(class),但缺点是需要实现Serializable接口,还有序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存。还有就是使用此序列化器不便于观察插入值,如下:
    • image.png
  • StringRedisSerializer:简单的字符串序列化;
  • 这里只修改了key的序列化器,value的序列化器没有改,效果如下:image.png

    9、Spring Cache

    介绍

  • Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

  • Spring Cache提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。
  • CacheManagerSpring提供的各种缓存技术抽象接口。
  • 针对不同的缓存技术需要实现不同的CacheManager

image.png

常用注解

image.png
在启动类上使用**@EnableCaching**注解开启缓存支持;

使用

最原始的CacheManager

image.png
这个服务是基于内存的,当服务停掉之后,缓存数据就没了;(

/*首先在主启动类加上注解:@EnableCaching*/
/*然后具体使用此缓存功能的类如下:*/
class xxx{//某个类
    @Autowired
    private CacheManager cacheManager;
    /*将方法返回值放入缓存
    value:缓存的名称,每个缓存名称下面可以有多个key
    key:缓存的key*/
    @CachePut(value = "userCacheTest",key = "#result")
    /*key支持spEL表达式,其中
    "1、#result"就代表返回值;
    "2、#root"内置对象("#root.method"当前方法;"#root.methodName"当前方法名...)
    "3、#参数名"(和方法形参名一致)就是得到方法的参数
        (也可以用"#root.args[i]"或"#pi"来获得第“i+1”个参数)
    */
    /*其他常用属性
    1、condition:当满足什么条件时才执行其本缓存注解
    2、unless:和condition相反,当满足某个条件时不执行此注解
    */
    public R<String> save(xxxx){
        ...
        result...
        /*注意:返回结果必须实现序列化接口“implements Serializable”
        public class R<T> implements Serializable {...
        */
    }
}

如果要同时删除多个特定valueredis缓存:

@Caching(evict = {
            @CacheEvict(value = "aaa",allEntries = true),
            @CacheEvict(value = "bbb",allEntries = true)
    })

底层用redis缓存数据

pom.xml导入依赖
<!--导入redis-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring cache操作redis-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

application.yaml配置
spring:
  cache:
    redis:
      time-to-live: 300000 #设置缓存有效期(单位:ms)

其余同上“最原始的CacheManager
注意:返回值对象必须实现序列化接口implements Serializable

补:
  • spring cache中,**@CacheEvict**是清除缓存的注解。其中注解参数可以只有value,加key的话意思是清除在value值空间中的key值数据,此时默认在当前注解方法成功执行之后再清除。这时候就会存在一个问题,也许你的注解方法成功执行了删除操作,但是后续代码抛出异常导致未能清除缓存,下次查询时依旧从缓存中去读取,这时查询到的结果值是删除操作之前的值。
  • 有一个简单的解决办法,在注解参数里面加上beforeInvocation属性为true,意思是说当执行这个方法之前执行清除缓存的操作,这样不管这个方法执行成功与否,该缓存都将不存在。
  • 当注解参数加上allEntries属性为true时,意思是说这个清除缓存是清除当前value值空间下的所有缓存数据。
  • 注解里的属性value不是redis中存储的key-value键值对的value,而是指定缓存组件的名称,即将方法的返回结果放在哪个缓存中,属性定义为数组,可以指定多个缓存;而属性key就是redis中存储的key-value键值对的key

    @Cacheable注解的属性介绍

    image.png

    10、Sharding-JDBC框架实现读写分离

    背景

    image.png

    Sharding-JDBC简介

    Sharding-JDBC定位为轻量级Java框架,在JavaJDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。使用**Sharding-JDBC**可以在程序中轻松的实现数据库读写分离。

  • 适用于任何基于JDBCORM框架,如:JPA,Hibernate,Mybatis,Spring JDBC Template或直接使用JDBC

  • 支持任何第三方的数据库连接池,如:DBCP,C3PO,BoneCP,Druid,HikariCP等。
  • 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSOL以及任何遵循SQL92标准的数据库。

    入门操作

    先提条件是必须有主从复制关系的多个MySQL;

  • 192.168.80.130:主数据库;

  • 192.168.80.131:从数据库1;
  • 192.168.80.132:从数据库2;

    pom.xml导入依赖

    <!--导入Sharding-JDBC-->
    <dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.0.0-RC1</version>
    </dependency>
    

    application.yaml配置读写分离规则

    spring:
      shardingsphere:
        datasource:
          names: master,slave1,slave2 #这里名字不固定,与下面照应即可
          master:
            type: com.alibaba.druid.pool.DruidDataSource
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://192.168.80.130:3306/reggie?serverTimezone=Asia/Shanghai
            username: root
            password: 12345ssdlh
          slave1:
            type: com.alibaba.druid.pool.DruidDataSource
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://192.168.80.131:3306/reggie?serverTimezone=Asia/Shanghai
            username: root
            password: 12345ssdlh
          slave2:
            type: com.alibaba.druid.pool.DruidDataSource
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://192.168.80.132:3306/reggie?serverTimezone=Asia/Shanghai
            username: root
            password: 12345ssdlh
        masterslave: #读写分离规则
          load-balance-algorithm-type: round_robin
          #从库负载均衡策略(这里是轮询)
          name: dataSource
          #最终数据源名称
          master-data-source-name: master
          #指定主数据库
          slave-data-source-names: slave1,slave2
          #指定从数据库(多个,分隔)
        props:
          sql:
            show: true #开启sql显示,在控制台显示sql
    

    application.yaml配置“允许bean定义覆盖配置项”

    如果只配置上面就启动项目会报错如下,因为shardingsphereDruidDataSourceAutoConfigure都默认创建各自的数据源,两者冲突了;

    APPLICATION FAILED TO START


    Description: The bean ‘dataSource’, defined in class path resource [org/apache/shardingsphere/shardingjdbc/spring/boot/SpringBootConfiguration.class], could not be registered. A bean with that name has already been defined in class path resource [com/alibaba/druid/spring/boot/autoconfigure/DruidDataSourceAutoConfigure.class] and overriding is disabled. Action: Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true这就是提示的解决方法

解决方法如下:

spring:
   main:
     allow-bean-definition-overriding: true
     #允许bean定义覆盖配置项

测试

此时测试启动就不会不错,并且默认还创建了多个数据源(与自己配置的主从数据库的数量有关),如下:
image.png
之后的数据库操作如常即可,shardingjdbc会自动判断,增删改操作调用主数据库,查调用从数据库;

11、前后端分离开发

简介

image.png

开发流程

image.png
前端接口API接口)就是一个http的请求地址,主要就是去定义:请求路径、请求方式、请求参数、响应数据等内容!

前端技术栈(了解)

开发工县:

  • Visual Studio Code
  • hbuilder

技术框架:

  • nodejs
  • VUE
  • ElementUl
  • mock(测试,模拟后端数据)
  • webpack(前端打包工具)

    Yapi介绍

    image.png

    Swagger

    介绍

    使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。
    knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案(集成Swagger,相当于Swagger增强版)。

    使用

    pom.xml导入依赖
    <!--导入knife4j-->
    <dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.2</version>
    </dependency>
    
    添加配置类
    ```java @Configuration @EnableSwagger2 @EnableKnife4j public class WebMvcConfig implements WebMvcConfigurer { @Bean public Docket createRestApi(){
      return new Docket(DocumentationType.SWAGGER_2)
              .apiInfo(apiInfo())
              .select()
              .apis(RequestHandlerSelectors.basePackage("com.mcr.reggie.controller"))
              .paths(PathSelectors.any())
              .build();
    
    } private ApiInfo apiInfo(){//描述接口文档
      return new ApiInfoBuilder()
              .title("瑞吉外卖")
              .version("1.0")
              .description("瑞吉外卖接口文档")
              .build();
    
    } @Override //静态资源映射 public void addResourceHandlers(ResourceHandlerRegistry registry){
      registry.addResourceHandler("doc.html")
              .addResourceLocations("classpath:/META-INF/resources/");
      registry.addResourceHandler("/webjars/**")
              .addResourceLocations("classpath:/META-INF/resources/webjars/");
    
    } }
静态资源位置:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/23158036/1653901520249-778cca7f-16e3-4b42-96de-90ae39f32947.png#clientId=ub42aea13-85e9-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=167&id=u228e9f9e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=188&originWidth=402&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10976&status=done&style=none&taskId=u3d1c6d99-7ccc-4ba3-b3fc-0c02a6a5c30&title=&width=357.3333333333333)
<a name="cvTgE"></a>
##### 过滤器
过滤器放行这五个请求路径:<br />`"/doc.html",<br />"/webjars/**",<br />"/swagger-resources",<br />"/v2/api-docs"`
<a name="SZLAo"></a>
##### 展示
之后访问`http://ip:port/doc.html`就可以访问**swagger**的页面,可以查看所有的接口信息;<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/23158036/1653902061358-8e81d120-cdac-4869-b6c4-95803ca1b739.png#clientId=ub42aea13-85e9-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=556&id=u641aee9b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=626&originWidth=903&originalType=binary&ratio=1&rotation=0&showTitle=false&size=65384&status=done&style=none&taskId=u037c72bb-25a2-4ed6-ac51-06a5c9a85e9&title=&width=802.6666666666666)
<a name="UWqMa"></a>
#### 常用注解
![image.png](https://cdn.nlark.com/yuque/0/2022/png/23158036/1653902534993-1fae552b-4312-4398-9429-1acd7c7e25b0.png#clientId=ub42aea13-85e9-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=196&id=u0f5d8133&margin=%5Bobject%20Object%5D&name=image.png&originHeight=220&originWidth=560&originalType=binary&ratio=1&rotation=0&showTitle=false&size=96008&status=done&style=none&taskId=uf9ec2fbe-3933-4a75-8ab1-7295e31ccae&title=&width=497.77777777777777)<br />使用示例:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/23158036/1653904876482-f89e34de-6bb4-4b04-bbc2-8672a90ae840.png#clientId=ub42aea13-85e9-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=464&id=u6d8dc65e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=522&originWidth=723&originalType=binary&ratio=1&rotation=0&showTitle=false&size=86719&status=done&style=none&taskId=u532e6011-02d3-408b-aa2e-655227ab42b&title=&width=642.6666666666666)
<a name="X1Xvr"></a>
## 12、邮箱发送验证码实现
<a name="p23iV"></a>
### `pomx.ml`:
```xml
<!--邮箱发送验证码依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

application.yaml

spring:
  mail:
    username: 1954976119@qq.com #邮箱账号
    password: lyvgwsehursaefbd  #邮箱授权码
    host: smtp.qq.com #邮箱服务器地址,qq邮箱则为smtp.qq.com;163邮箱是smtp.163.com
    default-encoding: UTF-8 # 编码集
    protocol: smtp #服务协议
    test-connection: true
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true
sendMail:
  prefix: "【reggie外卖】提醒您,您本次登陆的验证码是”"
  suffix: "“"
  subject: "邮箱验证码登录"

发送邮件工具类:

@Component
public class MailUtils {
    @Value("${sendMail.prefix}")
    private String prefix;
    @Value("${sendMail.suffix}")
    private String suffix;
    @Value("${sendMail.subject}")
    private String subject;
    @Value("${spring.mail.username}")
    private String from;//获取发件邮箱
    @Autowired
    private JavaMailSender mailSender;
    public void sendByQQEmail(String sendMail,String code){
        String msg=prefix+code+suffix;
        SimpleMailMessage message=new SimpleMailMessage();
        message.setFrom(from);//设置发件人
        message.setTo(sendMail);//设置收件人
        message.setSubject(subject);//设置邮箱主题
        message.setText(msg);//设置邮件内容
        mailSender.send(message);//发件
    }
}

13、配置redis连接池

pom.ml

<!--导入redis-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--采用Lettuce使用连接池,要依赖commons-pool2-->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-pool2</artifactId>
</dependency>

application.yaml

spring:
  redis:
    host: 124.70.84.192
    port: 6380
    password: 12345ssdlh
    database: 0
    connect-timeout: 5000ms
    lettuce:
      pool:
        max-active: 20 # 连接池最大连接数(使用负值表示没有限制)
        max-idle: 10 # 连接池中的最大空闲连接
        min-idle: 5 # 连接池中的最小空闲连接
        max-wait: 5000ms # 连接池最大阻塞等待时间(使用负值表示没有限制)