1 | Spring:基础注解使用及场景概述
本来是打算按照书上面的内容,跟着看一看spring的实现。它是从XML中读取bean配置,感觉现在不怎么用XML来配置了,虽然可以跳过bean的读取,直接看bean的生命周期,但是毕竟现在基本上用注解,所以从网上找了一个直接基于注解讲解spring源码的视频。
视频内容讲解spring的AOP感觉很好,至少第一遍的时候,看懂了一个大概。这篇博客在看第一遍的时候,记录了大概的内容,可以算是一个笔记;第二遍是在第一遍笔记的基础上,自己写代码进行实现、测试,并将代码和当时的想法再记录上来,有问题了再看视频。
概览
组件添加类注解
先看看比较基础的注解的用法。这个里面比较重要的注解有@Import,后面讲AOP经常用到,特别是在@Import中传ImportBeanDefinitionRegistrar
的用法。因为AOP的入口点就是利用了这个注解。
@Configuration + @Bean
这是一个很常用的组合,在项目中也经常用到过。但是有一个地方需要注意,被@Bean修改的函数的函数名,默认情况下,会是返回bean的名字。要改名可以通过设置@Bean的name属性。
1 | public class BasicMain { |
结果是:
这里有一个问题:为什么可以生成3个BeanDefinition
,但是获取的时候,会报错失败?。先留给后面吧。
@Scope
spring默认情况下,bean是单例。可设置为多实例:prototype。如下:
1 |
|
然后创建IOC容器,然后对上面的3个bean都获取两次,比较两次获得的内容是否为同一对象。
1 | public static void main(String[] args) { |
结果为:
@ComponentScan
自动扫描包下的bean。
1 | public class ComponentScanMain { |
包、类的情况如下,其中dao和services下面的类,都是空类,没有任何其他数据,只有@Service和@Repository注解。
1 | test-spring/src/main/java/ |
结果
@ComponentScan
还可以指定exclude
和include
属性,分别表示要排除的内容、扫描的内容。使用一个项目中用到的示例,效果就不演示了:
1 |
type还有好几种,分别是:
FilterType.ANNOTATION
。注解类型FilterType.ASSIGNABLE_TYPE
。给定的类型FilterType.ASPECTJ
。AspectJ表达式FilterType.REGEX
。正则表达式FilterType.CUSTOM
。自定义过滤规则
如果指定的是自定义过滤规则,那么后面value表示的类,需要实现TypeFilter
的match方法。
@Lazy
懒加载bean
@Conditional
条件加载bean。其中的value是一个数组类型,数组中的元素必须实现了Condition
接口。
1 |
|
Condition
接口只有一个match方法,实现即可。
1 | public interface Condition { |
matches方法的两个参数,可以用来做什么?简单理解,Spring Boot中有一个@ConditionalOnBean
,基于Conditional,它的作用就是当容器中已经某些特定的bean时,加载某些bean。
另外,在Spring Boot中有很多基于@ConditionalXXXXX的注解,都是基于此注解。以@ConditionalOnProperty
为例,项目中曾用到过此注解,解决一些关于不同环境的配置问题。
1 |
|
所以这个OnPropertyCondition
一定是一个实现了Condition
的类,可以从下面的继承继承结构中看出来:
@Import
快速导入bean,id默认是全类名。这个注解很重要,理解这个注解,对后面AOP的理解有很大的帮助。
普通类
不是很确定能不能导入,特地测试了一下,是可以import到IOC容器中的。
被@Configuration注解的类
实现了
ImportSelector
的类。此项返回的是一个类全名数组。实现了
ImportBeanDefinitionRegistrar
的类。 手动注册Bean到IOC容器。
主要测试一次实现了ImportBeanDefinitionRegistrar
接口的value值,后面用到的比较多。这里先实现一个如下:
1 | public class MyInportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { |
配置信息,主要是验证导入普通类、实现了ImportBeanDefinitionRegistrar
的类。
1 |
|
启动类为,主要功能是打印容器中的bean name:
1 | public class ImportMain { |
结果导入了两个bean,如下:
可以看出来,import普通类时,默认的名称是类全名。
FactoryBean
的使用
实现FactoryBean
的类,然后用@Bean注解一个返回实现了该类的对象。
定义一个Actor的FactoryBean,并实现相应的方法。
1 | public class ActorFactoryBean implements FactoryBean<Actor> { |
在配置类中,生成相应的bean配置。
1 |
|
通过ActorFactoryBean
的bean名称,获取bean,看得到的bean类型是什么,然后再加一个&符号,对比两次结果:
1 | public class ActorFactoryBeanMain { |
容器中存在的bean是getObject()
方法返回的对象,加&可获取factoryBean
本身。
Bean的生命周期
主要参考了这篇博客,总结得很好。
流程图
分类
Bean的完整生命周期经历了各种方法调用,这些方法可以划分为以下几类:
Bean自身的方法:这个包括了Bean本身调用的方法和通过配置文件中bean的
init-method
和destroy-method
指定的方法。Bean级生命周期接口方法:这个包括了
BeanNameAware
、BeanFactoryAware
、InitializingBean
和DiposableBean
这些接口的方法。容器级生命周期接口方法:这个包括了
InstantiationAwareBeanPostProcessor
和BeanPostProcessor
这两个接口实现,一般称它们的实现类为“后处理器”。工厂后处理器接口方法:这个包括了
AspectJWeavingEnabler
,ConfigurationClassPostProcessor
,CustomAutowireConfigurer
等等非常有用的工厂后处理器接口的方法。工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用。
用法
通过@Bean的
initMethod
和destroyMethod
分别指定初始化方法、销毁方法。- 对象创建完成、并赋值好,才调用initMethod;
- 单例bean会执行销毁方法,多实例bean不会执行销毁方法;
- 单示例:在容器启动时创建对象;多实例:在每次获取时创建对象。
- 初始化方法都执行
通过让Bean实现
InitializingBean
、DisposableBean
来实现参与初始化、销毁。InitializingBean
的afterPropertiesSet
方法在initMethod方法之前执行;DisposableBean
的destroy
方法在destroyMethod之前执行。通过
@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的所有依赖都注入完成后,会调用。在InitializingBean
的afterPropertiesSet
方法之前执行(测试结果如此)。@PreDestroy
在DisposableBean
的destroy
方法之前执行(测试结果如此)。通过实现BeanPostProcessor方法来
这个接口太厉害了,数据校验、@Autowire注入等等都是通过此接口来实现的。它有两个函数,他们的执行时机如下所示:
示例
对上面4种情况,参考一些资料写了如下demo,验证其中的调用顺序。
测试bean
1 | public class LifeCycleTestBean |
bean后置处理器
1 | public class MyBeanPostProcessor implements BeanPostProcessor { |
配置类
1 |
|
启动类
1 | public class LifeCycleMain { |
结果。结论都写在上面了。
赋值
@Value
- 基本数值;
@Value("洋芋")
- SpEL;
@Value("#{20 - 2}")
- 配置文件取值。
@Value("${server.port}")
。可以用@PropertySource("classpath:/**.properties")
指定外部配置文件
自动装配
Spring利用依赖注入(DI),完成对IOC容器中各个组件的依赖关系赋值。
@Autowired
Spring注解。 默认优先按类型去容器中找TestBean
组件。
1 |
|
如果有多个TestBean
,默认情况下,会再按照属性名testBean1
去匹配。此时也可以用@Qualifier
指定想要的beanName。如下会加载beanName为testBean的bean。
1 |
|
默认情况下,如果IOC容器中没有相应的bean,会报初始化错误。可以通过@Autowired
注解中的required
属性来设置,默认为true。即:
1 |
|
还可以通过@Primary
来设置首选bean。
@Autowired标注在不同地方的区别:
- set方法上。从IOC容器中取bean作为参数。
- 构造器上。从IOC容器中去bean作为参数;一个参数时,可省略@Autowired。
- 参数上。从IOC容器中去bean作为参数。
- 被@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
基础概念
- 通知(Advice)具体做的工作。比如打印日志。
- 连接点(JoinPoint)可以进行扩展的一个时机。比如说调用方法时、修改一个字段时等,但spring只支持调用方法时。
- 切点(PointCut)通知所要织入的具体位置。以调用方法这个连接点为例,方法有很多,不可能为所有的方法都加入通知,那些被选中的方法就是切点。
- 切面(Aspect)切点与切面的集合。
- 织入(Weaving)将切面应用到目标对象并创建代理对象的过程。通俗理解,把目标对象装饰加强成加入通知的代理对象。有3种织入时期:编译期、类加载期、运行期(Spring AOP)。
使用步骤
导入aop模块,即spring-aspects依赖
1
compile project(":spring-aspects")
定义业务逻辑类。
1
2
3
4
5public class Divider {
public int div(int x, int y) {
return x / y;
}
}定义切面类。
通知包括:①前置通知(@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
public class DividerAspect {
public void pointcut() {
}
public void before() {
System.out.println("Before div()");
}
public void after() {
System.out.println("After div()");
}
public void afterReturning() {
System.out.println("After div() returning");
}
public void afterThrowing() {
System.out.println("After div() throwing");
}
}@Aspect一定要加,这个注解相当于一个开关,加上了才会进行织入操作。
在切面类的目标方法中标注何时在何处运行。
可使用@PointCut简化代码。切点的写法可参考:
将切面类、业务类都放入IOC容器,并且通过IOC容器获取业务类。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class AopConfig {
public Divider getDivider() {
return new Divider();
}
public DividerAspect getDividerAspect() {
return new DividerAspect();
}
}必须使用@Aspect指明哪个bean将作为切面类。
在@Configuration注解标注的配置类中,开启注解
@EnableAspectJAutoProxy
。启动类
1
2
3
4
5
6
7
8public 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));
}
}执行结果
注意:JoinPoint在切面方法中如果要注入,必须要放在第一个参数位上。为啥?深刻怀疑是用了args[0],哈哈。
1 | Spring:基础注解使用及场景概述