一、AOP的概念
Spring AOP:一种约定流程的编程,即按照一定的规则,将代码织入事先约定的流程中。可以简化代码,最为典型的应用实际就是数据库事务的管控。
想像一下一个sql的执行过程:
•打开数据库连接,对其属性进行设置;
•执行SQL语句;
•如果没有异常,则提交事务;
•如果发生异常,则回滚事务;
•关闭数据库事务连接。
获取数据库事务连接、事务操控和关闭数据库连接的过程,都需要使用大量的try … catch … finally…语句去操作,而这些工作都可以通过AOP简化。
AOP可以实现业务方法织入,而数据库连接的打开和关闭以及事务管理都由它来默认实现,也就是它可以将大量重复的流程通过约定的方式抽取出来,然后给予默认的实现。
Spring现在只需要使用一个注解@Transactional,就可以实现上述的情景。
二、AOP 术语和流程
•连接点(join point):对应的是具体被拦截的对象,因为Spring只能支持方法,所以被拦截的对象往往就是指特定的方法,例如,前面的HelloServiceImpl的sayHello方法就是一个连接点,AOP将通过动态代理技术把它织入对应的流程中。
•切点(point cut):有时候,我们的切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点。
•通知(advice):按照约定的流程下的方法,分为前置通知(before advice)、后置通知(after advice)、环绕通知(around advice)、事后返回通知(afterReturning advice)和异常通知(afterThrowing advice),它会根据约定织入流程中,需要弄明白它们在流程中的顺序和运行的条件。
•目标对象(target):即被代理对象,例如,约定编程中的HelloServiceImpl实例就是一个目标对象,它被代理了。
•引入(introduction):是指引入新的类和其方法,增强现有Bean的功能。
•织入(weaving):它是一个通过动态代理技术,为原有服务对象生成代理对象,然后与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。
•切面(aspect):是一个可以定义切点、各类通知和引入的内容,Spring AOP将通过它的信息来增强Bean的功能或者将对应的方法织入流程。
Spring AOP流程约定
三、AOP开发详解
采用**@AspectJ**的注解方式讨论AOP的开发。因为Spring AOP只能对方法进行拦截,所以首先要确定需要拦截什么方法,让它能织入约定的流程中。
1.确定连接点
在Spring中就是什么类的什么方法需要AOP。
UserService接口:用户服务接口
public interface UserService {
public void printUser(User user);
}
UserServiceImpl类:用户服务接口实现类
@Service
public class UserServiceImpl implements UserService {
@Override
public void printUser(User user) {
if (user == null) {
throw new RuntimeException("检查用户参数是否为空......");
}
System.out.print("id =" + user.getId());
System.out.print("\tusername =" + user.getUsername());
System.out.println("\tnote =" + user.getNote());
}
}
下面我们将以printUser方法为连接点,进行AOP编程。
2.开发切面
有了连接点,我们还需要一个切面,通过它可以描述AOP其他的信息,用以描述流程的织入。
MyAspect类:
@Aspect
public class MyAspect {
@Before("execution(*
com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(..))")
public void before() {
System.out.println("before ......");
}
@After("execution(*
com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(..))")
public void after() {
System.out.println("after ......");
}
.......
}
注意Spring是以@Aspect作为切面声明的,当以@Aspect作为注解时,Spring就会知道这是一个切面,然后我们就可以通过各类注解来定义各类的通知了。即代码当中的@Before、@After、@AfterReturning和@AfterThrowing等几个注解,它们是定义流程中的方法,由Spring AOP将其织入约定的流程中。
3.切点定义
在上述切面的定义中,我们看到了@Before、@After、@AfterReturning和@AfterThrowing等注解,它们还会定义一个正则式,这个正则式的作用就是定义什么时候启用AOP,毕竟不是所有的功能都是需要启用AOP的,
也就是Spring会通过这个正则式去匹配、去确定对应的方法(连接点)是否启用切面编程,Spring定义了切点(Pointcut)的概念,切点的作用就是向Spring描述哪些类的哪些方法需要启用AOP编程。
有了切点的概念,就可以把冗余的正则式定义排除在外。
定义切点pointCut()方法:
@Aspect
public class MyAspect {
@Pointcut("execution(*
com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(..))")
public void pointCut() {
}
@Before("pointCut()")
public void before() {
System.out.println("before ......");
}
@After("pointCut()")
public void after() {
System.out.println("after......");
}
......
}
代码中,使用了注解@Pointcut来定义切点,它标注在方法pointCut上,则在后面的通知注解中就可以使用方法名称来定义了。
对这个正则式做进一步的分析:
execution(* com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(..))
其中:
•execution表示在执行的时候,拦截里面的正则匹配的方法;
•*表示任意返回类型的方法;
•com.springboot.chapter4.aspect.service.impl.UserServiceImpl 指定目标对象的全限定名称;
•printUser指定目标对象的方法;
•(…)表示任意参数进行匹配。
这样Spring就可以通过这个正则式知道要对类UserServiceImpl的printUser方法进行AOP增强,它就会将正则式匹配的方法和对应切面的方法织入约定流程当中,从而完成AOP编程。
4.测试AOP
进行测试AOP,为此需要先搭建一个Web开发环境,开发一个用户控制器(UserController)
定义用户控制器:UserController
// 定义控制器
@Controller
// 定义类请求路径
@RequestMapping("/user")
public class UserController {
// 注入用户服务
@Autowired
private UserService userService = null;
// 定义请求
@RequestMapping("/print")
// 转换为JSON
@ResponseBody
public User printUser(Long id, String userName, String note) {
User user = new User();
user.setId(id);
user.setUsername(userName);
user.setNote(note);
userService.printUser(user);// 若user=null,则执行afterthrowing方法
return user;// 加入断点
}
}
这里,通过自动注入UserService服务接口,然后使用它进行用户信息打印,因为方法标注了@ResponseBody,所以最后Spring MVC会将其转换为JSON响应请求。
这里的UserService的实现类满足切点的定义,因此Spring AOP会将其织入对应的流程中。
5.配置Spring Boot的启动文件:
// 指定扫描包
@SpringBootApplication(scanBasePackages = {"com.springboot.chapter4.aspect"})
public class Chapter4Application {
// 定义切面
@Bean(name="myAspect")
public MyAspect initMyAspect() {
return new MyAspect();
}
// 启动切面
public static void main(String[] args) {
SpringApplication.run(Chapter4Application.class, args);
}
}
运行这段代码,打开浏览器,等待服务器启动完成后,打开浏览器,最后在地址栏输入请求地址http://localhost:8080/user/print?id=1&userName= user_name_1¬e=2323,
调试一下,可以看到userService对象,实际上是一个JDK动态代理对象,它代理了目标对象UserServiceImpl,Spring将它织入AOP的流程中。
与此同时,也可以看到后台打出的日志:
before ......
id =1 username =user_name_1 note =2323
after ......
afterReturning ......
这就是Spring与我们约定的流程,即Spring已经通过动态代理技术把我们所定义的切面和服务方法织入约定的流程中了。
如果我们把控制器(UserController)中的user对象设置为null,那么它将抛出异常,这个时候将会执行异常通知(afterThrowing)而不会再执行返回通知(afterReturning),
但是无论如何它都会执行after方法。下面是设置用户对象为空时进行打印得到的测试日志:
before ......
after ......
afterThrowing ......
......
java.lang.RuntimeException: 检查用户参数是否为空......] with root cause
java.lang.RuntimeException: 检查用户参数是否为空......
......
无论是否发生异常,后置通知(after)都会被流程执行;而因为发生了异常,所以按照约定,异常通知(afterThrowing)会被触发,返回通知(afterReturning)则不会被触发。这都是Spring AOP与我们约定的流程。
6.环绕通知
环绕通知(Around)是所有通知中最为强大的通知,也最难控制。一般而言,使用它的场景是在你需要大幅度修改原有目标对象的服务逻辑时,否则都尽量使用其他的通知。环绕通知是一个取代原有目标对象方法的通知,当然它也提供了回调原有目标对象方法的能力。
加入环绕通知
@Around("pointCut()")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("around before......");
// 回调目标对象的原有方法
jp.proceed();
System.out.println("around after......");
}
它拥有一个ProceedingJoinPoint类型的参数。这个参数的对象有一个proceed方法,通过这个方法可以回调原有目标对象的方法。
进行调试,可以看到对于环绕通知的参数(jp),它是一个被Spring封装过的对象,但是我们可以明显地看到它里面的属性,带有原有目标对象的信息,这样就可以通过它的proceed方法回调原有目标对象的方法。
测试的日志如下:
around before......
before ......
id =1 username =user_name_1 note =2323
around after......
after ......
afterReturning ......
注意,这个结果是真实测试的结果,但却不是我期待的结果,因为执行顺序错乱了,所以要慎用。
7.引入(不太重要)
引入一个用户检测的接口UserValidator来对User做校验,以增强UserService接囗的功能:
用户检测的接口UserValidator:
public interface UserValidator {
// 检测用户对象是否为空
public boolean validate(User user);
}
UserValidator的实现类
package com.springboot.chapter4.aspect.validator.impl;
/**** imports ****/
public class UserValidatorImpl implements UserValidator {
@Override
public boolean validate(User user) {
System.out.println(“引入新的接口:” + UserValidator.class.getSimpleName());
return user != null;
}
}
引入新的接口:通过Spring AOP引入的定义就能够增强UserService接口的功能
@Aspect
public class MyAspect {
@DeclareParents(
value= "com.springboot.chapter4.aspect.service.impl.UserServiceImpl+",
defaultImpl=UserValidatorImpl.class)
public UserValidator userValidator;
......
}
注解@DeclareParents,它的作用是引入新的类来增强服务,它有两个必须配置的属性value和defaultImpl:
•value:指向你要增强功能的目标对象,这里是要增强UserServiceImpl对象,因此可以看到配置为com.springboot.chapter4.aspect.service.impl.UserServiceImpl+。
•defaultImpl:引入增强功能的类,这里配置为UserValidatorImpl,用来提供校验用户是否为空的功能。
测试引入的验证器
// 定义请求
@RequestMapping("/vp")
// 返回JSON
@ResponseBody
public User validateAndPrint(Long id, String userName, String note) {
User user = new User();
user.setId(id);
user.setUsername(userName);
user.setNote(note);
// 强制转换
UserValidator userValidator = (UserValidator)userService;
// 验证用户是否为空
if (userValidator.validate(user)) {
userService.printUser(user);
}
return user;
}
把原来的userService对象强制转换为UserValidator对象,然后就可以使用验证方法去验证用户对象是否为空。测试结果:
引入新的接口:UserValidator
around before......
before ......
id =1 username =user_name_1 note =2323
around after......
after ......
afterReturning ......
验证器是根据什么原理来增强原有对象功能的呢?可以看下生成代理对象的代码:
Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
proxyBean);
这里的newProxyInstance的第二个参数为一个对象数组,也就是说这里生产代理对象时,Spring会把UserService和UserValidator两个接口传递进去,让代理对象下挂到这两个接口下,这样这个代理对象就能够相互转换并且使用它们的方法了。
在JDK动态代理中下挂的两个接口,于是我们可以将这个代理对象通过这两个接口相互转换,然后调度其对应的方法,这就是引入的原理。同样地,CGLIB也可以做到类似的功能。
8.通知获取参数
在切点处加入对应的正则式就可以传递参数给通知。对于非环绕通知还可以使用一个连接点(JoinPoint)类型的参数。
在前置通知中获取参数
@Before("pointCut() && args(user)")
public void beforeParam(JoinPoint point, User user) {
Object[] args = point.getArgs();
System.out.println("before ......");
}
正则式pointCut() &&args(user)中,pointCut()表示启用原来定义切点的规则,并且约定将连接点(目标对象方法)名称为user的参数传递进来。
这里要注意,JoinPoint类型的参数对于非环绕通知而言,Spring AOP会自动地把它传递到通知中;
对于环绕通知而言,可以使用ProceedingJoinPoint类型的参数。使用它将允许进行目标对象的回调
9.织入
织入是一个生成动态代理对象并且将切面和目标对象方法编织成为约定流程的过程。
上面我们都是采用接口+实现类的模式,这是Spring推荐的方式,但是对于是否拥有接口则不是Spring AOP的强制要求,对于动态代理的也有多种实现方式,之前谈到的JDK只是其中的一种,业界比较流行的还有CGLIB、Javassist、ASM等。
Spring采用了JDK和CGLIB,对于JDK而言,它是要求被代理的目标对象必须拥有接口,而对于CGLIB则不做要求。因此在默认的情况下,Spring会按照这样的一条规则处理,即当你需要使用AOP的类拥有接口时,它会以JDK动态代理运行,否则以CGLIB运行。
不使用接口
@Service
public class UserServiceImpl{
public void printUser(User user) {
if (user == null) {
throw new RuntimeException("检查用户参数是否为空......");
}
System.out.print("id =" + user.getId());
System.out.print("\tusername =" + user.getUsername());
System.out.println("\tnote =" + user.getNote());
}
}
然后修改控制器的依赖注入,直接依赖于不存在接口的实现类
// 定义控制器
@Controller
// 定义类请求路径
@RequestMapping("/user")
public class UserController {
// 使用非接口注入
@Autowired
private UserServiceImpl userService = null;
// 定义请求
@RequestMapping("/print")
// 返回JSON
@ResponseBody
public User printUser(Long id, String userName, String note) {
User user = new User();
user.setId(id);
user.setUsername(userName);
user.setNote(note);
userService.printUser(user);
return user;// 加入断点测试
}
......
}
然后断点,可以看到此时Spring已经使用了CGLIB为我们生成代理对象,从而将切面的内容织入对应的流程中。
10.多切面的情况
Spring提供了一个注解@Order和一个接口Ordered,我们可以使用它们的任意一个指定切面的顺序。