spring注解-辅助学习springboot和springcloud

1. 前言

因为后面要学习sb(springboot)和sc(springcloud),所以需要学习一些相关的注解和了解他们底层代码实现,例如@import注解,Aware接口,生命周期。

​ 通过前面几个章节的学习,你会发现无论springboot还是springcloud的很多知识都是来源于spring相关的知识,章节还有一些内容没有补充完整,后续如果有时间会逐步更新完成!!!

2.组件注册

我们知道spring得IOC容器中存储了很多类的实例化对象,那么下面介绍几种往IOC容器中注册实体类的方式

2.1 xml配置文件实例化实体类(方式一)

  1. 首先新建spring.xml 文件,通过bean 标签实例化实体类

    1567414688035

2.通过ClassPathXmlApplicationContext获取实体类

1567414725611

缺点:大型项目的实体类会非常多,那么配置文件会变得非常的臃肿,而且也不易于维护。

2.2 使用@ Configuration和@bean(方式二)

Spring提供了配置实体类的第二种方式,就是不用通过书写xml文件,通过两个注解就可以达到2.1 的功能。

  1. 编写配置类,配置bean

1567414770812

  1. 通过AnnotationConfigApplicationContext获取实体类

    1567414786811

这里是new了一个AnnotationConfigApplicationContext对象,以前new的ClassPathXmlApplicationContext对象,的构造函数里面传的是配置文件的位置,而现在AnnotationConfigApplicationContext对象的构造函数里面传的是配置类的类型

2.3 @ComponentScan-自动扫描组件&指定扫描规则(方式三)

**实际上这个注解跟前面两种方式是配合使用的,避免书写ClassPathXmlApplicationContext或者AnnotationConfigApplicationContext** **获取IOC容器。**

我们知道,在实际开发中我们是不会通过ClassPathXmlApplicationContext这样的代码方式获取对象,而是通过包扫描的方式进行实例化对象并注入IOC容器中

1567414821714

他的扫描规则是:以下这几个注解都是继承自@Component

@controller(给web层的注解)

@service(给serivce层加的注解)

@repository(给dao层加的注解)

@component(给java类加注解,老版本spring只有这一个注解)

只要书写了上面四个注解的类,那么会自动装配到ioc容器中。Id默认是类名首字母小写

1. @ComponentScan注解的结构

1567414886849

1.这个注解上,也是可以指定要排除哪些包或者是只包含哪些包来进行管理:里面传是一个Filter[]数组。

2.Value :就相当于spring的xml配置文件-

1567414894134

2.用例

–那么我们使用配置类的方式实现component-sacn同样的功能

1.首先在配置文件类,添加扫描的范围

1567414906119

2.添加几个注解类-

@controller(UserController)

@service(UserService)

@repository (UserDao)

@component(ComponentTest)

1567414919145

3.书写IOC

1567414938745

输出:

1567414956045

发现-扫描进入IOC容器的bean的id默认是:类名首字母小写

4.使用Filter去除某些注解类

1567414983170

根据注解的方式排除,排除使用@Controller注解注解的类

–注意在使用includeFilters 扫描只包含那些组件的时候,要禁用spring默认全局扫描(跟配置文件一样,也是需要禁用的)

1567414996062

例子4

1567415050284

3. 扩展

我们打开Component注解的源码,发现他是:

1567415143194

多了Repeatable注解,也就是说明,这个Component注解是可以多次重复用的

1567415194264

那么你可能会问,如果不是jdk1.8,那么怎么书写多个扫描策略呢?

1567415212225

1567415217769

也就是说,我们可以在配置类,使用ComponentScans注解,配置多个扫描策略

例子:

1567415246906

跟 { 例子4 } 效果一样

4.1 FilterType 过滤规则

1567415321043

下面我们注重讲解一下,CUSTOM 自定义实现类

1567415342468

需要先实现 TypeFilter

1.首先定义一个扫描规则类

1567415368545

  1. 配置类,实现自定义过滤规则

1567415379844

表示 – 扫描的类中如果包含er那么就会被过滤掉(注意:他是会取扫描com.kingge下面的所有类-包括哪些没有被注解,注解的类也会被扫描

2.4 @Import注解

有三种使用方式

第一种写法:直接在import注解中配置需要导入的类

1567415399146

他在IOC容器中的id是:(全类名)

1567415407178

缺点:如果有多个类需要注入IOC,那么代码量就很长

第二种实现方式:自定义导入逻辑,批量导入,只需要返回需要导入的全类名数组

实现ImportSelector 类

1567415418134

配置类上使用

1567415430330

这样 pp就注入到了IOC 容器中

第三种方式:ImportBeanDefinitionRegistrar 实现这个类。

1567415449102

例子:

1567415496868

配置类引用

1567415510372

调用:

1567415521934

1567415535174

2.4.1 总结

@Import[快速的给容器中导入一个组件]

(1)、 @Import(要导入容器中的组件);容器中就会自动的注册这个组件,id默认是全类名

(2)、 ImportSelector :返回需要的组件的全类名的数组;

(3)、 ImportBeanDefinitionRegistrar : 手动注册bean到容器中

前面学习的springboot中,用到了该注解的次数很多。

2.5 Factorybean 工厂bean

1567415551953

例子:

1.实现这个工厂bean

1567415565829

getObject 方法:当调用bean时候,调用这个方法获取bean实例。

getObjectType:返回对象类型

isSIngleton: 是否是单例。False-表示是多例。True-表示是单例。(如果配置类中配置@Scope注解,企图改变UserDao的单实例,无效,以isDingleto方法设置为准

2.配置类配置

1567415579392

输出:

1567415589497

输出:

class com.kingge.dao.UserDao

false

第二个输出肯定是false

不过为什么第一个输出的是 Userdao的全类名而不是UserDaoFactoryBean的全类名呢?因为在构造的时候spring默认返回的就是getObjectType的值。

那么怎么获取这个工厂bean呢?spring提供了一个方式:加上&

1567415621835

输出:class com.kingge.utils.UserDaoFactoryBean

2.6 总结-组件注册

1567415637061

/**

 \* 给容器中注册组件:

 \* 1)、扫描+组件标注注解(@Controller/@Service/@Repository/@Component)

 \* 【局限于要求是自己写的类,如果导入的第三方没有添加这些注解,那么就注册不上了】

 *

 \* 2)、@Bean[导入的第三方包里面的组件]

 \* 3)、@Import[快速的给容器中导入一个组件]

 \*      (1)、 @Import(要导入容器中的组件);容器中就会自动的注册这个组件,id默认是全类名

 \*      (2)、 ImportSelector :返回需要的组件的全类名的数组;

 \*      (3)、 ImportBeanDefinitionRegistrar : 手动注册bean到容器中

 *

 \* 4)、使用Spring提供的FactoryBean(工厂bean)

 \*      (1)、默认获取到的是工厂bean调用getObject创建的对象

 \*      (2)、要获取工厂bean本身,我们需要给id前面加上一个“&”符号:&userDaoFactoryBean

第一种方式:一般用于自己定义的类,但是如果我们是通过导入第三方jar的方式导入了很多组件(类),但是我想把这些类注册到IOC容器中怎么办呢?这个时候就需要使用bean注解的方式注册组件。

第二种方式:可以实现自定义类或者第三方类的注入到IOC容器。缺点,那就是每实例化一个bean就得写个方法。这样代码量太多。

第三种方式:import标签(作用在配置类)

3.修饰Bean的相关注解

3.1 @Scope注解

他一般是和@Bean注解配套使用,标识实体类的作用范围。我们知道IOC容器中的实体类,默认是单实例的。

证明:

  1. 自定义IOC容器-实现配置类

1567415657803

2.获取 Person实体类

1567415670181

我们不难发现-这里输出的是true,所以spring扫描bean策略默认是单实例。

那么怎么修改这种作用域呢?

查看Scope注解源码,发现可以指定这四种类型的作用范围

1567415683803

第一个是多实例,第二个是:单实例(默认值

第三个是:web环境下,用一个请求创建一次实例

第四个是:web环境下,同一个session创建一次实例

那么上诉代码只需要修改为

1567415696308

这样就是多实例。

总结:

单实例:在IOC容器启动的时候就已经实例化好Person(调用getPerson实例化),那么每次获取的时候直接从IOC容器中拿。

1567415714441

这段代码运行时候会去调用getPerson方法完成实例

多实例:IOC容器启动时,不会去实例化Person,而是每次获取的时候才会去调用getPerson获取对象。

3.2 @Lazy注解

和@Bean注解配套使用,解决单实例bean在IOC容器启动就马上创建实例的问题。

懒加载bean,这个只对于单实例的情况下才有用

1567415726984

也就是IOC容器初始化的时候,不会去调用getPerson,实例化Person。第一次获取的时候才会去创建,以后再使用该实例化,会使用以前获取的

3.3 @Conditional

根据满足某个特定的条件创建一个特定的Bean。

因为我们可能存在一个需求那就是,根据不同的业务场景我们会有选择性的实例化某些bean,那么就可以使用这个注解。

1.例子

需求:根据不同的系统实例化不同的bean。

1567415756903

输出:

1567415769824

l 增加需求 当使用windows系统时,在IOC容器中创建windows实体类,反之创建linux 实体类

1567415788706

\1. 实现两个 条件类

1567415816993

1567415830634

2.配置类

1567415842983

\2. 实例化IOC 容器

1567415854254

1567415865773

你会发现仅仅只是实例化了windows,linux实体类已经不见了,那么说明是条件生效了。

备注:

@Conditional注解是可以作用在配置类上面的,那么他的作用就是全局的条件,只有满足了这个条件,配置类里面的bean才能够实例化。(局部方法配置Conditional注解会失效)

4.生命周期

我们知道,Bean的生命周期是由IOC容器来管理的,那么我们也是可以自定义初始化方法和销毁方法。

Bean生命周期:bean创建-初始化-销毁,那么下面我们将来介绍,能够控制Bean生命周期的几种方式。

4.1. init-method、destory-method 管理bean生命周期

需要注意的是,单实例和多实例的情况下,bean生命周期是不一样的。单实例bean的生命周期全部托管给IOC容器,多实例部分托管

1.在配置文件XML中:

1567415958501

这两个方法是来控制初始化和销毁的

2.代码控制初始化和销毁

单实例情况下

其实也就是在Bean注解,上填充init-method和destory-method方法

1567415979268

1567415990937

1567415998217

输出:

1567416012675

因为是单实例的原因-所以容器启动的时候就开始调用了无参构造器创建对象,然后调用init初始化方法,容器关闭时,调用销毁方法.

多实例情况下

我们把配置类获取Car对象的方法改为多实例的形式,观察输出。

1567416024604

1567416034091

输出

1567416054016

我们发现我们在关闭容器的时候,他并没有调用destory销毁实例,因为多实例的bean他是不归于容器管辖,需要我们自己手动销毁

3.总结

总的来说,bean在IOC容器的生命周期如下:

1567416082833

* 我们可以自定义初始化和销毁方法;容器在bean进行到当前生命周期的时候来调用我们自定义的初始化和销毁方法
*
* 构造(对象创建)
* 单实例:在容器启动的时候创建对象
* 多实例:在每次获取的时候创建对象
*
* BeanPostProcessor.postProcessBeforeInitialization
* 初始化:
* 对象创建完成,并赋值好,调用初始化方法。。。
* BeanPostProcessor.postProcessAfterInitialization
* 销毁:
* 单实例:容器关闭的时候
* 多实例:容器不会管理这个bean;容器不会调用销毁方法;

4.2 InitializingBean和DisposableBean 控制bean生命周期

1单实例情况下

1.新建Food类实现这两个接口

1567416100346

配置类扫描

1567416118482

测试

1567416129315

输出:

1567416137640

很明显单实例情况下,bean的生命周期是全部托管到IOC容器中。

2.多实例情况下

如果Food注入IOC容器时,选择多实例的方式的话,那么上面的案例在启动IOC容器时,不会有任何输出,因为多实例的情况下只有获取对象才会去做相关的初始化工作。

验证1:

1567416163330

没有任何输出。

验证2:

1567416175448

输出:

1567416191108

3.总结

很明显没有调用DisposableBean接口的destory方法和自定义的destory方法。

也就是说在多实例的情况下IOC容器只帮我们做创建和初始化bean的工作,但是销毁bean的工作他没有帮我们做,需要自己去实现。

4.3 @PostConstruct和@PreDestroy注解

这两个注解是作用在方法上面的。

可以使用JSR250规范里面定义的两个注解:

@PostConstruct :在bean创建完成并且属性赋值完成,来执行初始化方法

@PreDestroy :在容器销毁bean之前通知我们来进行清理工作

1567416209944

初始化容器

1567416222017

输出:

1567416237939

很明显-这两个注解的作用比4.2章节的两个接口的重载方法的调用更早,注意看官方的 注释说明

1567416255764

注意:这两个注解注解的方法,无返回值(void)

4.4 BeanPostProcessor 后置处理接口(重要)

我们发现上面三种管理bean生命周期的方式,他们的方法是没有入参和出参的。,下面这种方式提供了

BeanPostProcessor接口:bean的后置处理器,在bean初始化前后做一些处理工作,这个接口有两个方法:

postProcessBeforeInitialization:在初始化之前工作

postProcessAfterInitialization:在初始化之后工作

(1). 实现Food实体类

1567416280606

(2).配置类

1567416311128

(3). 启动容器查看输出

1567416341585

(4)输出

1567416364626

他没有销毁方法。

4.1-4.4总结

上面这四种方式调用顺序

对象构造器 –>> PostConstruct ->> afterPropertiesSet ->> init-method ->> 1567416382230 ->> 1567416398530->>PreDestroy注解 自定义实现的destory方法->> DisposableBean的destroy方法 ->>Food 自定义实现的destoryMethod方法

销毁:

@PreDestroy注解的 PreDestroy —》DisposableBean接口的destory —》 destroy-method

4.5BeanPostProcessor 原理

1.两个方法打上断点

1567416431574

Dubug方式启动IOC容器

1567416442452

2.查看方法栈调用

创建容器构造器

1567416469689

1567416483129

前置处理器调用的方法:调用getBeanPostProcessors()方法找到容器里面的所有的BeanPostProcessor,挨个遍历,调用BeanPostProcessor的postProcessBeforeInitialization方法,一旦调用postProcessBeforeInitialization方法的返回值为null的时候,就直接跳出遍历 ,后面的BeanPostProcessor 的postProcessBeforeInitialization也就不会执行了:

1567416501945

后置处理器调用的方法:调用getBeanPostProcessors()方法找到容器里面的所有的BeanPostProcessor,挨个遍历,调用BeanPostProcessor的postProcessAfterInitialization方法,一旦调用postProcessAfterInitialization方法的返回值为null的时候,就直接跳出遍历 ,后面的BeanPostProcessor 的postProcessAfterInitialization也就不会执行了:

1567416524867

1567416538029

1567416547870

2.BeanPostProcessor在springboot中的使用

查看该接口的实现类

1567416562220

这个接口,其实在spring的IOC容器中使用的频率是很多的,而且spring提供了很多实现类,例如如果我们想在bean中使用IOC容器的话,那么就可以使用

1.ApplicationContextAwareProcessor

给实体类,注入IOC容器。

ApplicationContextAware 接口,注入IOC容器

1567416620035

例如:Dog实体类需要使用到IOC容器,那么就可以实现这个接口

1567416633367

1567416638886

然后 他实际上是去 调用这个ApplicationContextAwareProcessor,方法,在创建Dog 对象他会去调用 postProcessBeforeInitialization 方法,判断实例化

1567416668422

然后判断当前Dog实体类是否实现了ApplicationContextAware,如果是,那么调用invokeAwareInterface注入,IOC容器。

1567416682517

最终去调用 Dog的 setApplicationContext 方法,赋值。

1. BeanValidationPostProcessor

实体类校验,后置处理器

2.InitDestroyAnnotationBeanPostProcessor

这个处理类,就是处理,我们3.3章节的两个注解。

3.AutowiredAnnotationBeanPostProcessor

这个类就是处理我们的Autoware注解的

4. BeanFactoryPostProcessor

BeanFactory的后置处理器,在BeanFactory的标准初始化之后调用

所有bean的定义已经保存加载到BeanFactory,但是bean的实例还未创建

1567416709158

运行IOC容器

1567416721160

查看输出:

1567416731721

1567416742609

很明显他是在bean实例创建之前执行的。

BeanFactoryPostProcessor原理:

1)、ioc容器创建对象

2)、invokeBeanFactoryPostProcessors(beanFactory);

如何找到所有的BeanFactoryPostProcessor并执行他们的方法;

          1)、直接在BeanFactory中找到所有类型是BeanFactoryPostProcessor的组件,并执行他们的方法

          2)、在初始化创建其他组件前面执行

5.BeanDefinitionRegistryPostProcessor

1567416769528

启动ioc容器查看输出

1567416783886

1567416793969

6. ApplicationListener

1567416807398

1567416820306

5.属性赋值

5.1 @Value注解

这个注解一般是作用在类的属性上面,他的作用等同于

1567417097750

那么他可以书写那些值呢?

1567416934645

第三种是取配置文件的数据

1567416950884

那么怎么使用 第三种方式赋值呢?下面讲解

5.2 @ PropertySource 注解

他的作用相当于XML的:

1567416959675

1.配置类添加配置注解

1567417123219

2.Person实体类

1567417130983

输出:

1567417138775

我们也可以通过IOC容器手动的去获取配置的信息

1567417160484

3.3 通过实现 EmbeddedValueResolverAware 获取属性值

l 见6.5 章节

6.自动装配

6.1 @Autowire、@Qualifier、@Primary(spring规范的注解)

在spring的项目中我们是经常这个Autowire来进行实体类之间的依赖注入,他的注入规则是:

\1. 默认按照类型去IOC容器中查找需要的实体类(例如UserDao.class)

\2. 如果找到多个同类型的实体类,那么他会根据属性名作为组件ID去进一步匹配。

例如:

1567417175481

然后IOC容器中有两个UserDao实例,一个是ID为userDao,一个ID为userDao1.

那么上面service注入的是哪一个呢?

答案:注入的是ID为userDao的实体类。如果想要注入userDao1,那么应该把属性名改为userDao1

1567417185951

@Qualifier,指定需要装配的ID,取消默认根据属性名去匹配。

1567417197152 默认是必须找到需要的依赖实体类,然后注入Service,否则就会报错,我们可以使用required属性来控制

@Primary : 1567417205766

这个注解是作用在被依赖的实体类(UserDao)上面,明确指定,当某个类(UserService)依赖这个实体类的时候,假设IOC容器中存在多个相同类型的被依赖类(UserDao)那么首选呗Primary注解的被依赖类。(

1567417215346

如果UserService同时使用了@Qualifier注解 ,那么@Primary的效果将会失效,以Qualifier注解需要的ID为主

@Autowire注解扩展

他可以标在构造器上,方法上

1567417225715

6.2 @Resource、@Inject(java规范的注解)

![1567417242591](spring注解-辅助学习springboot和springcloud\1567417242591.png)

@Resource注解

他的作用跟@Autowire注解的作用是一样的,默认根据属性名进行装配。Name属性可以更改装配的id

1567417260280

1567417265681

@Inject 的使用,需要添加依赖

1567417277299

1567417283634

1567417290770

支持@Primary功能,但是他没有属性

1567417301282

6.3 Aware接口 (重要)

自定义组件想要使用Spring容器底层的一些组件(ApplicationContext、BeanFactory…)

自定义组件实现xxxAware接口就可以实现,在创建对象的时候,会调用接口规定的方法注入相关的组件,把Spring底层的一些组件注入到自定义的bean中。 xxxAware等这些都是

利用后置处理器的机制,比如ApplicationContextAware 是通过ApplicationContextAwareProcessor来进行处理的。

如果我们想在自定义实体类中,使用IOC容器的context怎么办呢?

例子:我有在第四章节中 BeanPostProcessor中讲过。

1567417315217

1567417323109

例子:

1567417358798

1567417366098

1.下面我们就是用一个例子来详细讲解一下Aware接口的工作流程。

(1).实现一个entity,实现ApplicationContextAware 接口

实现该接口的setApplicationContext方法。

1567417382674

(2). 配置类,配置Blue实体类,实例化到IOC容器中

1567417392347

(3).获取IOC容器

1567417433281

(4)输出

1567417443301

2.源码分析

在setApplicationContext 打个断点。

1567417460772

发现他是去调用 ApplicationContextAwareProcessor 实体类,这个实体类实现了BeanPostProcessor 后置处理器。

2.执行postProcessBeforeInitialization 前置方法,判断当前的实体类是否继承了某些接口。做一些权限判断

1567417474653

3.然后调用 invokeAwareInterfaces ,紧接着调用实体类实现的 1567417484555 方法,注入IOC容器

1567417501646

6.4 @Profile注解

和Springboot的profile是一致的。

@profile注解是spring提供的一个用来标明当前运行环境的注解。我们正常开发的过程中经常遇到的问题是,开发环境是一套环境,qa测试是一套环境,线上部署又是一套环境。这样从开发到测试再到部署,会对程序中的配置修改多次,尤其是从qa到上线这个环节,让qa的也不敢保证改了哪个配置之后能不能在线上运行。

为了解决上面的问题,我们一般会使用一种方法,就是配置文件,然后通过不同的环境读取不同的配置文件,从而在不同的场景中跑我们的程序。

那么,spring中的@profile注解的作用就体现在这里。在spring使用DI来依赖注入的时候,能够根据当前制定的运行环境来注入相应的bean。最常见的就是使用不同的DataSource了。

下面-结合 properties配置文件的三种注入方式来讲解一下@Profile注解的用法

—-以作用在方法上,表示只要在当前设置的环境下才会往IOC容器中注册当前bean。

—-作用在类上面

1567417520813

类里面的所有bean,能够被注册到IOC容器中的条件是:只要开发环境满足了当前配置类上面的Prifile注解标识的环境。

例如:

1567417527878

主要开发环境是test 里面的bean才能够被注册。

-

6.5 @profile的使用

我们知道,如果在组件上标识了这个注解,那么如果没有激活,那么就不会被注册到IOC容器中。通过这个特性来过滤一些组件的注册。

@Profile(“default”) 是默认注册某个bean

1.配置类

1567417566591

2.测试

1567417580291

输出:

1567417588458

很明显,三个配置都没有被注册在IOC容器中,因为没有指定运行环境。

\3. 制定运行环境(第一种方式:虚拟机参数位置添加-Dspring.profiles.active=test)

1567417616396

这样

1567417625690 就会输出了。

\4. 制定运行环境(第二种方式-代码方式)

1567417638837

可以指定多个配置环境

输出:

1567417653302

7.AOP

什么叫AOP和他的作用

在程序运行期间,动态的将某段代码切入到指定方法运行时的指定时机运行,其实就是动态代理。

作用场景

可以在某个业务实现的过程前后,或者出现异常,进行一些额外业务的操作。例如当你调用add()方法进行加法运算的时候,我们可以在调用方法前,得到结果后,或者出现异常时,记录一些日志。以前我们传统的做法是,在方法里面打印日志(System.out.println),但是这样会造成耦合,而且我们也想把打印日志抽离成一个统一的模块。

1. 例子

Maven依赖:spring提供了对AOP的支持

(1)导入aop依赖

<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.14.RELEASE</version>
</dependency>

(2)MathCalculator.java

业务逻辑类:要求在业务方法运行时打印日志

1567417713757

(3):日志切面类:LogAspects.java

package com.kingge.aop;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
/**
* 切面类
* @Aspect: 告诉Spring当前类是一个切面类
*
*/
@Aspect
public class LogAspects {
//抽取公共的切入点表达式
//1、本类引用
//2、其他的切面引用
@Pointcut("execution(public int com.kingge.aop.MathCalculator.*(..))")
public void pointCut(){};
//@Before在目标方法之前切入;切入点表达式(指定在哪个方法切入)
@Before("pointCut()")
public void logStart(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
System.out.println(""+joinPoint.getSignature().getName()+"运行。。。@Before:参数列表是:{"+Arrays.asList(args)+"}");
}
@After("com.kingge.aop.LogAspects.pointCut()")
public void logEnd(JoinPoint joinPoint){
System.out.println(""+joinPoint.getSignature().getName()+"结束。。。@After");
}
//JoinPoint一定要出现在参数表的第一位
@AfterReturning(value="pointCut()",returning="result")
public void logReturn(JoinPoint joinPoint,Object result){
System.out.println(""+joinPoint.getSignature().getName()+"正常返回。。。@AfterReturning:运行结果:{"+result+"}");
}
@AfterThrowing(value="pointCut()",throwing="exception")
public void logException(JoinPoint joinPoint,Exception exception){
System.out.println(""+joinPoint.getSignature().getName()+"异常。。。异常信息:{"+exception+"}");
}
}
这四个方法,都是作用在MathCalculator的add方法,那么他们的切入点表达式都是一样的,为了避免重复书写,我们一般采用抽取公共切入点的方式,抽取出来,复用。---- 使用@PoinCut注解

切面类中的方法也称为通知方法:

前置通知(@Before):在目标方法运行之前运行

后置通知(@After):在目标方法运行之后运行,即使出现异常也会运行

返回通知(@AfterReturning):在目标方法正常返回之后运行

异常通知(@AfterThrowing):在目标方法运行出现异常之后运行

环绕通知(@Around):动态代理,手动推进目标方法的运行

(4)开启spring切面自动代理

1567417746703

**使用Spring的切面需要开启Spring的切面自动代理,只需要在配置类中加注解@EnableAspectJAutoProxy,Spring中有很多@EnableXxx(关于这点我们在springcloud中使用的最多,自动配置)注解,用来开启一些功能**
配置bean怎么区分哪个bean是切面类呢,它会看哪个类上有@Aspect注解,另外切面方法的执行仅对Spring容器中的bean起作用,对于我们自己new出来的对象是不起作用的,原因也很简单,我们自己创建的bean并没有被spring管理,也就没有为其设置切面方法等。

    通过JoinPoint对象获取调用目标方法时的信息,比如方法名、参数等,使用returning指定用通知方法的哪个入参接收返回值,使用throwing指定用哪个入参接收异常,另外如果使用JoinPoint,则必须将其放在切面方法入参的第一个位置,否则会报错

(5)测试

正常计算

1567417764521

错误计算

1567472449448

给一个出现异常的 1 /0 运算处理,查看日志。

1567417773670

你会发现,无论是否出现异常 logStart 和 logEnd 都会正常输出,如果正常返回那么@AfterReturning标识的方法会被调用,如果运算发生异常那么@AfterReturning标识的方法不会被调用,而@AfterThrowing标识的异常处理方法会被调用

总结

1567417705277

2. AOP原理

通过上面的例子我们知道,实现AOP的关键点在于我么能使用@EnableAspectJAutoProxy注解,那么接下来我们查看一下这个注解到底做了什么工作

1.查看@EnableAspectJAutoProxy注解

1567417786810

两个属性的含义:

英文注解已经很详细了,这里简单介绍一下两个参数,一个是控制aop的具体实现方式,为true 的话使用cglib,为false的话使用java的Proxy,默认为false,第二个参数控制代理的暴露方式,解决内部调用不能使用代理的场景,默认为false

2.查看一下 AspectJAutoProxyRegistrar.java 到底导入了哪些类

1567417799676

很明显这个类是采用了ImportBeanDefinitionRegistrar的方式注册了某些类大oIOC容器中,那么我们看一下他到底注入了什么类。

核心是这里: AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
一个AOP的工具类,这个工具类的主要作用是把AnnotationAwareAspectJAutoProxyCreator这个类定义为BeanDefinition放到spring容器中,这是通过实现ImportBeanDefinitionRegistrar接口来装载的,具体装载过程不是本篇的重点,这里就不赘述,我们重点看AnnotationAwareAspectJAutoProxyCreator这个类.

1567438805334

从类图是可以大致了解AnnotationAwareAspectJAutoProxyCreator这个类的功能.它实现了一系列Aware的接口,在Bean装载的时候获取BeanFactory(Bean容器),Bean的ClassLoader,还实现了order接口,继承了PorxyConfig,ProxyConfig中主要封装了代理的通用处理逻辑,比如设置目标类,设置使用cglib还是java proxy等一些基础配置.

而能够让这个类参与到bean初始化功能,并为bean添加代理功能的还是因为它实现了BeanPostProcessor这个接口.这个接口的postProcessAfterInitialization方法会在bean初始化结束后(赋值完成)被调用。

3.最顶部的抽象类:AbstractAutoProxyCreator

注意看bean初始化的方法

1567439298675

当我们开启了EbableAspectJAutoProxy后,每次Bean的装配时,都会执行这段逻辑.前面主要是校验是否需要对bean进行代理(特殊的类,和已经被代理),核心逻辑在后面几行.getAdvicesAndAdvisorsForBean方法来获取所有符合条件的切面,具体的实现在子类,这里是抽象方法,获取切面后就是创建代理:

1567439441240

TargetSource中存放被代理的对象,这段代码主要是为了构建ProxyFactory,将配置信息(是否使用java proxy,是否threadlocal等),目标类,切面,传入ProxyFactory中,而在ProxyFactory中,会通过createAopProxy()方法创建代理工厂DefaultAopProxyFactory,由代理厂生成具体的代理对目标类进行代理:

进入proxyFactory.getProxy(getProxyClassLoader());getProxy()方法

跳到

1567439674687

进入createAopProxy(),跳转到

1567439733523

我们可以查看AopProxy的都有哪些 ,在AOpProxy上按键:ctrl t,

1567439796941

很明显有我们熟悉的cglib和jdk、默认的实现

紧接着进入createAopProxy(this)

1567439888919

是个接口,查看他的默认实现类。

4. DefaultAopProxyFactory aop代理获取类

1567439971239

可以看到,在这里有我们在注解中设置的参数的判断逻辑,是创建java代理,还是cglib代理,有关cglib的讲解请看cglib的使用.

我们主要看一下JdkDynamicAopProxy的实现,因为我们没有设置@EnableAspectJAutoProxy(proxyTargetClass=true) 所以我们默认使用jdk自带实现。cglib其实差不多。

5. JdkDynamicAopProxy 默认切面代理类

@Override
public Object getProxy() {
return getProxy(ClassUtils.getDefaultClassLoader());
}
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isDebugEnabled()) {
logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

findDefinedEqualsAndHashCodeMethods方法是为了查询被代理的接口是否包括equals和hashcode方法,这会影响到下面的调用。

可以看到InvocationHandler的实现就是this。我们看一下invoke方法的实现:

@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

关键代码

// Get the interception chain for this method.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

构建代理链,因为一个方法可能有多个切点匹配上,这个时候就需要构建一个链式的执行结构。

进入getInterceptorsAndDynamicInterceptionAdvice()方法

1567440444983

这里做了一个缓存,虽然new了一个对象作为key,但是对象的equals和hashcode方法都被重写了,所以没有问题,我们主要来看一下它是如何组装这个链式处理结构的:

进入getInterceptorsAndDynamicInterceptionAdvice()方法,紧接着发现是一个接口,那么查看他的实现类

6.DefaultAdvisorChainFactory 处理链式切点

1567440624426

可以看到,它会遍历自己的所有切点,那这些advisor是从哪里来的呢:

还记得最开始,我们说过,AbstractAutoProxyCreator中通过getAdvicesAndAdvisorsForBean方法来装载切面,而这个是一个抽象方法,现在来看它的实现,在AbstractAdvisorAutoProxyCreator中:

@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}
findCandidateAdvisors又是一个抽象方法,主要功能就是找到候选的切面,为什么是候选的,因为它是加载了所有的切面,有些切面并不需要,在最底层AnnotationAwareAspectJAutoProxyCreator的实现类中也有:
protected List<Advisor> findCandidateAdvisors() {
// Add all the Spring advisors found according to superclass rules.
List<Advisor> advisors = super.findCandidateAdvisors();
// Build Advisors for all AspectJ aspects in the bean factory.
if (this.aspectJAdvisorsBuilder != null) {
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}

可以看到,通过aspectJAdvisorsBuilder来将该类关心的所有的切面装载进来,并添加到父类的集合里面.aspectJAdvisorsBuilder里缓存了advisor的信息,拿到切面后,通过findAdvisorsThatCanApply方法来筛选合适的切面,之后对切面进行排序(如果实现了Order接口),然后返回切面的链表.

8.声明式事务@Transactional注解

1.前言

我们知道spring的事务管理分为两大部分:声明式和编程式,两种方式均为我们提供便捷的事务管理方法,各自优劣。

声明式事务

声明式的事务管理对业务代码基本0入侵,能够很好的把事务管理和业务代码剥离开来,提高代码扩展性和可读性但是控制的粒度只能是方法级别而且必须是public,同时还不能在一个类中调用等。

编程式事务

编程式事务则需要通过编写具体的事务代码来获得事务的管理能力,TransactionTemplate,或者直接使用PlatformTransactionManager,好处是控制粒度小,没有太多限制,坏处就是对业务代码有入侵,如果事务需要嵌套或者事务本身很繁琐,使用编程式则会十分麻烦。

这里讲述的是声明式事务,因为他比较常用。而且两种方式的源码其实是一样的。

2. 环境搭建

1.1导入相关依赖:数据源、数据库驱动、Spring-jdbc模块

1567417823342

1.2配置数据源、JdbcTemplate(Spring提供简化数据库操作的工具)操作数据

1567417832614

1.3新建PersonDao、PersonService

1567417847101

1567417852548

1.4 测试

1567417897324

插入成功

1.5 测试事务

修改Service方法,故意暴露一个异常

1567417910208

我们运行测试,发现还是插入成功,那么怎么阻止这种行为呢?添加事务

1.6 添加事务

  1. 给insertUser方法添加注解- @Transactional

1567417923873

  1. @EnableTransactionManagement 开启基于注解的事务管理功能

  2. 配置事务管理器来控制事务

1567417943208

事务添加成功

1.8 再次运行测试

发现插入失败,满足事务的原子性。

1.7 源码分析

声明式事务:
环境搭建:
1、导入相关依赖
数据源、数据库驱动、Spring-jdbc模块
2、配置数据源、JdbcTemplate(Spring提供的简化数据库操作的工具)操作数据
3、给方法上标注 @Transactional 表示当前方法是一个事务方法;
4、 @EnableTransactionManagement 开启基于注解的事务管理功能;
@EnableXXX
5、配置事务管理器来控制事务;
@Bean
public PlatformTransactionManager transactionManager()
原理:
1)、@EnableTransactionManagement
利用TransactionManagementConfigurationSelector给容器中会导入组件
导入两个组件
AutoProxyRegistrar
ProxyTransactionManagementConfiguration
2)、AutoProxyRegistrar:
给容器中注册一个 InfrastructureAdvisorAutoProxyCreator 组件;
InfrastructureAdvisorAutoProxyCreator:?
利用后置处理器机制在对象创建以后,包装对象,返回一个代理对象(增强器),代理对象执行方法利用拦截器链进行调用;
3)、ProxyTransactionManagementConfiguration 做了什么?
1、给容器中注册事务增强器;
1)、事务增强器要用事务注解的信息,AnnotationTransactionAttributeSource解析事务注解
2)、事务拦截器:
TransactionInterceptor;保存了事务属性信息,事务管理器;
他是一个 MethodInterceptor;
在目标方法执行的时候;
执行拦截器链;
事务拦截器:
1)、先获取事务相关的属性
2)、再获取PlatformTransactionManager,如果事先没有添加指定任何transactionmanger
最终会从容器中按照类型获取一个PlatformTransactionManager;
3)、执行目标方法
如果异常,获取到事务管理器,利用事务管理回滚操作;
如果正常,利用事务管理器,提交事务

3.源码深入分析

未完待续

9 总结

通过使用AOP和声明式事务,我们知道了一个套路,如果我们想使用某项功能,例如上面的aop和声明式事务、或者以后的springcloud的eureka、zull、feign等等功能,都遵循一下三点:

1.导入功能组件相关的依赖

2.在配置类开启组件(@EnableXXXX)

3.在关键位置标示使用的地方(例如@Aspect、@Transactional)

所以以后需要在spring中使用某个组件,一般都是遵循这样的思路

#

如果你感觉文章对你又些许感悟,你可以支持我!!
-------------本文结束感谢您的阅读-------------