1 | Spring:基础注解使用及场景概述

本来是打算按照书上面的内容,跟着看一看spring的实现。它是从XML中读取bean配置,感觉现在不怎么用XML来配置了,虽然可以跳过bean的读取,直接看bean的生命周期,但是毕竟现在基本上用注解,所以从网上找了一个直接基于注解讲解spring源码的视频。

视频内容讲解spring的AOP感觉很好,至少第一遍的时候,看懂了一个大概。这篇博客在看第一遍的时候,记录了大概的内容,可以算是一个笔记;第二遍是在第一遍笔记的基础上,自己写代码进行实现、测试,并将代码和当时的想法再记录上来,有问题了再看视频。

概览

组件添加类注解

先看看比较基础的注解的用法。这个里面比较重要的注解有@Import,后面讲AOP经常用到,特别是在@Import中传ImportBeanDefinitionRegistrar的用法。因为AOP的入口点就是利用了这个注解。

@Configuration + @Bean

这是一个很常用的组合,在项目中也经常用到过。但是有一个地方需要注意,被@Bean修改的函数的函数名,默认情况下,会是返回bean的名字。要改名可以通过设置@Bean的name属性。

1
2
3
4
5
6
7
8
9
10
11
12
public class BasicMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);

// System.out.println(applicationContext.getBean(Person.class));

for (String definitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(definitionName);
}

}
}

结果是:

这里有一个问题:为什么可以生成3个BeanDefinition,但是获取的时候,会报错失败?。先留给后面吧。

@Scope

spring默认情况下,bean是单例。可设置为多实例:prototype。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class BasicAnnotationConfig {

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person getPerson() {
return new Person("萨日朗", 11);
}

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public Actor getActor() {
return new Actor();
}

@Bean
public Programmer getProgrammer() {
return new Programmer();
}
}

然后创建IOC容器,然后对上面的3个bean都获取两次,比较两次获得的内容是否为同一对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicAnnotationConfig.class);

Person person1 = applicationContext.getBean(Person.class);
Person person2 = applicationContext.getBean(Person.class);
System.out.println("Person: " + (person1 == person2));

Actor actor1 = applicationContext.getBean(Actor.class);
Actor actor2 = applicationContext.getBean(Actor.class);
System.out.println("Actor: " + (actor1 == actor2));

Programmer programmer1 = applicationContext.getBean(Programmer.class);
Programmer programmer2 = applicationContext.getBean(Programmer.class);
System.out.println("Programmer: " + (programmer1 == programmer2));
}

结果为:

@ComponentScan

自动扫描包下的bean。

1
2
3
4
5
6
7
8
public class ComponentScanMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ComponentScanConfig.class);
for (String definitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(definitionName);
}
}
}

包、类的情况如下,其中dao和services下面的类,都是空类,没有任何其他数据,只有@Service和@Repository注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
test-spring/src/main/java/
└── run
└── oxffff
└── spring
├── apps
│ └── ComponentScanMain.java
├── configs
│ └── ComponentScanConfig.java
├── dao
│ └── PersonDao.java
└── services
├── SpeakService.java
└── TalkService.java

结果

@ComponentScan还可以指定excludeinclude属性,分别表示要排除的内容、扫描的内容。使用一个项目中用到的示例,效果就不演示了:

1
2
3
4
5
@ComponentScan(excludeFilters = {@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
value = {com.***.service.AlgoProducer.AlgoProducerConfig.class,
com.***.service.WsProducer.WsProducerConfig.class})
})

type还有好几种,分别是:

  • FilterType.ANNOTATION。注解类型
  • FilterType.ASSIGNABLE_TYPE。给定的类型
  • FilterType.ASPECTJ。AspectJ表达式
  • FilterType.REGEX。正则表达式
  • FilterType.CUSTOM。自定义过滤规则

如果指定的是自定义过滤规则,那么后面value表示的类,需要实现TypeFilter的match方法。

@Lazy

懒加载bean

@Conditional

条件加载bean。其中的value是一个数组类型,数组中的元素必须实现了Condition接口。

1
2
3
4
5
6
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}

Condition接口只有一个match方法,实现即可。

1
2
3
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

matches方法的两个参数,可以用来做什么?简单理解,Spring Boot中有一个@ConditionalOnBean,基于Conditional,它的作用就是当容器中已经某些特定的bean时,加载某些bean。

另外,在Spring Boot中有很多基于@ConditionalXXXXX的注解,都是基于此注解。以@ConditionalOnProperty为例,项目中曾用到过此注解,解决一些关于不同环境的配置问题。

1
2
3
4
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {
// ...
}

所以这个OnPropertyCondition一定是一个实现了Condition的类,可以从下面的继承继承结构中看出来:

@Import

快速导入bean,id默认是全类名。这个注解很重要,理解这个注解,对后面AOP的理解有很大的帮助。

  1. 普通类

    不是很确定能不能导入,特地测试了一下,是可以import到IOC容器中的。

  2. 被@Configuration注解的类

  3. 实现了ImportSelector的类。此项返回的是一个类全名数组。

  4. 实现了ImportBeanDefinitionRegistrar的类。 手动注册Bean到IOC容器。

主要测试一次实现了ImportBeanDefinitionRegistrar接口的value值,后面用到的比较多。这里先实现一个如下:

1
2
3
4
5
6
public class MyInportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
registry.registerBeanDefinition("programmer", new RootBeanDefinition(Programmer.class));
}
}

配置信息,主要是验证导入普通类、实现了ImportBeanDefinitionRegistrar的类。

1
2
3
4
@Configuration
@Import({Actor.class, MyInportBeanDefinitionRegistrar.class})
public class ImportConfig {
}

启动类为,主要功能是打印容器中的bean name:

1
2
3
4
5
6
7
8
public class ImportMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ImportConfig.class);
for (String definitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(definitionName);
}
}
}

结果导入了两个bean,如下:

可以看出来,import普通类时,默认的名称是类全名。

FactoryBean的使用

实现FactoryBean的类,然后用@Bean注解一个返回实现了该类的对象。

定义一个Actor的FactoryBean,并实现相应的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ActorFactoryBean implements FactoryBean<Actor> {
@Override
public Actor getObject() throws Exception {
return new Actor();
}

@Override
public Class<?> getObjectType() {
return Actor.class;
}

@Override
public boolean isSingleton() {
return true;
}
}

在配置类中,生成相应的bean配置。

1
2
3
4
5
6
7
@Configuration
public class FactoryBeanConfig {
@Bean
public ActorFactoryBean getActorFactoryBean() {
return new ActorFactoryBean();
}
}

通过ActorFactoryBean的bean名称,获取bean,看得到的bean类型是什么,然后再加一个&符号,对比两次结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ActorFactoryBeanMain {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(FactoryBeanConfig.class);

for (String definitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(definitionName);
}

Object obj1 = applicationContext.getBean("getActorFactoryBean");
System.out.println(obj1.getClass());

Object obj2 = applicationContext.getBean("&getActorFactoryBean");
System.out.println(obj2.getClass());
}
}

容器中存在的bean是getObject()方法返回的对象,加&可获取factoryBean本身。

Bean的生命周期

主要参考了这篇博客,总结得很好。

流程图

分类

Bean的完整生命周期经历了各种方法调用,这些方法可以划分为以下几类:

  1. Bean自身的方法:这个包括了Bean本身调用的方法和通过配置文件中bean的init-methoddestroy-method指定的方法。

  2. Bean级生命周期接口方法:这个包括了BeanNameAwareBeanFactoryAwareInitializingBeanDiposableBean这些接口的方法。

  3. 容器级生命周期接口方法:这个包括了InstantiationAwareBeanPostProcessorBeanPostProcessor 这两个接口实现,一般称它们的实现类为“后处理器”。

  4. 工厂后处理器接口方法:这个包括了AspectJWeavingEnabler, ConfigurationClassPostProcessor, CustomAutowireConfigurer等等非常有用的工厂后处理器接口的方法。工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用。

用法

  1. 通过@Bean的initMethoddestroyMethod分别指定初始化方法、销毁方法。

    • 对象创建完成、并赋值好,才调用initMethod
  • 单例bean会执行销毁方法,多实例bean不会执行销毁方法;
    • 单示例:在容器启动时创建对象;多实例:在每次获取时创建对象。
    • 初始化方法都执行
  1. 通过让Bean实现InitializingBeanDisposableBean来实现参与初始化、销毁。

    InitializingBeanafterPropertiesSet方法在initMethod方法之前执行;

    DisposableBeandestroy方法在destroyMethod之前执行。

  2. 通过@PostConstruct@PreDestroy注解来指定

    这两个是J2EE中的注解,需要导入javax.annotation-api依赖,如下:

    1
    2
    // https://mvnrepository.com/artifact/javax.annotation/javax.annotation-api
    compile group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2'

    @PostConstruct在bean的所有依赖都注入完成后,会调用。在InitializingBeanafterPropertiesSet方法之前执行(测试结果如此)。

    @PreDestroyDisposableBeandestroy方法之前执行(测试结果如此)。

  3. 通过实现BeanPostProcessor方法来

    这个接口太厉害了,数据校验、@Autowire注入等等都是通过此接口来实现的。它有两个函数,他们的执行时机如下所示:

示例

对上面4种情况,参考一些资料写了如下demo,验证其中的调用顺序。

测试bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class LifeCycleTestBean
implements InitializingBean, DisposableBean,
BeanFactoryAware, BeanNameAware {

public LifeCycleTestBean() {
System.out.println("构造器执行...");
}

public void init1() {
System.out.println("bean init method -> init1...");
}

@PostConstruct
public void init2() {
System.out.println("PostConstruct...init2...");
}

@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitializingBean...afterPropertiesSet...");
}

@Override
public void destroy() throws Exception {
System.out.println("DisposableBean...destroy...");
}

@PreDestroy
public void destroy2() throws Exception {
System.out.println("PreDestroy...destroy...");
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("BeanFactoryAware...setBeanFactory...");
}

@Override
public void setBeanName(String name) {
System.out.println("BeanNameAware...setBeanName...");
}
}

bean后置处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Before 【" + beanName + "】 initialization");
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("After 【" + beanName + "】 initialization");
return bean;
}
}

配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class LifeCycleConfig {
@Bean(initMethod = "init1", destroyMethod = "destroy2")
public LifeCycleTestBean getLifeCycleTestBean() {
return new LifeCycleTestBean();
}

@Bean
public MyBeanPostProcessor getMyBeanPostProcessor() {
return new MyBeanPostProcessor();
}

@Bean
public Actor getActor() {
return new Actor();
}
}

启动类

1
2
3
4
5
6
7
8
public class LifeCycleMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
System.out.println("容器创建完成...");
LifeCycleTestBean lifeCycleBean = ac.getBean(LifeCycleTestBean.class);
ac.close();
}
}

结果。结论都写在上面了。

赋值

@Value

  1. 基本数值;@Value("洋芋")
  2. SpEL;@Value("#{20 - 2}")
  3. 配置文件取值。@Value("${server.port}")。可以用@PropertySource("classpath:/**.properties")指定外部配置文件

自动装配

Spring利用依赖注入(DI),完成对IOC容器中各个组件的依赖关系赋值。

@Autowired

Spring注解。 默认优先按类型去容器中找TestBean组件。

1
2
@Autowired
private TestBean testBean1;

如果有多个TestBean,默认情况下,会再按照属性名testBean1去匹配。此时也可以用@Qualifier指定想要的beanName。如下会加载beanName为testBean的bean。

1
2
3
@Qualifier("testBean")
@Autowired
private TestBean testBean1;

默认情况下,如果IOC容器中没有相应的bean,会报初始化错误。可以通过@Autowired注解中的required属性来设置,默认为true。即:

1
2
@Autowired(required = false)
private TestBean testBean1;

还可以通过@Primary来设置首选bean。

@Autowired标注在不同地方的区别:

  1. set方法上。从IOC容器中取bean作为参数。
  2. 构造器上。从IOC容器中去bean作为参数;一个参数时,可省略@Autowired。
  3. 参数上。从IOC容器中去bean作为参数。
  4. 被@Bean注解的函数的参数,可用@Autowired注入,可省略。

@Resource

Java规范注解。 默认按照组件名称进行装配,可以通过name属性修改。不支持@Primary、没有required属性。

@Inject

Java规范注解。 需要导入依赖javax.inject。与@Autowired类似,但是没有required属性。

环境相关

@Profile

它的作用是指定此Bean所对应的环境,不是该环境时,不加载此bean。环境可以通过spring.profiles.active属性来指定,也可以通过JVM参数:-Dspring.profiles.active=test来指定。

获取Spring容器底层组件

想要名为xxx的组件,实现xxxAware接口,最后交由xxxAwareProcessor

例如:获取ApplicationContext需要实现ApplicationContextAware,而对它的处理是通过ApplicationContextAwareProcessor来处理,ApplicationContextAwareProcessor即实现了BeanPostProcessor,也就是前面所遇到的那个后置处理器。

Spring AOP

基础概念

  1. 通知(Advice)具体做的工作。比如打印日志。
  2. 连接点(JoinPoint)可以进行扩展的一个时机。比如说调用方法时、修改一个字段时等,但spring只支持调用方法时
  3. 切点(PointCut)通知所要织入的具体位置。以调用方法这个连接点为例,方法有很多,不可能为所有的方法都加入通知那些被选中的方法就是切点
  4. 切面(Aspect)切点切面的集合。
  5. 织入(Weaving)将切面应用到目标对象并创建代理对象的过程。通俗理解,把目标对象装饰加强成加入通知的代理对象。有3种织入时期:编译期类加载期运行期(Spring AOP)。

使用步骤

  1. 导入aop模块,即spring-aspects依赖

    1
    compile project(":spring-aspects")
  2. 定义业务逻辑类。

    1
    2
    3
    4
    5
    public class Divider {
    public int div(int x, int y) {
    return x / y;
    }
    }
  3. 定义切面类。

    通知包括:①前置通知(@Before)、②后置通知(@After)、③返回通知(@AfterReturning)、④异常通知(@AfterThrowing)、⑤环绕通知(@Around),分别对应目标方法执行前、执行后、返回后、异常时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Aspect
    public class DividerAspect {
    @Pointcut("execution(* run.oxffff.spring.beans.Divider.div(..))")
    public void pointcut() {

    }

    @Before("pointcut()")
    public void before() {
    System.out.println("Before div()");
    }

    @After("pointcut()")
    public void after() {
    System.out.println("After div()");
    }

    @AfterReturning("pointcut()")
    public void afterReturning() {
    System.out.println("After div() returning");
    }

    @AfterThrowing("pointcut()")
    public void afterThrowing() {
    System.out.println("After div() throwing");
    }
    }

    @Aspect一定要加,这个注解相当于一个开关,加上了才会进行织入操作。

  4. 在切面类的目标方法中标注何时在何处运行。

    可使用@PointCut简化代码。切点的写法可参考:

  5. 切面类业务类都放入IOC容器,并且通过IOC容器获取业务类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    @EnableAspectJAutoProxy
    public class AopConfig {
    @Bean
    public Divider getDivider() {
    return new Divider();
    }

    @Bean
    public DividerAspect getDividerAspect() {
    return new DividerAspect();
    }
    }
  6. 必须使用@Aspect指明哪个bean将作为切面类。

  7. 在@Configuration注解标注的配置类中,开启注解@EnableAspectJAutoProxy

  8. 启动类

    1
    2
    3
    4
    5
    6
    7
    8
    public class AopMain {
    public static void main(String[] args) {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
    Divider divider = ac.getBean(Divider.class);
    System.out.println(divider.div(1, 2));
    System.out.println(divider.div(1, 0));
    }
    }
  9. 执行结果

注意:JoinPoint在切面方法中如果要注入,必须要放在第一个参数位上。为啥?深刻怀疑是用了args[0],哈哈。

1 | Spring:基础注解使用及场景概述

https://eucham.me/2020/03/25/5b62a62a88c2.html

作者

遇寻

发布于

2020-03-25

更新于

2022-04-21

许可协议

评论