Spring&SpringBoot注解详解

Lou.Chen2022年11月6日
大约 51 分钟

属性配置

@ConfigurationProperties

例如:需要向以下yml属性注入到实体对象中。

baidu:
	api:
   APP_ID: 17757379
   API_KEY: QkqPC4AHa7ZCqVMGrWCvQ5Ns
   SECRET_KEY: rUIGyeIXpgWomyVASObwgNOgqc5fD9j9
@Data
//@Configuration注解的作用是将此配置注入到IOC容器中,若不加此注解,则IOC容器无法获取此配置。
@Configuration
@ConfigurationProperties(prefix = "baidu.api")  
public class BaiduProperties {
    private String APP_ID;
    private String API_KEY;
    private String SECRET_KEY;
}

BaiduProperties类可以通过@Autowired注入直接使用。

@EnableConfigurationProperties

@Data
@ConfigurationProperties(prefix = "baidu.api")  
public class BaiduProperties {
    private String APP_ID;
    private String API_KEY;
    private String SECRET_KEY;
}

以上配置没有加入到IOC容器,所以不能通过注入直接使用。⚠️ 并且@EnableConfigurationProperties注解不能加在属性配置类上,否则注入失败

@SpringBootApplication
@EnableConfigurationProperties(BaiduProperties.class)
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

使用@EnableConfigurationProperties注入配置类BaiduProperties到其他配置类上。

即可以通过依赖注入从容器中拿到此配置。

@PropertySource

resource目录下新建配置文件personinfo.properties

lc.info.name=louchen
lc.info.age=20
lc.info.city=nanjing

新建配置类,@PropertySource导入属性文件

@PropertySource(value = "classpath:personinfo.properties")
@ConfigurationProperties("lc.info")
@Data
@Configuration
public class PersonInfoProperties {
    private String name;
    private Integer age;
    private String city;
}

可以直接通过@Autowired获取使用

@Value

lc.info.name=louchen
lc.info.age=20
lc.info.city=nanjing

使用@Value指定属性名(必须为全属性名),冒号后面为默认值,当这个属性不存在(而不是配置为空)会使用默认值。

@Data
@Configuration
public class PersonInfoProperties {
    @Value("${lc.info.name:zhangsan}")
    private String name;
    @Value("${lc.info.age}")
    private Integer age;
    @Value("${lc.info.city}")
    private String city;
}
  • 直接在配置类中直接使用该属性可以不用定义该属性的get/set方法
  • 通过@Autowired注入PersonInfoProperties类使用,需要定义get/set方法才能获取使用。

导入配置文件

@ImportResource

新建xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean class="org.lc.xml.controller.HelloController" id="sayHello"></bean>
</beans>
public class HelloController {
    public String sayHello() {
        return "say hello";
    }
}
@Configuration
//加载bean.xml配置文件,注入到IOC容器中
@ImportResource(locations = "classpath:beans.xml")
public class WebMvcConfig {
}

可通过@Autowired导入HelloController使用

统一异常处理

@RestControllerAdvice

此注解包含 @ResponseBody+@ControllerAdvice

其中@ResponseBody用作全局捕获异常后方法返回结果直接转化为JSON字符串

@ControllerAdvice

此注解为全局异常处理注解。全局捕获异常后方法返回实体对象,而不是JSON字符串

AOP面向切面编程

@Aspect

定义某个配置类为一个切面

@Component
@Aspect
public class LogUtils {}

切点表达式

切点表达式根据规则指定哪些方法需要进行增强。

@Before("execution(访问修饰符 返回类型 类的全路径名.方法名(参数类型1,参数类型2...))")

  • 匹配一个或多个字符
@Before("execution(public int org.lc.aop.CaculateIm*.*(int,int))")
  • 匹配任意一个参数,第一个是int类型,第二个任意参数类型(匹配两个参数)
@Before("execution(public int org.lc.aop.CaculateIm*.*(int,*))")
  • 匹配任意多个参数,任意参数类型
@Before("execution(public int org.lc.aop.CaculateIm*.*(..))")
  • 匹配一层路径

即aop和CaculateIm之间最多有一层路径

@Before("execution(public int org.lc.aop.*.CaculateIm*.*(int,*))")
  • 匹配多层路径。即aop和CaculateIm之间可以有任意层路径
@Before("execution(public int org.lc.aop..CaculateIm*.*(int,*))")
  • 访问修饰符的位置不能写 * ,权限位置不写就行。 public关键字可选
  • 最模糊的匹配(最好不要这样写)

任意包下的任意方法的任意参数的任意返回值

@Before("execution(* *.*(..))")
  • 满足多个表达式&&
execution(public int org.lc.aop.CaculateIm*.*(..))&&execution(public int org.lc.aop.CaculateIm*.*(int,int))
  • 满足一个表达式
execution(public int org.lc.aop.CaculateIm*.*(int,double))||execution(public int org.lc.aop.CaculateIm*.*(int,int))
  • 不满足表达式
!execution(public int org.lc.aop.CaculateIm*.*(..))

@Pointcut

定义切点方法,切点中可以定义切点表达式,各种通知可以引用该切点方法。

//切面类
@Component
@Aspect
public class LogUtils {
    @Pointcut("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
    public void myCutPointExpress(){
    }
    @Before("myCutPointExpress()")
    public static void startLog(JoinPoint joinPoint) {
    }
}

各种通知

指定需要在增强的方法的什么位置执行。通知可以直接定义切点表达式,也可以引用切点方法。

  • @Before 在目标方法执行之前运行

  • @After 在目标方法结束(无论目标方法是否成功完成)之后运行

  • @AfterReturning 在目标方法正常完成后之后运行

  • @AfterThrowing 在目标方法抛出异常之后运行

  • @Around 环绕通知,拥有所有通知

    • 	       Object invoke=null;
                try {
                    //@Before
                    invoke = method.invoke(object, args);
                 	//@AfterReturning
                } catch (Exception e) {
                   //@AfterThrowing
                }finally {
                   //@After
                }
                return invoke;
      

示例:

//切面类
@Component
@Aspect
public class LogUtils {
    /**
     * 前置通知
     * @param joinPoint 连接点,封装了目标方法的信息
     */
    @Before("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
    public static void startLog(JoinPoint joinPoint) {
        //获取方法的参数列表
        Object[] args = joinPoint.getArgs();
        //获取方法对象
        Signature signature = joinPoint.getSignature();
        //获取方法的名称
        signature.getName();
        //获取方法的访问修饰符
        signature.getModifiers();
        System.out.println("执行:【" + signature.getName() + "】方法之前的记录,方法的参数列表为【"+Arrays.toString(args)+"】");
    }

    /**
     * 返回通知(方法正常返回)
     * @param joinPoint 连接点,封装了目标方法的信息
     * @param object 指定返回值 需要在注解中使用 returning = "参数名称",告诉spring要使用什么参数作为返回值
     *              注意:如果指定返回类型为 double,方法只能返回double的类型才会执行此 返回通知
     *              所以我们在声明返回类型时,一般声明为较大范围的类型
     */
    @AfterReturning(value = "execution(public int org.lc.aop.CaculateImpl.*(int,int))",returning = "object")
    public static void returnLog(JoinPoint joinPoint,Object object) {
        System.out.println("执行:【" + joinPoint.getSignature().getName() + "】方法之后的记录,方法的结果为【" + object + "】");
    }

    /**
     * 异常通知 方法抛出异常执行
     * @param joinPoint
     * @param e 方法抛出的异常类型 需要在注解中使用 throwing = "参数名称" ,告诉spring要使用什么参数作为返回值
     *          注意:如果指定异常为 NullPointerException的异常,方法只能抛出的异常为NullPointerException的异常才会执行此 异常通知
     *          所以我们在声明异常时,一般声明为较大范围的类型
     */
    @AfterThrowing(value = "execution(public int org.lc.aop.CaculateImpl.*(int,int))",throwing = "e")
    public static void exceptionLog(JoinPoint joinPoint,Exception e) {
        System.out.println("【"+joinPoint.getSignature().getName()+"】方法抛出的异常为:【"+e+"】");
    }

    /**
     * 后置通知 无论方法是否正常返回都执行
     * @param joinPoint
     */
    @After("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
    public static void finallyLog(JoinPoint joinPoint) {
        System.out.println("方法【"+joinPoint.getSignature().getName()+"】总要执行的记录....");
    }
  
   /**
     * 1、给定一个空的无参无返的方法
     * 2、在方法上加入@Pointcut注解并加上重用的切点表达式
     */
    @Pointcut("execution(public int org.lc.aop.CaculateImpl.*(int,int))")
    public void  myCutPointExpress(){
    }  
    
	  /**
     * 环绕通知
     * interface ProceedingJoinPoint extends JoinPoint 可以使用JoinPoint中的方法
     * @return
     */
    @Around("myCutPointExpress()")
    public Object aroundAdvice(ProceedingJoinPoint pjp){
        //获取当前执行方法的参数
        Object[] args = pjp.getArgs();
        //获取当前执行的方法
        Signature signature = pjp.getSignature();
        Object proceed=null;
        try {
            //@Before
            System.out.println("【前置通知】+【"+signature.getName()+"】方法执行之前的通知,方法的参数为【"+Arrays.toString(args)+"】");
            //相当于利用反射调用目标方法,相当于 invoke.method(obj,args)
            //帮我们执行目标方法
            proceed = pjp.proceed(args);
            //@AfterReturing
            System.out.println("【返回通知】+【"+signature.getName()+"】方法返回之后的通知,方法结果为【"+proceed+"】");
        }catch (Throwable throwable) {
            //@AfterThrowing
            System.out.println("【异常通知】+【"+signature.getName()+"】方法执行异常的通知,异常原因为【"+throwable.getCause()+"】");
        }finally {
            //@After
            System.out.println("【后置通知】+【"+signature.getName()+"】方法最终执行的通知");
        }
        //将方法执行的结果返回出去 供外部接收使用
        return proceed;
    }
}

多切面执行顺序

切面1

@Component
@Aspect
@Order(1)
public class TestAspect1 {

    @Pointcut("execution(* org.lc.demo.test.TestAopController.*(..))")
    public void pc() {
    }

    @Before("pc()")
    public void before() {
        System.out.println("TestAspect1>>>before");
    }

    @After("pc()")
    public void after() {
        System.out.println("TestAspect1>>>after");
    }

    @AfterReturning("pc()")
    public void afterReturning() {
        System.out.println("TestAspect1>>>afterReturning");
    }

    @AfterThrowing("pc()")
    public void afterThrowing() {
        System.out.println("TestAspect1>>>afterThrowing");
    }

    @Around("pc()")
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            System.out.println("TestAspect1>>>around>>>before");
            proceed = pjp.proceed();
            System.out.println("TestAspect1>>>around>>>afterReturning");
        } catch (Throwable throwable) {
            System.out.println("TestAspect1>>>around>>>afterThrowing");
        } finally {
            System.out.println("TestAspect1>>>around>>>after");
        }
        return proceed;
    }
}

切面2:

@Component
@Aspect
@Order(2)
public class TestAspect2 {

    @Pointcut("execution(* org.lc.demo.test.TestAopController.*(..))")
    public void pc() {
    }

    @Before("pc()")
    public void before() {
        System.out.println("TestAspect2>>>before");
    }

    @After("pc()")
    public void after() {
        System.out.println("TestAspect2>>>after");
    }

    @AfterReturning("pc()")
    public void afterReturning() {
        System.out.println("TestAspect2>>>afterReturning");
    }

    @AfterThrowing("pc()")
    public void afterThrowing() {
        System.out.println("TestAspect2>>>afterThrowing");
    }

    @Around("pc()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object proceed = null;
        try {
            System.out.println("TestAspect2>>>around>>>before");
            proceed = pjp.proceed();
            System.out.println("TestAspect2>>>around>>>afterReturning");
        } catch (Throwable throwable) {
            System.out.println("TestAspect2>>>around>>>afterThrowing");
            //需要抛出,后面执行的切面才会拦截到异常
            throw throwable;
        } finally {
            System.out.println("TestAspect2>>>around>>>after");
        }
        return proceed;
    }
}

执行结果:

TestAspect1>>>around>>>before
TestAspect1>>>before
TestAspect2>>>around>>>before
TestAspect2>>>before
TestAopController拦截真正的方法执行
TestAspect2>>>afterReturning
TestAspect2>>>after
TestAspect2>>>around>>>afterReturning
TestAspect2>>>around>>>after
TestAspect1>>>afterReturning
TestAspect1>>>after
TestAspect1>>>around>>>afterReturning
TestAspect1>>>around>>>after

**执行结果分析:**先执行切面1,然后执行切面2:

  • 切面1:

    • 环绕-前置
    • 前置
  • 切面2:

    • 环绕-前置
    • 前置
  • 执行真正的方法

  • 切面2:

    • 后置正常返回/后置异常
    • 后置
    • 环绕-(后置正常返回/后置异常(需要抛出,后面执行的切面才会拦截到异常))
    • 环绕-后置
  • 切面1:

    • 后置正常返回/后置异常
    • 后置
    • 环绕-(后置正常返回/后置异常)
    • 环绕-后置

AOP实现原理

参考:https://mp.weixin.qq.com/s/T1toQ7NeHQKqof9A0o-99gopen in new window

配置AOP步骤

先使用@Aspect指定一个切面,然后定义切点(通知上指定)/或者切点表达式(@Pointcut)来指定需要增强的方法,最后指定通知(@Before/@After等)

  • 需要增强的类

    @Data
    @Service
    public class Louzai {
    
        public void everyDay() {
            System.out.println("睡觉");
        }
    }
    
  • 切面类

    @Aspect
    @Component
    public class LouzaiAspect {
        
        @Pointcut("execution(* com.java.Louzai.everyDay())")
        private void myPointCut() {
        }
    
        // 前置通知
        @Before("myPointCut()")
        public void myBefore() {
            System.out.println("吃饭");
        }
    
        // 后置通知
        @AfterReturning(value = "myPointCut()")
        public void myAfterReturning() {
            System.out.println("打豆豆。。。");
        }
    }
    
AOP执行流程

例如:上述例子,LouzaiAspect切面类Louzai类everyDay方法进行增强

  • 前置处理器阶段:在创建被增强类Louzai的时候,当在Bean的前置处理器阶段时,会遍历所有的切面类将所有的切面信息保存在缓存中
  • 后置处理器阶段:
    • 首先从缓存中拿到所有切面信息,将切面类中切点指定的方法和Louzai中的所有方法进行匹配,找到所有相匹配的方法后返回一个切面列表(为切面类中指定的通知方法能够匹配上Louzai中方法的Advisor对象)
    • 然后会根据切面列表方法和Louzai类创建对象,根据Louzai类是否实现接口而选择不同的代理方式创建代理对象:
      • SpringBoot 2.0 之前:默认情况下,如果被代理的对象有接口,就使用 JDK 动态代理,如果被代理的对象没有接口,则使用 CGLIB 动态代理。如果有接口,但是你又希望使用 CGLIB 动态代理,则可以 @EnableAspectJAutoProxy(proxyTargetClass = true)
      • SpringBoot 2.0 之后:有接口的类默认就是使用 CGLIB 动态代理的。但是此时如果有接口的类你又想使用 JDK 动态代理:spring.aop.proxy-target-class=false
  • 切面执行:拿到AOP代理对象,通过“责任链(每次执行完后判断后继节点是否有元素,而决定是否需要继续执行。这里为一个切面列表数组) + 递归(就是反复执行invoke回调方法)”,去执行切面。

Spring事务

@Transaction

@Transaction注解可以使用在「方法」和「类」上。使用在类上,则所有方法使用事务。使用在方法上,则只有该方法使用事务。

其中注解有常用以下属性:

  • transactionManager: 配置多事务管理器,若配置多数据源则需要配置多事务管理器

  • timeout:单位为,事务超出指定执行时长后自动终止并回滚

  • readOnly: 默认为false,设置事务为是否为只读事务,可以进行事务优化,加快查询速度。忽略并发安全和一些繁琐的操作。设置为只读事

    务的方法中只能有查询操作,不能有修改操作,否则会报错。

**指定不回滚异常:**默认运行时异常都会回滚,可以指定某个运行时异常不回滚。

  • noRollbackForClassName: 指定不回滚异常的全路径类名
    • @Transactional(noRollbackForClassName = {"java.lang.ArithmeticException"})
  • noRollbackFor: 指定不回滚异常的Class类
    • @Transactional(noRollbackFor = {ArithmeticException.class})

**指定回滚异常:**默认非运行时异常不会回滚(因为在编码时已捕获,即使抛出非运行异常也不会回滚)。可以指定某个非运行时异常回滚,然后通过throws或者throw抛出让其回滚

  • rollbackForClassName: 指定需要回滚异常的全路径类名
    • @Transactional(rollbackForClassName = {"java.io.FileNotFoundException"})
  • rollbackFor: 指定需要回滚异常的Class类
    • @Transactional(rollbackFor = {FileNotFoundException.class})

事务隔离级别:

  • isolation: 默认隔离级别DEFAULT, 使用数据库设置的隔离级别(默认) ,由DBA 默认的设置来决定隔离级别。
    • Isolation.READ_UNCOMMITTED 读未提交
    • Isolation.READ_COMMITTED 读已提交
    • Isolation.REPEATABLE_READ 可重复读
    • Isolation.SERIALIZABLE 串行化

事务传播行为:

  • propagation: 默认的传播行为**Propagation.REQUIRED **

    事务传播行为类型说明
    支持当前事务情况:↓
    PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。Spring中的默认的传播行为,常用。
    PROPAGATION_SUPPORTS如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    PROPAGATION_MANDATORY如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
    不支持当前事务:↓
    PROPAGATION_REQUIRES_NEW创建一个新的事务,如果当前存在事务,则把当前事务挂起。常用。
    PROPAGATION_NOT_SUPPORTED以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    PROPAGATION_NEVER以非事务方式运行,如果当前存在事务,则抛出异常。
    其他情况:↓
    PROPAGATION_NESTED如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。

事务回滚异常分类

默认发生运行时异常都回滚,发生编译时异常不会回滚。

  • 运行时异常(非检查异常):可以不用处理,默认都回滚。

    • 例如 空指针异常,数组索引越界异常等
  • 编译时异常(检查异常):通过try-catch捕获

    • 例如 文件读写流等

总结事项⚠️:

  • 方法中所有被try-catch(包括运行时异常和非运行异常)的异常都不会回滚。因为无法抛出异常,则外部代理无法捕获。

  • 运行时异常 出现异常回自动抛出,或者手动通过throw或throws抛出异常会回滚

  • 非运行时异常默认不会回滚(因为运行之前就已经被try-catch了),即使通过throw和throws抛出非运行时异常也不会回滚。

    只有设置rollbackFor指定其非运行时异常,再通过thows抛出的异常才能回滚。

配置开启

@Enable*

功能:开启某方面的支持

@EnableScheduling 开启计划任务的支持

@EnableAsync 开启异步方法的支持

@EnableAspectJAutoProxy 开启对 AspectJ 代理的支持

@EnableTransactionManagement 开启对事务的支持

@EnableCaching 开启对注解式缓存的支持

所有@Enable注解都是有@Import的组合注解,@Enable自动开启其实就是导入了某个Bean到IOC容器中

例如:@EnableScheduling注解其实就是导入SchedulingConfiguration类并注册到IOC容器中

  @Target(ElementType.TYPE)
  @Retention(RetentionPolicy.RUNTIME)
  @Import(SchedulingConfiguration.class)
  @Documented
  public @interface EnableScheduling {
  }

JSON序列化

@JsonProperty

Spring Boot项目中使用@RequestBody接收请求数据,前端通过json格式传递数据,发现获取不到所发送的部分数据

@ApiModel(value = "码表分类")
@Data
public class ClassVo {
    @ApiModelProperty(value = "码表分类编号", required = true)
    private String cId;
    @ApiModelProperty(value = "码表分类名", required = true)
    private String name;
    @ApiModelProperty(value = "备注", required = false)
    private String memo;
}
    @PostMapping(value = "/class")
    public CommonreturnType insertClass(@RequestBody ClassVo classVo){
        return CommonreturnType.create(classVo);
    }
  • 请求数据
{
    "cId": "01",
    "name": "测试",
    "memo": "测试"
}
  • 返回结果
{
    "status": "success",
    "data": {
        "cId": null,
        "name": "测试",
   		"memo": "测试"
    }
}

传入的参数cId并没有被解析到实体类中,返回结果中为null

Spring在解析的时候,如果参数第一个字母为小写,第二个字母为大写,就不能正常的从JSON中解析出来,如:cId,mName,aBcd等参数都不能被正常的解析,而myName,abCd等最前面不止一个字母小写的情况是可以用的。

所以,@RequestBody接收的实体类中的属性名如果是第一个字母小写第二个字母大写的情况,不能正常的从JSON转换成实例类属性

  • @JsonProperty注解,指定某个方法JSON转换时的名字
@ApiModel(value = "码表分类")
@Data
public class ClassVo {
    @ApiModelProperty(value = "码表分类编号", required = true)
    private String cId;
    @ApiModelProperty(value = "码表分类名", required = true)
    private String name;
    @ApiModelProperty(value = "备注", required = false)
    private String memo;

    @JsonProperty(value = "cId")
    public String getcId() {
        return cId;
    }
    @JsonProperty(value = "cId")
    public void setcId(String cId) {
        this.cId = cId;
    }
}

@JsonFormat/@DateTimeFormat

注解@JsonFormat主要是后台到前台的时间格式的转换

注解@DataFormat主要是前后到后台的时间格式的转换

	@DateTimeFormat(pattern = "yyyy-MM-dd")    
	@JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
  private Date birthday;

@JsonIgnore

忽视该字段json的序列化。

Jackson注解总结

https://blog.csdn.net/blwinner/article/details/98532847open in new window

配置切换

@Profile

@Profile可以在指定的配置环境下使「」或者「方法」中声明的Bean是否生效,即是否注册到IOC容器中。

  • 若标注在类上,则在指定的配置环境下,该配置类以及类中的所有Bean会注册到IOC容器
  • 若标注在方法上,则在指定的配置环境下,该配置方法所声明的Bean会注册到IOC容器

指定配置环境有两种方式:

  • 在主配置application.yml文件中指定Spring.profiles.active=dev
  • 命令行指定:java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev;

示例:

public interface ParentEnviroment {
}


@Data
@AllArgsConstructor
public class MyEnviroment implements ParentEnviroment {
    private String reason;
}

@Configuration
public class EnviromentConfiguration {
    @Profile("test")
    @Bean
    public MyEnviroment myEnviromentTest() {
        return new MyEnviroment("test环境");
    }
    @Profile("dev")
    @Bean
    public MyEnviroment myEnviromentDev() {
        return new MyEnviroment("dev环境");
    }
    @Profile("pro")
    @Bean
    public MyEnviroment myEnviromentPro() {
        return new MyEnviroment("pro环境");
    }
}

新建不同环境配置文件:

image-20221018161958007
spring:
  profiles:
  #指定生效的配置文件
    active: pro

结果为:被@Profile("pro")注解标注的Bean会注册到IOC容器中

配置声明/组件注册

@Configuration的proxyBeanMethods属性

首先引出两个概念:Full 全模式Lite 轻量级模式

  • @Configuration(proxyBeanMethods = true)

    • Full 全模式,默认为true
    • 该配置模式下的创建Bean实例的方法注入容器中的同一个组件无论被取出多少次都是同一个Bean实例,即单实例对象
    • 使用代理,也就是说该配置类会被代理,直接从IOC容器之中取得Bean对象,不会创建新的对象。SpringBoot总会检查这个组件是否在容器中是否存在,保持组件的单实例
    • 这种模式下的类会被Spring所代理,那么在这个类中的@Bean方法的相互调用,就相当于调用了代理方法,那么在代理方法中会判断,是否调用getBean方法还是invokeSuper方法
  • @Configuration(proxyBeanMethods = false)

    • Lite 轻量级模式
    • 该配置模式下的创建Bean实例的方法注入容器中的同一个组件无论被取出多少次都是不同的bean实例,即多实例对象
    • 每次调用@Bean标注的方法获取到的对象是一个新的bean对象,和之前从IOC容器中获取的不一样,SpringBoot会跳过检查这个组件是否在容器中
    • 这种模式下的注解不会被Spring所代理,就是一个标准类,如果在这个类中有@Bean标注的方法,那么方法间的相互调用,其实就是普通Java类的方法的调用。

什么时候用Full全模式,什么时候用Lite轻量级模式?

  • 当在你的同一个Configuration配置类中,注入到容器中的bean实例之间有依赖关系时,建议使用Full全模式
  • 当在你的同一个Configuration配置类中,注入到容器中的bean实例之间没有依赖关系时,建议使用Lite轻量级模式,以提高SpringBoot的启动速度和性能

测试:

@Data
@AllArgsConstructor
public class Car {
    private String id;
    private String name;
}

@Data
@AllArgsConstructor
public class People {
    private String id;
    private String name;
    private Car car;
}
@Configuration(proxyBeanMethods = true)
public class DefineConfiguration {

    @Bean
    public Car car() {
        System.out.println("初始化car");
        return new Car("1", "奥迪");
    }

    @Autowired
    private Car car;

    @Bean
    public People people() {
        //这里注入car的方式注意::
        //如果通过「构造器注入」或者「@Autowired」都是从容器中直接获取,每次获取都是单实例
        //如果通过方法调用方法注入:
        //  proxyBeanMethods = true:会对方法进行代理,每次方法调用都返回同实例
        //  proxyBeanMethods = false:不会对方法进行代理,每次方法调用都相当于普通调用,即每次调用都返回不同实例对象。
        //所以无论在哪里,只要方法没被代理,通过方法调用都会创建实例,否则为单实例
        System.out.println("初始化people");
        return new People("1", "张三", car);
    }

}
@SpringBootTest
class DemoApplicationTests {
    @Autowired
    ApplicationContext applicationContext;
    @Test
    void contextLoads() {
        DefineConfiguration bean = applicationContext.getBean(DefineConfiguration.class);
        Car car1 = bean.car();
        Car car2 = bean.car();
        System.out.println(car1 == car2);

        People p1 = bean.people();
        People p2 = bean.people();
        Car car3 = p1.getCar();
        System.out.println(car1 == car3);

        //如果方法中对Car实例的注入为「构造方法注入」或者「@Autowired注入」,那么每次获取都从容器中拿,即每次都是单实例。即始终为true
        //如果方法中对Car实例对注入为「方法调用注入」,那么需要根据proxyBeanMethods值来判断:
        //      proxyBeanMethods = false:会对方法进行代理,每次方法调用都返回同实例。即结果为true
        //      proxyBeanMethods = false:不会对方法进行代理,每次方法调用都相当于普通调用,即每次调用都返回不同实例对象。即结果为false
        System.out.println(p1.getCar()==p2.getCar());
    }
}
  • proxyBeanMethods = true,结果为 true true true
  • proxyBeanMethods = false,结果为 false false true

@Configuration和@Component区别

  • @Component注解标注的配置类中创建的Bean的实例方法不会被代理,每次调用都会生成多例对象。

  • @Component效果类似于@Configuration(proxyBeanMethods="false")

@Scope

@Scope注解是 Spring IOC 容器中的一个作用域,在 Spring IOC 容器中,他用来配置「Bean实例的作用域对象」或者「配置类的单多实例」。

@Scope 具有以下几种作用域:

  • singleton: 默认为单实例,每次从容器中获取都是同一个实例
    • 若配置类被@Configuration@Component@Controller@Service@Repository标记,那么从容器中获取该配置类都是同一个。
    • 若配置中还有声明@Bean实例方法,那么会根据配置类上的@Configuration/@Component标记的不同注解,如果进行Bean方法的调用获取容器中的对象,那么会有不同的结果。具体看上述「@Configuration和@Component区别」
  • prototype: 多实例的,即每次从容器中获取都是不同的实例。
  • request: 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
  • session: 在同一会话内,所产生的bean都是同一个bean,即该bean仅在当前 HTTP session 内有效。
配置类范围

默认被@Configuration@Component@Controller@Service@Repository标记,那么从容器中获取该配置类都是同一个。

// @Configuration
// @Component
// @Controller
// @Service
@Repository
public class MyConfiguration {
}

@SpringBootTest
class DemoApplicationTests {
    @Autowired
    ApplicationContext applicationContext;
		@Test
    void test2() {
        MyConfiguration d1 = applicationContext.getBean(MyConfiguration.class);
        MyConfiguration d2 = applicationContext.getBean(MyConfiguration.class);
      	//true
        System.out.println(d1 == d2);
    }
}

若要将获取的配置类为多例就可以使用@Scope("prototype")注解

@Scope("prototype")
@Repository
public class MyConfiguration {
}

即结果为false

@Bean范围

这里都是通过类型或者名称从容器中注入的Bean实例,而不是通过方法调用获取Bean实例,所以不存在@Configuration和@Compnent配置类的影响。

默认申明的Bean实例为单例的,即从容器中获取都是同一对象。

@Configuration
public class MyConfiguration {
    @Bean
    public Student student() {
        return new Student("1", "张三");
    }
}
@SpringBootTest
class DemoApplicationTests {
    @Autowired
    ApplicationContext applicationContext;
		@Test
    void test2() {
        Student d1 = applicationContext.getBean(Student.class);
        Student d2 = applicationContext.getBean(Student.class);
      	//true
        System.out.println(d1 == d2);
    }
}

在Bean实例上加@Scope("prototype")

@Configuration
public class MyConfiguration {
    @Scope("prototype")
    @Bean
    public Student student() {
        return new Student("1", "张三");
    }
}

结果为false

@Bean/@Lazy

使用@Bean注解可以声明一个实例对象并注入到IOC容器中。默认为单实例

Bean实例创建/销毁时机
  • 单实例Bean:
    • 容器启动的时候,Bean的对象就创建了
      • 如果想要单实例的情况下,获取容器的时候再创建Bean,那么可以加上@Lazy注解,表明为懒加载的方式。
    • 容器销毁的时候,也会销毁该Bean
  • 多实例Bean:
    • 容器启动的时候,Bean是不会被创建,而是在获取bean的时候被创建
    • Bean的销毁不受IOC容器的管理,是由GC来处理的
Bean初始化/销毁方法

Bean创建的时候使用initMethod指定相应的初始化方法,销毁的时候会使用destroyMethod指定相应的销毁方法

@Data
@AllArgsConstructor
public class Student {

    private String id;
    private String name;

    public void initMethod() {
        System.out.println("student initMethod...");
    }

    public void destroyMethod() {
        System.out.println("student destroyMethod...");
    }
}
@Configuration
public class MyConfiguration {
    @Bean(initMethod = "initMethod",destroyMethod = "destroyMethod")
    public Student student() {
        System.out.println("初始化student>>>");
        return new Student("1", "张三");
    }
}

@Import

组件注册方式一般包含以下几种:

  • 包扫描(@ComponentScan)+组件标注注解(@Component,@Controller,@Service,@Repository,@Configuration)
  • @Bean【一般导入第三方里面的组件】
  • @Import【要导入到容器的组件,容器就会自动注册这个组件,Bean的id默认为「全类名」
导入「类名.class」
public class Red {}
public class Green {}
@Configuration
@Import({Red.class, Green.class})
public class MyBeanConfig3 {
    @Bean
    public Person person() {
        return new Person("张三",11);
    }
}
@Test
  public void Test5(){
      ApplicationContext annotation = new AnnotationConfigApplicationContext(MyBeanConfig3.class);
      String[] beanDefinitionNames = annotation.getBeanDefinitionNames();
      //person
      //org.lc.custom.Red
		  //org.lc.custom.Green
      for (String s : beanDefinitionNames) {
          System.out.println(s);
      }
  }
实现ImportSelector接口
public class White {}
public class Blue {}
/**
 * 自定义逻辑返回需要导入的组件
 */
public class MyImportSelector implements ImportSelector {
    /**
     * @param annotationMetadata 当前标注@Import注解上的所有注解信息
     * @return
     */
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        //默认不能返回null,否则会抛异常
        //直接将要导入的组件以全类名写入到数组中
        return new String[]{"org.lc.custom.White","org.lc.custom.Blue"};
    }
}
@Configuration
@Import({Red.class, Green.class, MyImportSelector.class})
public class MyBeanConfig3 {
    @Bean
    public Person person() {
        return new Person("张三",11);
    }
}
    @Test
    public void Test5(){
        ApplicationContext annotation = new AnnotationConfigApplicationContext(MyBeanConfig3.class);
        String[] beanDefinitionNames = annotation.getBeanDefinitionNames();
        // org.lc.custom.Red
        // org.lc.custom.Green
        // org.lc.custom.White
        // org.lc.custom.Blue
        // person
        for (String s : beanDefinitionNames) {
            System.out.println(s);
        }
    }
实现ImportBeanDefinitionRegistrar接口
public class RainBow {}
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        Set<String> annotationTypes = annotationMetadata.getAnnotationTypes();
        for (String str:annotationTypes) {
            System.out.println(str);
        }
      	//得到一个bean对象
        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(RainBow.class);
      	//指定注册一个bean,并指定bean的id为rainBow
        beanDefinitionRegistry.registerBeanDefinition("rainBow",rootBeanDefinition);
    }
}
@Configuration
//注册不分先后顺序,只要在这个Import注解中都认为按照指定规则可以注册到ioc中
@Import({Red.class, Green.class,MyImportSelector.class, MyImportBeanDefinitionRegister.class})
public class MyBeanConfig3 {
    @Bean
    public Person person() {
        return new Person("张三",11);
    }
}
      @Test
      public void Test5(){
        ApplicationContext annotation = new AnnotationConfigApplicationContext(MyBeanConfig3.class);
        //org.lc.custom.Red
        // org.lc.custom.Green
        // org.lc.custom.White
        // org.lc.custom.Blue
        // person
        // rainBow
        String[] beanDefinitionNames = annotation.getBeanDefinitionNames();
        for (String s : beanDefinitionNames) {
            System.out.println(s);
        }
    }

Bean生命周期

Bean的初始化方法

  • 1⃣️实现InitializingBean接口的afterPropertiesSet方法。
  • 2⃣️@Bean注解中的initMethod属性。

Bean的销毁方法

  • 1⃣️ @PreDestroy注解指定销毁之前的方法

  • 2⃣️ 实现DisposableBean接口的 destroy方法

  • 3⃣️ @Bean注解中的destroyMethod属性。

@PostConstruct/@PreDestroy

  • @PostConstruct: 构造器之后,然后属性(注入)设置完之后执行的方法
  • @PreDestroy: 实例销毁之前执行的方法

初始化/销毁/设值等方法执行顺序

@Data
@AllArgsConstructor
public class Animal {
    private String id;
    private String name;
}

@Configuration
public class MyConfiguration {
    @Bean(initMethod = "initMethod",destroyMethod = "destroyMethod")
    public Student student() {
        return new Student("1", "张三");
    }

    @Bean
    public Animal animal1() {
        return new Animal("1001", "lion01");
    }

    @Bean
    public Animal animal2() {
        return new Animal("1002", "lion02");
    }
}
@Data
public class Student implements InitializingBean, DisposableBean {

    private String id;
    private String name;

    //属性注入
    @Autowired
    private Animal animal1;

    private Animal animal2;

    //setter方法注入
    @Autowired
    public void setAnimal2(Animal animal2) {
        this.animal2=animal2;
    }

    //构造器
    public Student(String id, String name) {
        this.id = id;
        this.name = name;
        System.out.println("student constructor...");
    }


    //@Bean(initMethod = "initMethod",destroyMethod = "destroyMethod")
    public void initMethod() {
        System.out.println("student initMethod...");
    }

    //@Bean(initMethod = "initMethod",destroyMethod = "destroyMethod")
    public void destroyMethod() {
        System.out.println("student destroyMethod...");
    }

    //InitializingBean
    //bean创建完成并且里面的属性赋值完毕就会调用
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("student afterPropertiesSet...");
    }

    //DisposableBean
    //实例销毁执行的方法
    @Override
    public void destroy() throws Exception {
        System.out.println("student destroy...");
    }

    //构造器之后执行的方法,属性(注入)设置完之后
    @PostConstruct
    public void postConstruct() {
        System.out.println("student postConstruct...");
    }

    //实例销毁之前执行的方法
    @PreDestroy
    public void preDestroy() {
        System.out.println("student preDestroy");
    }
}

从以上例子可以得出**执行顺序从上往下**:

初始化方法:

  • 构造器方法(有参/无参)

  • 属性注入:三种依赖注入方式从上到下顺序如下:

    • 构造器注入
    • 字段注入(@Autowired)
    • Setter方法注入
  • @PostConstruct注解方法

  • InitializingBean接口的afterPropertiesSet方法

  • @BeaninitMethod指定的方法

销毁方法:

  • @PreDestroy注解方法
  • DisposableBean接口的destroy方法
  • @BeandestroyMethod指定的方法

Bean生命周期具体流程

总体来说分为实例化属性注入初始化销毁这几个步骤:具体如下

  • 从配置类中先找到Bean的定义@Bean标识的方法

  • 构造器推断:根据构造器上是否加了@Autowired注解找寻beanClass中所有符合候选条件的构造器加入到候选构造器集合中

  • 对象实例化:根据候选构造器集合中的构造器优先级依次遍历构造器对beanClass进行实例化,若成功则退出。否则使用下一个构造器进行实例化

  • 处理BeanDefinition:获取该实例中所有被标注了@Autowired@Resource@Value等注解标识的「属性」和 「方法参数(Setter注入)」上的属性。并将这些待赋值的属性封装到InjectionMetadata类中,并将该类以beanName为key,value为InjectionMetadata放入Map集合中

    Spring为了达到可扩展性,将获取被注解标识的属性的过程实际赋值的过程进行了分离。

  • 属性填充:根据beanNamekey从中取出上一步保存在Map集合中的InjectionMetadata类,从类中拿到属性集合并遍历该集合,并根据属性的类型从IOC容器中得到该Bean。然后通过反射将该Bean设置到属性中。

  • 调用实现了Aware接口的三个方法,比如BeanNameAware(获取Bean的名称)、BeanClassLoaderAware(获取Bean的类加载器)、BeanFactoryAware(获取Bean工厂)。

  • 初始化前的处理:执行实现了BeanPostProcessor接口的 postProcessBeforeInitialization方法。以下处理类都实现BeanPostProcessor接口

    • 处理一系列的*.Aware接口的方法:例如:ApplicationContextAware(获取Spring容器上下文实例) ,EnvironmentAware(Spring容器环境实例) 等。

      处理类:InitDestroyAnnotationBeanPostProcessor

    • 处理@PostConstruct注解标识的方法。

      处理类:ApplicationContextAwareProcessor

  • **调用初始化方法:**这里有两种指定初始化的方法并且执行顺序如下:

    • 1⃣️ 实现InitializingBean接口的afterPropertiesSet方法。

    • 2⃣️ @Bean注解中的initMethod属性。

  • 初始化后的处理:执行实现了BeanPostProcessor接口的 postProcessAfterInitialization方法。以下处理类都实现BeanPostProcessor接口

    • 处理实现了ApplicationListener接口的bean添加到事件监听器列表中

      处理类:ApplicationListenerDetector

    • 如果使用了AOP相关功能,则会进行创建代理对象

      处理类:AbstractAutoProxyCreator

  • Bean的销毁:这里指定销毁时的方法有三种并且执行顺序如下:

    • 1⃣️ @PreDestroy注解指定销毁之前的方法

    • 2⃣️ 实现DisposableBean接口的 destroy方法

    • 3⃣️ @Bean注解中的destroyMethod属性。

参考:https://mp.weixin.qq.com/s/uNTs41XzG0-fnmJYyNxIPwopen in new window

Aware接口

Aware 接口解决的核心问题是如何在业务代码中获得 Spring 框架内部对象的问题

使用 Aware 接口,我们基本可以获得 Spring 所有的核心对象,代价是业务代码和 Spring 框架强耦合。

  • BeanNameAware:获取Bean的名称
  • BeanClassLoaderAware:获取Bean的类加载器
  • BeanFactoryAware:获取该Bean的工厂
  • ApplicationContextAware:获取Spring容器上下文实例
  • ....
@Service
public class ServiceImplA implements ApplicationContextAware, BeanFactoryAware, BeanNameAware {
    private ApplicationContext applicationContext;
    private BeanFactory beanFactory;
    private String beanName;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
    @Override
    public void setBeanName(String s) {
        this.beanName = s;
    }
}
扩展Aware接口

如下我们定义一个资源感知器,继承了 Aware 接口,此时它是不会生效的,需要增加一个 Bean 后置处理器。

public interface ResourceAware extends Aware {
    void setResource(String resource);
}

BeanPostProcessor 会被 Spring 扫描到,然后在 Bean 被创建后执行,这里可以判断 Bean 的类型,如果是我们定义的 Aware 子接口,就调用该接口的 setResource 方法将准备好的资源注入到 Bean 中。

@Component
public class MyAWareBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof ResourceAware) {
            ((ResourceAware) bean).setResource("test Resource");
        }
        return bean;
    }
}

使用:

@Service
public class ServiceImplA implements ResourceAware {
    private String resouece;
    @Override
    public void setResource(String resource) {
        this.resouece=resource;
    }
}

参考:https://juejin.cn/post/6933194990814232583open in new window

BeanFactoryPostProcessor接口

使用目的:在BeanFactory标准初始化之后调用,用来定制和修改BeanFactory的内容

工作时机:所有的bean定义已经保存加载到beanFactory中,但bean的实例还没创建。可以用此来创建Bean

@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware {
    /**
     * Spring应用上下文环境
     */
    private static ConfigurableListableBeanFactory beanFactory;

    private static ApplicationContext applicationContext;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        SpringUtils.beanFactory = beanFactory;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtils.applicationContext = applicationContext;
    }

    /**
     * 获取对象
     *
     * @param name
     * @return Object 一个以所给名字注册的bean的实例
     * @throws org.springframework.beans.BeansException
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        return (T) beanFactory.getBean(name);
    }

    /**
     * 获取类型为requiredType的对象
     *
     * @param clz
     * @return
     * @throws org.springframework.beans.BeansException
     */
    public static <T> T getBean(Class<T> clz) throws BeansException {
        T result = (T) beanFactory.getBean(clz);
        return result;
    }
}

Bean的加载/执行顺序

@Order/Ordered接口

⚠️注意:@Order或者Ordered接口的作用是定义IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序,Bean的加载顺序不

受@Order或Ordered接口的影响。@Order只是决定在IOC容器中在集合中的顺序。

  • Ordered 接口优先级 大于@Order注解优先级
  • 数值「越小」,优先级「越高
Order作用
  • 多个 Bean 注入到集合时在集合中的顺序

  • 指定AOP的优先级:@Aspect声明的切面类的优先级

  • 控制ApplicationListener实现类的加载顺序

  • 控制ApplicationRunnerCommandLineRunner实现类的加载顺序,参考「本文档的SpringBoot启动初始化任务方式」

Order失效场景
  • @Configuration + @Order 无效

    @Order(2)
    @Configuration
    public class My01Configuration {
        @Bean
        public Car car01() {
            return new Car("1", "car01");
        }
    }
    
    @Order(1)
    @Configuration
    public class My02Configuration{
        @Bean
        public Student student01() {
            return new Student("1", "s1");
        }
    }
    

    测试发现

    • @Bean的加载不受@Order控制

    • My01ConfigurationMy02Configuration 的执行顺序也不受控制

  • @Bean + @Order 无效

    @Configuration
    public class My02Configuration{
        @Order(2)
        @Bean
        public Student student01() {
            System.out.println("01");
            return new Student("1", "s1");
        }
        @Order(1)
        @Bean
        public Student student02() {
            System.out.println("02");
            return new Student("1", "s1");
        }
    }
    

    测试发现,@Bean的加载不受@Order控制

@DependsOn

@DependsOn注解可以用来控制bean的创建顺序,该注解用于声明当前bean依赖于另外一个bean。所依赖的bean会被容器确保在当前bean实例化之前被实例化。

用在方法上
@Configuration
public class DependOnTest {
    @Bean
    @DependsOn(value = "beanB")
    public BeanA beanA() {
        System.out.println("BeanA=====>");
        return new BeanA();
    }
    @Bean
    @DependsOn({"beanC","beanD"})
    public BeanB beanB() {
        System.out.println("BeanB=====>");
        return new BeanB();
    }
    @Bean
    public BeanC beanC() {
        System.out.println("BeanC=====>");
        return new BeanC();
    }
    @Bean
    public BeanD beanD() {
        System.out.println("BeanD=====>");
        return new BeanD();
    }
    @Bean
    public BeanE beanE() {
        System.out.println("BeanE=====>");
        return new BeanE();
    }
}

从以上代码可以得出,beanA依赖于beanB,beanB依赖于beanC和beanD,beanE不依赖任何Bean

BeanC=====> BeanD=====> BeanB=====> BeanA=====> BeanE=====>

用在类上
@Configuration
public class My02Configuration{
    @Bean
    public Car car02() {
        System.out.println("car02....");
        return new Car("1", "c2");
    }
}
@DependsOn("car02")
@Configuration
public class My01Configuration{
    @Bean
    public Car car01() {
        System.out.println("car01....");
        return new Car("1", "c1");
    }
}

car02.... car01....

@AutoConfigureOrder

这个注解用来指定配置文件的加载顺序。但是在实际测试中发现,以下这样使用是不生效的:

@Configuration
@AutoConfigureOrder(2)
public class DependOnTest1 {
    @Bean
    public BeanA beanAAA() {
        System.out.println("BeanAAA=====>");
        return new BeanA();
    }
    @Bean
    public BeanB beanBBB() {
        System.out.println("BeanBBB=====>");
        return new BeanB();
    }
}
@Configuration
@AutoConfigureOrder(1)
public class DependOnTest2 {
    @Bean
    public BeanC beanCCC() {
        System.out.println("BeanCCC=====>");
        return new BeanC();
    }
    @Bean
    public BeanD beanDDD() {
        System.out.println("BeanDDD=====>");
        return new BeanD();
    }
}
  • 加载顺序依然是:

    BeanAAA=====>
    BeanBBB=====>
    BeanCCC=====>
    BeanDDD=====>
    

@AutoConfigureOrder只能改变外部依赖的@Configuration的顺序。

如何理解是外部依赖呢? 能被你工程内部scan到的包,都是内部的Configuration,而spring引入外部的Configuration,是通过spring.factories配置文件

换句话说,@AutoConfigureOrder能改变spring.factories中的@Configuration的顺序。

具体使用方式:

  • resources目录下新建META-INF文件夹,并新建spring.factories文件,并添加配置的类路径:

    • org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
        org.lc.test.demo.denpendon_demo.DependOnTest1,\
        org.lc.test.demo.denpendon_demo.DependOnTest2	
      
    • spring.factories配置的类路径可省略@Configuration注解

  • 其它注解类似用法:

    • @AutoConfigureAfter
    • @AutoConfigureBefore
    • @AutoConfigurationPackage

依赖注入

@Autowired & @Qualifier & @Primary

@Autowire

Spring的注解,只能用在Spring框架中。可以用在「字段」上,也可以用在「方法」上。

默认情况下要求对象必须存在, 它要求依赖对象必须存在. 若允许null值, 可以设置它的requiredfalse.

  • 首先按照注入的类型去IOC容器中查找对应的组件,若找到则装配,找不到抛异常
  • 若找到多个名称不同类型相同的,则按照注入的属性名称(默认为类名首字母小写)作为bean的id去容器中查找,找到则装配,找不到,抛异常

@Qualifier

若使用**@Autowired找到多个类型,注入时不想通过默认属性名作为id寻找,那么直接可以使用@Qualifier**按照其指定的名称去容器中

@Primary

若使用**@Autowired**找到多个类型,注入时找到多个类型,若在其中一个Bean上配置了@Primary注解,则会直接使用使用被该注解标记的Bean

@Resource

J2EE的注解. 扩展性更强,可以用在其他框架上。属于JSR-250规范中的注解

  • name和type都不指定,即按照默认按照名称来装配注入,若找不到则按照bean的类型来找,若找到多个名称不同类型相同则抛出异常
  • 它有两个属性是比较重要的:
    • name: Spring将name的属性值解析为bean的名称, 使用byName的自动注入策略, 该name必须存在,否则编译的时候就会提示错误,启动则抛异常
    • type: Spring将type的属性值解析为bean的类型, 使用byType的自动注入策略,若找到多个类型,则按照名称属性取找,找不到则抛出异常。
    • 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常

依赖注入几种方式比较

  • 1⃣️使用field变量注入,即通过@Autowired或者@Resource加在字段上。

    @Autowired
    private UserDao userDao;
    
    • 优点:变量方式注入非常简洁,没有任何多余代码,非常有效的提高了java的简洁性
    • 缺点:可能出现依赖注入的对象为null
  • 2⃣️构造器注入

    @Component
    public class MyOneConfiguration {
    
        private final Student student;
        
        public MyOneConfiguration(Student student) {
            this.student=student;
            System.out.println("MyOneConfiguration 构造器>>>>>");
            System.out.println(student.toString());
        }
        //或者
        //这里「方法上」的和「方法参数上」的@Autowired可以省略
        //@Autowired
        //public MyOneConfiguration(@Autowired Student student) {}
    
        public void test01() {
            System.out.println("MyOneConfiguration test01>>>>>");
            System.out.println(student.toString());
        }
    }
    
    • ⚠️ 同时存在多个构造函数时执行规则

      • 构造器都不存在@Autowired的情况:

        • 存在手动编写默认的无参构造函数:则执行默认的无参的构造函数
        • 不存在手动编写的无参构造函数,则只能出现一个有参数的构造器,否则报错。
      • 构造器有存在@Autowired的情况:

        • 有无默认的无参数的构造器不重要。

        • 哪个构造器上有@Autowired(require=true)则优先执行哪个构造器。

        • 在所有的构造器中只能存在一个构造器上有@Autowired(require=true)的情况,若有多个@Autowired(require=true)则报错。

        • 可以有多个构造器上有@Autowired(require=false)

        • 若没有构造器上有@Autowired(require=true),则会从多个@Autowired(require=false)的构造器选择一个执行,规则如下:

          • public修饰的构造器 > private修饰的构造器
          • 修饰符相同的情况下参数数量更多的优先
  • 3⃣️Setter方法注入

    @Component
    public class MyOneConfiguration {
    
        private Student student;
    
        //必须显示使用@Autowired注解进行注入,不可省略。方法名随意。
        @Autowired
        public void setStudent(Student student) {
            this.student = student;
        }
    
        public void test01() {
            System.out.println("MyOneConfiguration test01>>>>>");
            System.out.println(student.toString());
        }
    }
    

    构造器注入比Setter方式先初始化。因为一个对象的实例化总是先执行构造器然后从上到下初始化字段的

总结:

  • 依赖注入的使用上,构造器注入是首选,使用构造器能避免注入的依赖是空的情况。因为在bean的生命周期里面先执行的是bean的构造器,然后才给bean里面的属性赋值。一个对象所有依赖的对象先实例化后,才实例化这个对象。没有他们就没有我原则
  • 使用@Autowired注解的时候,要使用Setter Injection方式,这样代码更容易编写单元测试。

依赖注入几种方式执行顺序

  • 构造注入发生在Bean生命周期的实例化阶段,不是发生在填充属性阶段

  • 属性注入Setter注入都是发生在Bean生命周期的填充属性阶段,且属性注入发生在Setter注入之前

  • 如果三种注入方式都被使用了,最终生效的是Setter注入; 顺序是 构造注入 > 属性filed注入 > Setter注入

循环依赖

什么是循环依赖

类A依赖类B,类B也依赖类A,这种情况就会出现循环依赖。 Bean A → Bean B → Bean A

Spring官方是推荐使用构造器注入的,所以如果是通过构造器注入那就会产生一个无限循环注入的问题了。如下:构造器注入启动会报错

@Service
public class AService {
    public AService(BService bService) {}
}
@Service
public class BService {
    public BService(AService aService) {}
}

所以循环依赖问题其实都是问Setter方式内部如何解决循环依赖的?而不是问的构造器。

循环依赖出现的三种情况
  • 通过构造方法进行依赖注入时产生的循环依赖问题。

  • 通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。

    setter方法(多例)的情况下,每一次getBean()时,都会产生一个新的Bean,如此反复下去就会有无穷无尽的Bean产生了,最终就会导致OOM问题的出现。

  • 通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。

在Spring中,只有第3种方式单例模式的循环依赖问题被解决了

Spring如何解决循环依赖

这里Spring解决其实是「Setter」方式/「属性field」注入的的循环依赖。例如:以下Setter注入方式,Spring不会报错

@Service
public class AService {
    @Autowired
    private BService bService;
    @Autowired
    public void setaService(BService bService) {
        this.bService=bService;
    }
}
@Service
public class AService {
    @Autowired
    private BService bService;
    @Autowired
    public void setaService(BService bService) {
        this.bService=bService;
    }
}
三级缓存
 * @author Juergen Hoeller
 * @since 2.0
 * @see #registerSingleton
 * @see #registerDisposableBean
 * @see org.springframework.beans.factory.DisposableBean
 * @see org.springframework.beans.factory.config.ConfigurableBeanFactory
 */
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

 /** Cache of singleton objects: bean name to bean instance. */
 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

 /** Cache of singleton factories: bean name to ObjectFactory. */
 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

 /** Cache of early singleton objects: bean name to bean instance. */
 private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
  
  // 省略其他的一些方法。。。
}
  • 一级缓存(singletonObjects):用于保存已经实例化初始化完成的实例Bean,是一个完整的Bean。为“Spring 的单例属性”而生,就是个单例池,用来存放已经初始化完成的单例
  • 二级缓存(earlySingletonObjects):用于保存已经实例化但是还未属性赋值初始化的Bean,是一个半成品的Bean。由三级缓存的工厂对象创建后Bean后放入二级缓存
  • 三级缓存(singletonFactories): key为beanName, value为 beanObjectFactory,该对象为一个Bean工厂,可以创建Bean实例,延迟Bean对生成,工厂生产的Bean会存入二级缓存。解决当依赖对象实现AOP情况下,存在依赖对象不一致的情况。
实例创建过程

假设A依赖B, B依赖AC, C依赖AB

  • 首先实例化A对象,在进行属性注入之前将A对象放入三级缓存(Bean工厂)。然后进行属性注入:
    • 首先检查一/二/三级缓存是否有B实例,发现容器中还没有B实例,则会去创建B实例,先进行实例化,并将B放入三级缓存,然后进行属性注入:
      • 首先检查一/二/三级缓存中是否有A实例,此时拿到实例A工厂,生产A对象并放入二级缓存,并进行A属性注入,然后将实例A放入二级缓存,并移除三级缓存中A的工厂。
      • 首先检从一/二/三级缓存中是否有C实例,发现容器还没有实例C,则会去创建C实例,先进行实例化,并将C放入三级缓存,然后进行属性注入:
        • 首先从一/二/三级缓存去拿实例A,从二级缓存中拿到实例A,并对A进行属性注入
        • 首先从一/二/三级缓存去拿实例B。从三级缓存中拿到实例B工厂,然后创建B实例,对B属性进行注入,并放入二级缓存,并移除三级缓存中B的工厂。
        • 至此C实例已经完成属性注入,并放入一级缓存,并移除二/三级缓存中的C
    • 返回B属性注入,从一级缓存拿到实例C
    • 至此B实例已经完成属性注入,并放入一级缓存,并移除二/三级缓存中的B
  • 返回A属性注入,从一级缓存中拿到实例B。
  • 至此A实例已经完成属性注入,并放入一级缓存,并移除二/三级缓存中的A。整个循环依赖结束。
问题
二级缓存作用?

为了提高性能,避免每次从工厂中获取实例

三级缓存作用/为什么要三级缓存?

❓ 只要二级缓存行不行?如果只有二级缓存那么将可能出现以下问题:

只要二级缓存的情况:

A依赖B,B依赖A。并且A存在AOP的情况。

  • 首先A实例化后放入二级缓存,然后进行属性注入:
    • 接着实例化B,进行属性注入,然后从二级缓存中拿到实例A,B初始化完成放入一级缓存
  • 回到A属性注入阶段,从一级缓存中拿到实例B。接着来到A的后置处理器阶段,此时发现A实例实现了AOP代理,那么会在此阶段生成代理对象,并将此对象加入一级缓存中。此时最终的A实例为代理对象。

结果:B实例中的A为原始对象,而非代理对象。而A最终初始化完成的为代理对象

加入三级缓存后的情况:

A依赖B,B依赖A。并且A存在AOP的情况。

  • 首先A实例化后将工厂实例放入三级缓存,然后进行属性注入:
    • 接着实例化B,进行属性注入,从三级缓存中拿到A工厂后,如果A实现了AOP,那么此时就会使用此工厂创建一个代理对象后A实例后放入二级缓存,并删除三级缓存的工厂,接着B完成初始化放入一级缓存。
  • 然后回到A进行属性注入,从一级缓存中拿到B进行属性注入,当进行到A实例的后置处理器阶段时,此时会从二级缓存拿到实例A,然后判断此实例已经是代理后的对象,则进行其他处理后放入一级缓存。
  • 至此循环依赖结束

结果:B实例中的A的为代理对象,最终的实例A也是代理对象

参考:

https://blog.csdn.net/weixin_44129618/article/details/122839774open in new window

SpringBoot启动初始化任务方式

@PostConstruct注解

@Component
public class PostComponentA {
    public PostComponentA() {
        System.out.println("PostComponentA 初始化");
    }
  	//构造器执行完后,然后属性的设值之后执行的方法
    @PostConstruct
    public void init(){
        System.out.println("PostComponentA 执行 init 方法...");
    }
}

实现CommandLineRunner接口

主程序:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        System.out.println("主程序 start >>>>>");
        SpringApplication.run(DemoApplication.class, args);
        System.out.println("主程序 end >>>>>");
    }
}

实现CommandLineRunner接口

@Order(3)
@Component
public class CommandApplication01 implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("commandLineRunner01==>参数:" + Arrays.toString(args));
    }
}
@Order(2)
@Component
public class CommandApplication02 implements CommandLineRunner{
    @Override
    public void run(String... args) throws Exception {
        System.out.println("commandLineRunner02==>参数:" + Arrays.toString(args));
    }
}

@Order(1)
@Component
public class CommandApplication03 implements CommandLineRunner{
    @Override
    public void run(String... args) throws Exception {
        System.out.println("commandLineRunner03==>参数:" + Arrays.toString(args));
    }
}

控制台:

  • 在主启动程序中加入固定参数:

    @SpringBootApplication
    public class DemoApplication {
        public static void main(String[] args) {
            System.out.println("主程序 start >>>>>");
            //加入启动参数
            args= new String[]{"hello-world"};
            SpringApplication.run(DemoApplication.class, args);
            System.out.println("主程序 end >>>>>");
        }
    }
    

    主程序 start >>>>>

    //容器其他操作

    commandLineRunner03==>参数:[hello-world] commandLineRunner02==>参数:[hello-world] commandLineRunner01==>参数:[hello-world] 主程序 end >>>>>

  • IDEA启动时手动传入参数

    Edit Configurations =>Environment=>Program agruments

    添加参数即可: aaa bbb

    主程序 start >>>>>

    //容器其他操作

    commandLineRunner03==>参数:[aaa, bbb] commandLineRunner02==>参数:[aaa, bbb] commandLineRunner01==>参数:[aaa, bbb] 主程序 end >>>>>

  • 以jar包的方式启动加入启动参数

    java -jar xml-1.0-xxxxx.jar aaa bbb

实现ApplicationRunner接口

主程序

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        System.out.println("主程序 start >>>>>");
        SpringApplication.run(DemoApplication.class, args);
        System.out.println("主程序 end >>>>>");
    }
}

实现ApplicationRunner接口

@Order(6)
@Configuration
public class ApplicationRun01 implements ApplicationRunner{
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("ApplicationRun01 ==> 相关数据:");
        System.out.print("获取传入的源数据:");
        String[] sourceArgs = args.getSourceArgs();
        System.out.println(Arrays.toString(sourceArgs));
        System.out.print("没有键的数据:");
        List<String> nonOptionArgs = args.getNonOptionArgs();
        System.out.println(nonOptionArgs);
        System.out.println("通过键获取值:");
        //先获取所有的键
        Set<String> optionNames = args.getOptionNames();
        for (String optionName : optionNames) {
            //一个键对应的值可以有多个
            List<String> optionValues = args.getOptionValues(optionName);
            System.out.println("key:" + optionName + "value:" + optionValues);
        }
    }
}
@Order(5)
@Configuration
public class ApplicationRun02 implements ApplicationRunner{
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("ApplicationRun02 ==> ");
    }
}
@Order(4)
@Configuration
public class ApplicationRun03 implements ApplicationRunner{
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("ApplicationRun03 ==> ");
    }
}

控制台

  • IDEA启动时手动传入参数

    • 步骤Edit Configurations =>Environment=>Program agruments

    • 参数:--name=zhangshan,lisi --age=12,22 city birthday

    • 打印结果:

    主程序 start >>>>>
    //容器其它操作
    ApplicationRun03 ==> 
    ApplicationRun02 ==> 
    ApplicationRun01 ==> 相关数据:
    获取传入的源数据:[--name=zhangshan,lisi, --age=12,22, city, birthday]
    没有键的数据:[city, birthday]
    通过键获取值:
    key:namevalue:[zhangshan,lisi]
    key:agevalue:[12,22]
    主程序 end >>>>>
    
  • 在jar启动的时候传入参数:

    java -jar xx-1.0-SNAPSHOT.jar --name=zhangshan,lisi --age=12,22 city birthday

实现InitializingBean接口

Spring启动后,初始化Bean时,若该Bean实现InitialzingBean接口,会自动调用afterPropertiesSet()方法,完成一些用户自定义的初始化操作。

@Component
public class MyInitializingBean implements InitializingBean {

    public MyInitializingBean() {
        System.out.println("MyInitializingBean 初始化");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("MyInitializingBean 执行 afterPropertiesSet方法");
    }

    @PostConstruct
    public void init() {
        System.out.println("MyInitializingBean 执行 init...");
    }
}
  • 主程序启动控制台打印

    MyInitializingBean 初始化
    MyInitializingBean 执行 init...
    MyInitializingBean 执行 afterPropertiesSet方法
    

    @PostContsuct -> 执行InitializingBean接口中定义的方法 -> 执行init-method属性指定的方法。

实现ApplicationListener接口(事件)

ContextRefreshedEvent是Spring的ApplicationContextEvent一个实现,此事件会在Spring容器初始化完成后以及刷新时触发。

@Component
public class ApplicationListenDemo implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        System.out.println("ApplicationListenDemo 执行 onApplicationEvent方法");
        //获取Spring容器
        ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
        //获取Spring中所有Bean名称
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        System.out.println(Arrays.toString(beanDefinitionNames));
        //通过类型获取Bean实例
        PersonServiceImp bean = applicationContext.getBean(PersonServiceImp.class);
        //执行Bean实例的业务方法
        bean.asyncTaskForTransaction(false);
    }
}

SpringBoot自动配置原理

@ComponentScan

扫描被@Configuration@Controller@Component@Service@Mapper等注解的Bean。

  • 只扫描注解为@Controller的类。不实用默认扫描规则(默认为扫描所有)

    @ComponentScan(basePackages = "org.lc",useDefaultFilters = false,
                   includeFilters = {
      @ComponentScan.Filter(type = FilterType.ANNOTATION,classes ={Controller.class} )
    })
    
  • @Controller注解的类不扫描。

    @ComponentScan(basePackages = "org.lc",excludeFilters = {
            @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
    })
    
  • 指定多个扫描器

    @ComponentScans({
            @ComponentScan(basePackages = "org.t2"),
            @ComponentScan(basePackages = "org.t1")
    })
    

FilterType过滤类型介绍:

  • FilterType.ANNOTATION:按照注解过滤

  • FilterType.ASSIGNABLE_TYPE : 按照类型进行过滤

    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,classes = {EmployeeService.class})
    
  • FilterType.REGEX 按照正则表达式过滤

  • FilterType.CUSTOM 自定义规则

    @ComponentScan(basePackages = "org.lc",useDefaultFilters = false,includeFilters = {
            @ComponentScan.Filter(type = FilterType.CUSTOM,classes = {MyFilter.class})
    })
    
    //需要实现 TypeFilter接口
    public class MyFilter implements TypeFilter {
        @Override
        public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
            //获取当前类的注解的信息
            AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
            //获取当前正在扫描的类的class类信息
            ClassMetadata classMetadata = metadataReader.getClassMetadata();
            //获取当前类资源(路径)
            Resource resource = metadataReader.getResource();
            //获取当前类的类名
            String className = classMetadata.getClassName();
            System.out.println("------:"+className);
            //自定义类名中包含为'er'的字符串就加入到容器中
            if (className.contains("er")) {
                return true;
            }
            //返回true 扫描到容器中
            //返回false 不扫描到容器中
            return false;
        }
    
    

@Conditional

@Conditional: 传入一个Condition接口实例类,通过重写接口的matches方法,如果方法返回true,则该条件下的bean配置生效。如果返回

false,则bean配置失效

  • @Conditional 在类上:
    • 若实现Condition接口中的matches方法返回false,则该配置类中的所有bean方法都不会生效(都不会注册到IOC容器中)
  • @Conditional 在bean方法上:
    • 若实现Condition接口中的matches方法返回false,则该beanf方法不会生效(不会注册到IOC容器中)

示例:

条件实现类:

public class WindowsCondition implements Condition {
    /**
     * 返回true则条件生效(该配置生效),返回false则条件不生效(配置失效)
     * @param conditionContext 判断条件能使用的上下文(环境)
     * @param annotatedTypeMetadata 注释信息
     * @return
     */
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        //获取当前ioc使用的bean工厂
        ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory();
        //获取类加载器
        ClassLoader classLoader = conditionContext.getClassLoader();
        //获取当前环境信息
        Environment environment = conditionContext.getEnvironment();
        //获取到bean定义的注册类 。可以判断容器中bean的注册情况,也可以给容器注册bean
        BeanDefinitionRegistry registry = conditionContext.getRegistry();
        //判断当前容器中是否有该bean,传入需要判断的bean的id
        boolean person = registry.containsBeanDefinition("bill-gaci");
        System.out.println("++++"+person);
        //获取当前的操作系统
        String s = environment.getProperty("os.name");
        //如果当前时windows则返回true。代表该配置有效
        if (s.contains("Windows")) {
            return true;
        }
        return false;
    }
}

public class LinuxCondition implements Condition {
    /**
     * 返回true则条件生效(该配置生效),返回false则条件不生效(配置失效)
     * @param conditionContext 判断条件能使用的上下文(环境)
     * @param annotatedTypeMetadata 注释信息
     * @return
     */
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Environment environment = conditionContext.getEnvironment();
        //获取当前的操作系统
        String s = environment.getProperty("os.name");
        //如果当前时linux则返回true。代表该配置有效
        if (s.contains("linux")) {
            return true;
        }
        return false;
    }
}

配置类:

//若WindowsCondition中的matches为false则该配置下的所有bean不生效
@Conditional({WindowsCondition.class})
@Configuration
public class MyBeanConfig2 {

    //如果操作系统是linux 则给容器注册@Bean("linas-tuoa")
    //如果操作系统是windows 则给容器注册@Bean("bill-gaci")

    //传入一个Condition接口实例类,通过matches方法返回true或false
    @Conditional({WindowsCondition.class})
    @Bean("bill-gaci")
    public Person person1() {
        return new Person("比尔-盖茨", 60);
    }

    @Conditional(LinuxCondition.class)
    @Bean("linas-tuoa")
    public Person person2() {
        return new Person("林纳斯·托瓦兹", 60);
    }
}

测试:

    @Test
    public void Test4(){
        ApplicationContext annotation = new AnnotationConfigApplicationContext(MyBeanConfig2.class);
        //获取当前ioc运行的环境,操作系统等属性
        Environment environment = annotation.getEnvironment();
        //获取操作系统
        String property = environment.getProperty("os.name");
        System.out.println("本操作系统:"+property);
        //获取所有的bean的id名称
        String[] beanDefinitionNames = annotation.getBeanDefinitionNames();
        for (String s : beanDefinitionNames) {
            System.out.println(s);
        }
        //获取该Bean的所有bean对象。
        //key为bean的id,value为初始化的对象
        Map<String, Person> beansOfType = annotation.getBeansOfType(Person.class);
        System.out.println(beansOfType);
    }

可通过启动参数配置本操作系统的虚拟变量 -Dos.name=linux来决定哪个Bean注册成功。

@ConditionalOnClass

配置类上或者方法上有「指定类」时条件生效,才会将该Bean实例注册到IOC容器

  • name:指定类的全包路径名
  • value:指定类的类型

例如:当存在类Animal或者Blue类时,My01Configuration配置类才会生效,car01才会注册到IOC容器中。

//这里因为在本地测试,所有类不存在时,编译就会报错。而在打包后Jar包中则是正常判断的。 
//@ConditionalOnClass(Animal.class) 
@ConditionalOnClass(name = "org.lc.demo.test.Blue")
@Configuration
public class My01Configuration{
    @Bean
    public Car car01() {
        System.out.println("car01....");
        return new Car("1", "c1");
    }
}

@ConditionalOnProperty

配置类上或者方法上有「指定属性值」时条件生效,才会将该Bean实例注册到IOC容器

  • name:指定属性名
  • havingValue:指定属性名对应的值
  • matchIfMissing:如果此属性不匹配是否生效

若不存在该属性名,则配置失效

若存在属性名,并且是否等于指定的属性值,如果相等,则生效,不等则失效。

@ConditionalOnProperty(name = "swagger.enable",  havingValue = "true")
@Configuration
public class My01Configuration{
    @Bean
    public Car car01() {
        System.out.println("car01....");
        return new Car("1", "c1");
    }
}

必须存在swagger.enable配置,并且值为true时,该配置类才生效

@Condtional*其他注解

  • @ConditionalOnBean:当容器里有指定 Bean 的条件下🌟
  • @ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下🌟
  • @ConditionalOnSingleCandidate:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
  • @ConditionalOnClass:当类路径下有指定类的条件下🌟
  • @ConditionalOnMissingClass:当类路径下没有指定类的条件下🌟
  • @ConditionalOnProperty:指定的属性是否有指定的值🌟
  • @ConditionalOnResource:类路径是否有指定的值
  • @ConditionalOnExpression:基于 SpEL 表达式作为判断条件
  • @ConditionalOnJava:基于 Java 版本作为判断条件
  • @ConditionalOnJndi:在 JNDI 存在的条件下差在指定的位置
  • @ConditionalOnNotWebApplication:当前项目不是 Web 项目的条件下
  • @ConditionalOnWebApplication:当前项目是 Web 项 目的条件下

@SpringBootApplication启动类原理

该注解包含以下注解:

  • @SpringBootConfiguration :本启动类也是一个配置类
    • @Configuration: 启动类也是以一个配置类
  • @EnableAutoConfiguration: 开启SpringBoot的自动配置
  • @ComponentScan:默认会扫描启动类所在包下的所有配置类。
@EnableAutoConfiguration注解
  • 内部使用@Import({AutoConfigurationImportSelector.class})AutoConfigurationImportSelector类注册到IOC容器中

  • AutoConfigurationImportSelector实现了ImportSelector接口重写selectImports方法

  • selectImports方法会扫描项目中所有包下的META-INF/spring.factories文件

  • spring.factories文件中包含所有自动配置类的全类名,例如:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
    
  • 这里并不是所有的自动配置类都会注册到IOC容器,而是还要通过自动配置类上的@ConditionalOnClass注解来判断是否需要将该自动配置注入到IOC容器中,例如:

    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnClass({RabbitTemplate.class, Channel.class})
    @EnableConfigurationProperties({RabbitProperties.class})
    @Import({RabbitAnnotationDrivenConfiguration.class})
    public class RabbitAutoConfiguration {}
    

    @ConditionalOnClass({RabbitTemplate.class, Channel.class}):这里只有存在RabbitTemplate类和Channel类此自动配置类才会生效,才会注册到IOC容器。

  • 在所有的生效的自动配置类中,还有对@Bean声明的有效性的判断,根据@Bean上的@ConditionalOnMissingBean注解,若容器中不存在指定的某个Bean实例,则该Bean实例生效,会注册到IOC容器,否则不会注册到IOC容器中。这也就是为什么无需额外配置,某些功能可以直接使用,并切当需要自定义配置时,即当手动配置类某些配置类时,那么自动配置类也就会失效。

自定义Starter

新建项目

新建一个maven项目,导入必须依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.lc.demo</groupId>
    <artifactId>spring-demo-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.3.7.RELEASE</version>
        </dependency>
        <dependency>
            <!--打包时自动根据属性注释生成提示文件 `spring-configuration-metadata.json`-->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.3.7.RELEASE</version>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <build>
        <plugins>
            <plugin>
                <!--打包时自动根据属性注释生成提示文件 `spring-configuration-metadata.json`-->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-configuration-processor</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

IDEA开启打包自动生成提示文件spring-configuration-metadata.json

image-20221018143048623
代码编写
@Data
@Builder
public class PersonIdCarInfo {
    private String idCar;
    private String name;
    private String city;
    private String gender;
    private Data birthday;
    private List<String> hobby;
}
@Data
@ConfigurationProperties(prefix = "person.properties")
public class PersonProperties {
    /**
     * 身份编号
     */
    private String id="000001";
    /**
     * 姓名
     */
    private String name="张三";
    /**
     * 性别
     */
    private String gender="男";
    /**
     * 城市
     */
    private String city="北京";
    /**
     * 爱好
     */
    private List<String> hobby = Arrays.asList("乒乓球,篮球");
}

热插拔配置:

public class PersonIdCarOpen {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(PersonIdCarOpen.class)
@Documented
public @interface EnablePersonIdCarAuth {
}

自动配置类

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PersonProperties.class)
public class PersonAutoConfiguration {
    private PersonProperties personProperties;
		//构造器注入配置属性
    public PersonAutoConfiguration(PersonProperties personProperties) {
        this.personProperties=personProperties;
    }
    //如果IOC存在PersonIdCarOpen实例,则该Bean生效。
  	//这里通过@EnablePersonIdCarAuth注解,注入PersonIdCarOpen类,来决定是否使该Bean生效
    @Bean
    @ConditionalOnBean(PersonIdCarOpen.class)
    public PersonIdCarInfo personIdCarInfo() {
        return PersonIdCarInfo.builder()
                .idCar(personProperties.getId())
                .city(personProperties.getCity())
                .gender(personProperties.getGender())
                .name(personProperties.getName()).build();
    }

}

Resource目录下新建META-INF文件夹,并在其下新建spring.factories文件。写入自动配置全路径名

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.lc.demo.configuration.PersonAutoConfiguration
编写提示文件(可选)

Resource目录下的META-INF文件夹下新建。生成提示文件一般有两种方式:

  • 1⃣️手动编写提示文件additional-spring-configuration-metadata.json,然后mvn install 后会根据该文件生成 spring-

    configuration-metadata.json即为提示文件。

    additional-spring-configuration-metadata.json文件内容如下:

    {
      "properties": [
        {
          "name": "person.properties.id",
          "defaultValue": "000001",
          "type": "java.lang.String",
          "description": "id",
          "sourceType": "org.lc.demo.bean.PersonProperties"
        },
        {
          "name": "person.properties.name",
          "defaultValue": "张三",
          "type": "java.lang.String",
          "description": "姓名",
          "sourceType": "org.lc.demo.bean.PersonProperties"
        },
        {
          "name": "person.properties.gender",
          "defaultValue": "男",
          "type": "java.lang.String",
          "description": "性别",
          "sourceType": "org.lc.demo.bean.PersonProperties"
        },
        {
          "name": "person.properties.city",
          "defaultValue": "北京",
          "type": "java.lang.String",
          "description": "籍贯",
          "sourceType": "org.lc.demo.bean.PersonProperties"
        },
        {
          "name": "person.properties.hobby",
          "defaultValue": [
            "乒乓球",
            "篮球"
          ],
          "type": "java.util.List",
          "description": "爱好",
          "sourceType": "org.lc.demo.bean.PersonProperties"
        }
      ]
    }
    
  • 2⃣️无需编写任何提示文件,需要在@ConfigurationProperties标识的配置属性中定义注释即可在mvn install打包时自动生成提示

    文件spring-configuration-metadata.json

测试

导入依赖后测试

@SpringBootTest
//开启PersonIdCarInfo注入到Bean中
@EnablePersonIdCarAuth
class DemoApplicationTests {
  	//注入属性
    @Autowired
    private PersonProperties personProperties;

    //注入自定义的Bean。若没有此@EnablePersonIdCarAuth注解,则该Bean会注入失败,出现空指针。
    @Autowired
    private PersonIdCarInfo personIdCarInfo;
    @Test
    void test3() {
        System.out.println(personIdCarInfo.toString());
        System.out.println(personProperties.toString());
    }
}

SpringMVC执行流程

  • 客户端(浏览器)发送请求,前端控制器(DispatcherServlet)拦截请求
  • 前端控制器根据请求信息(包括URL、HTTP方法、请求报文头、请求参数、Cookie等)调用处理器映射器(HandlerMapping),处理器映射器根据url找到相应的Handler处理器,也就是controller,并返回一个执行链
  • 前端控制器调用处理器适配器(HandlerAdapter)执行Handler
  • Handler完成业务处理后返回一个ModelAndView对象给前端控制器。ModelAndView包含数据对象和视图逻辑名
  • 视图解析器(ViewResolver)根据ModelAndView中的视图逻辑名查找真正的View
  • 前端控制器就会把ModelAndView中的数据对象填充渲染到View
  • 最后把View响应给客户端渲染