1 快速使用

1.1 创建入门项目

image.png

  1. 创建一个新项目(File->new->Project->Spring initalizr)
  2. 填写项目信息
  3. 选择初始化的组件(后期可以通过Pom添加)
  4. 创建成功

    1.2 项目目录结构

    1.2.1 项目目录结构

    image.png

  5. 主启动类(XxxApplication)

  6. 项目配置文件(application.properties)
  7. 项目构建文件(pom.xml)
  8. 测试类(XxxApplicationTests)

1.2.2 主启动类

  1. @SpringBootApplication
  2. public class DemoApplication {
  3. public static void main(String[] args) {
  4. SpringApplication.run(DemoApplication.class,args);
  5. }
  6. }

main方法的参数args为运行时参数,可以在程序启动时通过program arguments来修改启动的参数。

对于主启动类的详细分析(跳转)

1.2.3 项目配置文件

配置文件默认生成为application.properties,也可以使用application.yml
(1) YAML
基本语法:

  • 格式-key:value(空格必须有)
  • 属性和值是大小写敏感的
  • 使用缩进表示层级关系
  • 缩进时不允许使用Tab键,只允许使用空格。
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可。
  • 字符串默认不用加上单引号或者双引号,使用单引号会对转义字符进行转义 ```yaml

person: lastName: hello age: 18 boss: false birth: 2017/12/12 maps: {k1: v1,k2: 12} lists:

  - lisi
  - zhaoliu
dog:
  name: 小狗
  age: 12


**(2) 配置文件值注入**<br /> 使用@ConfigurationProperties 或 @Value 注解将配置文件的值注入对象

-  @ConfigurationProperties: SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定;prefix = "person":配置文件中哪个下面的所有属性进行一一映射;  需要@Component注册组件;
-  @Value({"person.lastName"})在单个属性上注入
```java
@Component
@ConfigurationProperties(prefix = "person")
public class Person {

    private String lastName;
    private Integer age;
    private Boolean boss;
    private Date birth;

    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;

自定义的类和配置文件绑定一般没有提示,在pom引入

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

(3) 配置文件的拆分
默认使用application.yml 主配置文件,可以通过来文件名指定多个环境版本( application-{profile}.properties/yml )

# 用来指定使用哪个环境
spring:
  profiles:
    active: dev

(4) 配置文件的加载顺序
Spring Boot启动会扫描以下位置的application.properties或者application.yml文件作为Spring Boot的默认配置文件,优先级由高到底,高优先级的配置会覆盖低优先级的配置,并互补配置。

  1. file:/config/
  2. file:/
  3. classpath:/config/
  4. classpath:/

在使用SpringCloud Config配置中心时,会使用到bootstrap.yml(bootstrap.properties)文件,bootstrap优先级比application更高

1.2.4 项目构建文件

<!--springboot项目的父项目,管控依赖 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<!--springboot项目启动器,如web启动器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Pom文件对项目依赖进行管理

  • 通过配置的maven仓库进行依赖的获取(默认中央仓库),中央仓库没有的依赖需要从私库获取或者通过手动引入
  • 默认无需关注版本号,自动版本仲裁
  • 约定俗成的 spring-boot-starter-* 为官方提供的场景依赖,第三方的使用 *-spring-boot-starter

自动配置原理(跳转)

1.2.5 测试类

单元测试基本操作(跳转)


2 WEB开发

2.1 静态资源访问

2.1.1 默认静态资源文件夹

image.png

  • resources/static (新项目只有 /static)
  • resources/public
  • resources/resources
  • resources/META-INF/resources

SpringBoot放置静态资源的目录,只要把静态资源放到这几个目录下,就能直接通过当前项目根路径 + / + 静态资源名访问到。

spring:
  mvc:
    static-path-pattern: /res/**

  resources:
    static-locations: [classpath:/haha/]

默认情况下SpringBoot是帮你映射的路径是 /**,可通过修改配置添加前缀。

2.1.2 自定义样式

自定义样式放在把对应的文件放在静态资源文件夹即可(修改默认访问路径会导致失效),欢迎页(index.html)、横幅(banner.txt)、网站图标(favicon.ico)

2.1.3 静态资源配置原理

WebMvcAutoConfiguration里的addResourceHandler进行了配置,
即请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。

2.2 模板引擎

Spring Boot支持Thymeleaf,freemarker等模板引擎,通过模板引擎来展示页面
前后端分离开发不再使用这种模板引擎,模板引擎现在用来生产代码等,如果用到请参考官方文档。

2.3 请求处理

2.3.1 请求映射

使用注解@RequestMapping、@xxxMapping(RESTFUL风格)

@Controller
public class TestController {

    @RequestMapping("/t1")
    public String test1(){
        //默认访问classpath:/templates/test.html
        return "test";
    }

}

2.3.2 请求参数

使用注解@PathVariable、@RequestParam、@RequestBody

@GetMapping("/save")
public Map getMethod(@RequestParam String userName){
    Map<String,Object> map = new HashMap<>();
    map.put("content",content);
    return map;
}

@PostMapping("/save")
public Map postMethod(@RequestBody String content){
    Map<String,Object> map = new HashMap<>();
    map.put("content",content);
    return map;
}

@GetMapping("/save/{id}")
public Map varMethod(@PathVariable String id){
    Map<String,Object> map = new HashMap<>();
    map.put("content",content);
    return map;
}

2.3.3 请求原理

原理:即springmvc的原理

2.3.4 扩展使用SpringMVC


//应为类型要求为WebMvcConfigurer,所以我们实现其接口
//可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 浏览器发送/test , 就会跳转到test页面;
        registry.addViewController("/test").setViewName("test");
    }
}

2.3.5 优雅的参数校验

(1)引入JSR-303验证规范的Validator校验框架

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

(2)实体类

@Data
public class User {

    @NotBlank(message = "名字为必填项")
    private String name;

    @Length(min = 8, max = 16, message = "password长度必须位于8到12之间")
    private String password;

    @Email(message = "请填写正确的邮箱地址")
    private String email;

    //自定义注解
    @EnumCheck(clazz = SexEnum.class)
    private String sex;

    //使用@Valid支持嵌套
    @Valid
    private Address address;
}

(3)接口调用

@RestController
@Slf4j
@RequestMapping("/valid")
public class UserController {

    @PostMapping("/testJson")
    public String testJson(@Valid @RequestBody User user) {
        return "testJson valid success";
    }
}

(4)自定义注解校验

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Constraint(validatedBy = EnumValidator.class)
public @interface EnumCheck {

    /**
     * 枚举的类型
     */
    Class<? extends Enum<?>> clazz();
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
public class EnumValidator implements ConstraintValidator<EnumCheck, String> {

    Class<? extends Enum<?>> clazz;

    @Override
    public void initialize(EnumCheck constraintAnnotation) {
        clazz = constraintAnnotation.clazz();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        List<Object> types = EnumUtil.getFieldValues(clazz, "type");
        return StrUtil.isBlank(s) || types.contains(s);
    }
}

2.3.6 优雅的结果响应

@RestControllerAdvice
public class GlobalResponseHandler<T> implements ResponseBodyAdvice<Object> {

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof String) {
            return objectMapper.writeValueAsString(Result.success(body));
        }
        if (body instanceof Result) {
            return body;
        }
        return Result.success(body);
    }
}

@Data
@ToString
public class Result<T> implements Serializable {
    private static final long serialVersionUID = 1L;

    private Integer code;

    private String msg;

    private T data;

    public Result(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public Result(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static <T> Result<T> success() {
        return new Result<>(HttpStatus.OK.value(), HttpStatus.OK.getReasonPhrase(), null);
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(HttpStatus.OK.value(), HttpStatus.OK.getReasonPhrase(), data);
    }

    public static <T> Result<T> success(String msg, T data) {
        return new Result<T>(HttpStatus.OK.value(), msg, data);
    }

    public static<T> Result<T> fail() {
        return new Result<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),null);
    }

    public static Result<String> fail(String msg) {
        return new Result<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg);
    }

    public static Result<String> fail(Integer code ,String msg) {
        return new Result<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg);
    }

}

2.4 异常处理

2.4.1 默认的异常处理

程序中出现了异常 SpringBoot 就会请求 /error 的 url ;
SpringBoot 提供BasicExceptionController 来处理 /error 请求,然后跳转到默认显示异常的页面来展示异常信息。
接下来就是自定义异常错误页面了,方法很简单,就是在目录 src/main/resources/templates/ 下定义一个叫 error 的文件,可以是 jsp 也可以是 html 。

2.4.2 优雅的异常处理

@RestControllerAdvice 
public class GlobalExceptionHandler {

    //自定义的异常进行处理
    @ExceptionHandler(BusinessException.class) 
    public Result businessExceptionHandler(BusinessException e){
        return Result.error().code(e.getErrorCode()).message(e.getErrorMsg());
    }

    //其他异常统一处理
    @ExceptionHandler(Exception.class)
    public Result zeroException(Exception e){ 
        return Result.error().message(e.getMessage());
    }

    //Validator校验异常
    @ExceptionHandler(value = {BindException.class, ConstraintViolationException.class, MethodArgumentNotValidException.class})
    public ResponseEntity<Result<String>>  exceptionHandler(Exception  e) {
        Result<String> resp = null;
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
            resp = Result.fail(HttpStatus.BAD_REQUEST.value(),
                    ex.getBindingResult().getAllErrors().stream()
                            .map(ObjectError::getDefaultMessage)
                            .collect(Collectors.joining("; "))
            );
        } else if (e instanceof ConstraintViolationException) {
            ConstraintViolationException ex = (ConstraintViolationException) e;
            resp = Result.fail(HttpStatus.BAD_REQUEST.value(),
                    ex.getConstraintViolations().stream()
                            .map(ConstraintViolation::getMessage)
                            .collect(Collectors.joining("; "))
            );
        } else if (e instanceof BindException) {
            BindException ex = (BindException) e;
            resp = Result.fail(HttpStatus.BAD_REQUEST.value(),
                    ex.getAllErrors().stream()
                            .map(ObjectError::getDefaultMessage)
                            .collect(Collectors.joining("; "))
            );
        }
        return new ResponseEntity<>(resp,HttpStatus.BAD_REQUEST);
    }
}

@Data
public class BusinessException extends RuntimeException{

    private Integer errorCode;

    private String errorMsg;

    public BusinessException(String code) {
        this.code = code;
        this.errMsg = getMessage(code);
    }

    //异常代码编写在国际化文件中
    private String getMessage(String errCode) {
        ResourceBundle bundle = ResourceBundle.getBundle("i18n/messages", Locale.CHINA);
        return bundle.getString(errCode);
    }
}

2.5 切面编程

2.5.1 springboot使用切面

(1)引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

(2)编写切面类

@Aspect
@Component
public class MyAspect {
    @Before("execution(* com.demo.service.*.*(..))")
    public void before(JoinPoint joinPoint){
        System.out.println("前置通知");
        joinPoint.getTarget();//目标对象
        joinPoint.getSignature();//方法签名
        joinPoint.getArgs();//方法参数
    }
}

2.6 日志处理

2.6.1 日志

springboot默认的日志门面是SLF4J ,日志框架是Logback
如果在springboot的resources目录下配置 logback-spring.xml, logback-spring.groovy, logback.xml or logback.groovy,则spring就不在使用默认的日志配置
Logback 日志级别 TRACE < DEBUG < INFO < WARN < ERROR

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
  <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
  <property name="LOG_HOME" value="./logs/"/>

  <!-- 彩色日志依赖的渲染类 -->
  <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
  <conversionRule conversionWord="wex"
                  converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
  <conversionRule conversionWord="wEx"
                  converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>

  <!-- 日志格式 -->
  <property name="LOG_PATTERN"
            value="%boldWhite(%date{yyyy-MM-dd HH:mm:ss}) | %highlight(%-5level) | %boldYellow(%thread) | %boldMagenta(traceId:%X{traceId}) | %boldGreen(%logger) ---> %boldCyan(%msg%n)"/>

  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
      <pattern>${LOG_PATTERN}</pattern>
    </layout>
  </appender>

  <!-- 按照每天生成日志文件 INFO_FILE-->
  <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 正在记录的日志文件的路径及文件名 -->
    <file>${LOG_HOME}/info.log</file>
    <!--日志文件最大的大小-->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <MaxFileSize>10MB</MaxFileSize>
    </triggeringPolicy>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>INFO</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!--日志文件输出的文件名-->
      <FileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
      <!--日志文件保留天数-->
      <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
        <maxFileSize>10MB</maxFileSize>
      </timeBasedFileNamingAndTriggeringPolicy>
      <!--日志文件保留天数-->
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
      <pattern>${LOG_PATTERN}</pattern>
      <!-- 设置字符集 -->
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <!-- 按照每天生成日志文件 WARN_FILE-->
  <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 正在记录的日志文件的路径及文件名 -->
    <file>${LOG_HOME}/warn.log</file>
    <!--日志文件最大的大小-->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <MaxFileSize>10MB</MaxFileSize>
    </triggeringPolicy>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>WARN</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!--日志文件输出的文件名-->
      <FileNamePattern>${LOG_HOME}/warn-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
      <!--日志文件保留天数-->
      <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
        <maxFileSize>10MB</maxFileSize>
      </timeBasedFileNamingAndTriggeringPolicy>
      <!--日志文件保留天数-->
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
      <pattern>${LOG_PATTERN}</pattern>
      <!-- 设置字符集 -->
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <!-- 按照每天生成日志文件 ERROR_FILE-->
  <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 正在记录的日志文件的路径及文件名 -->
    <file>${LOG_HOME}/error.log</file>
    <!--日志文件最大的大小-->
    <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
      <MaxFileSize>10MB</MaxFileSize>
    </triggeringPolicy>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>ERROR</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!--日志文件输出的文件名-->
      <FileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
      <!--日志文件保留天数-->
      <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
        <maxFileSize>10MB</maxFileSize>
      </timeBasedFileNamingAndTriggeringPolicy>
      <!--日志文件保留天数-->
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
      <pattern>${LOG_PATTERN}</pattern>
      <!-- 设置字符集 -->
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <!--控制台打印SQL语句,去重additivity="false"-->
  <logger name="com.firstlinkgroup.paycenter.dao" level="debug" additivity="false">
    <appender-ref ref="CONSOLE"/>
  </logger>

  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="INFO_FILE"/>
    <appender-ref ref="WARN_FILE"/>
    <appender-ref ref="ERROR_FILE"/>
  </root>
</configuration>

2.6.2 切面日志

@Aspect
@Component
@Slf4j
public class IcbcLogAspect {

    @Pointcut("execution(public * com.demo.service.impl.DemoServiceImpl.*(..))")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void before(JoinPoint jp) {
        MDC.put("traceId", IdUtil.simpleUUID());
        log.info("方法{},参数{}", jp.getSignature().getName(), JSONUtil.toJsonStr(jp.getArgs()));
    }

    @AfterReturning(value = "pointcut()", returning = "returnValue")
    public void after(JoinPoint jp, Object returnValue) {
        log.info("方法{},返回值{}", jp.getSignature().getName(), JSONUtil.toJsonStr(returnValue));
        MDC.clear();
    }

}

MDC作用:方便日志追踪

2.7 拦截器

2.7.1拦截器作用

可以对URL路径进行拦截,可以用于权限验证、解决乱码、操作日志记录、性能监控、异常处理等

2.7.2 使用拦截器

(1)配置拦截器
创建拦截器类,实现HandlerInterceptor接口,重写preHandle方法,在该方法中编写业务拦截的规则

public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        System.out.println("======1=====");
        return true;//返回true 放行  返回false阻止
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {
        System.out.println("=====2=====");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {
        System.out.println("=====3=====");
    }
}

(2)注册拦截器

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor())//添加拦截器
                .addPathPatterns("/hello/**")//添加拦截的请求路径
                .excludePathPatterns("/hello/world");//添加排除那些请求路径不经过拦截器
    }
}
//springboot2.x 静态资源在自定义拦截器之后无法访问的解决方案
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**") //代表以什么样的请求路径访问静态资源
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/templates/");

    }
}

2.8 自定义starter

(1)新建工程
命名为xxx-spring-boot-starter
(2)pom依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
</dependencies>

(3)自定义一个实体类映射配置信息

@ConfigurationProperties(prefix = "demo.kafka" )
@Data
public class KafkaProperty {
    private String topics;
    private String groupId;
}

(4)创建自动配置类XXXXAutoConfiguration
@ConditionalOnProperty:指定的属性是否有指定的值
@ConditionalOnBean:当SpringIoc容器内存在指定Bean的条件,使用这个可以完成

@Configuration
@EnableConfigurationProperties(KafkaProperty.class)
@ConditionalOnProperty(prefix = "oiltour.consumer",name = "isopen",havingValue = "true")
public class AutoConfiguration {

    @Bean(name = "kafkaProperty")
    public KafkaProperty kafkaProperty() {
        return new KafkaProperty();
    }
}

(5)编写spring.factories文件
在resources文件夹META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.example.springbootstarter.config.AutoConfiguration

(6)生成自定义的starter
执行mvn clean install

2.9 其他功能

2.9.1 ApplicationRunner的使用
SpringBoot中提供两个接口CommandLineRunner和ApplicationRunner进行一些额外的操作,比如读取配置文件、数据库操作等自定义的内容。

@Component
@Slf4j
@Order(1)
public class TestApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("=======项目启动,ApplicationRunner执行一次=======");
    }
}
  • ApplicationRunner中run方法的参数为ApplicationArguments,而CommandLineRunner接口中run方法的参数为String数组。
  • ApplicationRunner默认优先级比CommandLineRunner高

3 原理

3.1 自动装配

3.1.1 @SpringBootApplication作用

@SpringBootApplication是一个复合注解,主要包括以下三个注解
@SpringBootConfiguration:配置Spring容器,也即 JavaConfig 形式的 Spring IoC 容器的配置类所使用
@ComponentScan:指定组件扫描的类型和范围,默认为入口类所在子包以及子包
@EnableAutoConfiguration:注解启用自动配置,其可以帮助 SpringBoot 应用将所有符合条件的@Configuration 配置都加载到当前 IoC 容器之中
image.png

3.1.2 流程解析

  1. @SpringBootConfiguration标明启动类是一个配置类
  2. 当启动类启动的时候SpringBoot会通过@EnableAutoConfiguration中的@AutoConfigurationPackage自动装配包
  3. @AutoConfigurationPackage中的@ImportRegistrar.class)标注包及其子包要被扫描
  4. @ComponentScan去扫描这些包下的添加@component的类
  5. 然后@Import(AutoConfigurationImportSelector.class)用于向容器中导入自动配置组件
    1. AutoConfigurationImportSelector继承了DeferredImportSelector实现了 ImportSelector,重写selectImports()(SPI技术),
    2. 调用getCandidateConfigurations(),然后通过
    3. SpringFactoriesLoader.loadFactoryNames()获取资源文件
    4. META-INF/spring.factories中的XXXXAutoConfiguration(默认的配置位于springboot-autoconfigure下)

3.1.2 SPI

SPI的英文名称是Service Provider Interface,是Java 内置的服务发现机制。

  • 定义接口
  • 提供方’META-INF/services’目录下新建一个名称为接口全限定名的文件,内容为接口实现类全限定名
  • 调用方通过ServiceLoader.load方法加载接口的实现类实例
//1.定义一个接口
public interface UploadCDN {
     void upload(String url);
 }
//2.定义两个接口实现类
public class Qiyi implements UploadCDN {  //上传爱奇艺
    @Override
    public void upload(String url) {
        System.out.println("upload to qiyi cdn");
    }
}

public class ChinaNet implements UploadCDN {//上传网宿
    @Override
    public void upload(String url) {
        System.out.println("upload to chinaNet cdn");
    }
}
//3.在resources下的‘META-INF/services’目录下新建一个名称为接口全限定名(Upload)的文件,内容为接口实现类全限定名。(Qiyi/ChinaNet)

//4.通过ServiceLoader.load方法加载接口的实现类实例
public static void main(String[] args) {
         ServiceLoader<UploadCDN> uploadCDN = ServiceLoader.load(UploadCDN.class);
         for (UploadCDN u : uploadCDN) {
             u.upload("filePath");
         }
     }

4 整合

4.1 整合ORM框架

4.1 MyBatis和MyBatisPlus

(1) 引入依赖

<!-- Mybatis-->   
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>${mybatis.version}</version>
</dependency>
<!-- Mybatis-Plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>

(2) 修改配置文件

mybatis:
  type-aliases-package: com.demo.pojo          # 所有POJO类所在包路径
  mapper-locations: classpath:mapper/*.xml      # mapper映射文件

(3)新增配置类

@Configuration
public class MybatisPlusConfig {

    /**
    * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题
    */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

4.2 整合缓存

4.2.1 SpringCache

SpringCache是基于注解的缓存技术,本质上不是一种缓存的实现,而是一种缓存的抽象。
实际使用的时候还要引入缓存实现框架(如redis等)的依赖
(1)引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

(2)使用

@Cacheable 没有缓存就调用方法返回,并缓存。有缓存则直接返回
@CachePut 会把方法结果缓存下来,与@Cacheable的区别是每次方法都被调用
@CacheEvict 会清空指定缓存
@Caching 缓存注解组合
@CacheConfig 以上都是方法级别的注解,这个是类级别的注解用于配置缓存

4.2.2 Redis和Redisson

(1)引入依赖

<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>${redisson.version}</version>
</dependency>

(2) 修改配置文件

redis:
  database: 0
  host: 127.0.0.1
  port: 6379

(3)新增redis配置类

@Data
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@EnableCaching //开启缓存
public class RedisConfig {

    private String host;

    private String port;

    private Integer database;

    private String password;

    public static final String REDISSON_CACHE_MANAGER = "redissonCacheManager";
    public static final String REDIS_CACHE_MANAGER = "redisCacheManager";


    /**
    * 定义Redisson,指定连接、序列化等配置
    *
    * @return RedissonClient
    */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port).setDatabase(database).setPassword(password);
        config.setCodec(new JsonJacksonCodec());
        return Redisson.create(config);
    }

    /**
    * Redisson的CacheManager,@Primary指定为默认
    *
    * @param redissonClient redissonClient
    * @return CacheManager
    */
    @Bean(RedisConfig.REDISSON_CACHE_MANAGER)
    @Primary
    CacheManager cacheManager(RedissonClient redissonClient) {
        Map<String, CacheConfig> config = new HashMap<>(16);
        return new RedissonSpringCacheManager(redissonClient, config);
    }

    /**
    * 定义StringRedisTemplate ,指定序列号和反序列化的处理类
    *
    * @param redisConnectionFactory redisConnectionFactory
    * @return RedisTemplate
    */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        //json序列化配置
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        //string的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value也采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        //将所有配置set进配置文件中
        template.afterPropertiesSet();

        return template;
    }

    /**
    * RedisTemplate的CacheManager
    *
    * @param template RedisTemplate
    * @return CacheManager
    */
    @Bean(RedisConfig.REDIS_CACHE_MANAGER)
    public CacheManager cacheManager(RedisTemplate<String, Object> template) {
        RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(template.getStringSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(template.getValueSerializer()));
        return RedisCacheManager.RedisCacheManagerBuilder
            .fromConnectionFactory(Objects.requireNonNull(template.getConnectionFactory()))
            .cacheDefaults(defaultCacheConfiguration)
            .transactionAware()
            .build();
    }
}