开始约定编程——Spring AOP
一、AOP的概念
1. 为什么使用AOP
- 使用AOP可以处理一些无法使用OOP实现的业务逻辑,例如数据库事务管理。
- 我们可以将业务中一些通用的逻辑抽取出来,然后事先给予默认实现,那么当我们在真正处理业务逻辑时,只需要完成部分的功能就可以了,这样可以使得开发者的代码更加简短,同时提高了可维护性。以JDBC操作为例,关于数据库的打开和关闭以及事务的提交和回滚这些通用操作都有流程事先默认给你实现,你需要做的仅仅是编写SQL语句这一步而已,然后织入流程中即可。
2. AOP术语
-
连接点:指业务方法。AOP通过动态代理技术把它织入对应的流程中。
-
切面:指拦截器。
-
通知:拦截器中的方法被称为通知。通知可分为前置通知(before advice)、后置通知(after advice)、环绕通知(around advice)、事后返回通知(afterReturning advice)和异常通知(afterThrowing advice)
,它们会根据约定织入流程中,我们需要弄明白它们在流程中的顺序和运行的条件。
-
织入:生成代理对象,并组织流程的过程。
二、AOP开发详解
1. 确定连接点
对于任何AOP编程,首先要确定的是在什么地方需要AOP,也就是需要确定连接点(在Spring中就是什么类的什么方法)的问题。
现在我们假设有一个UserService接口,它有一个printUser方法:
// 定义一个用户服务接口
public interface UserService{
public void printUser(User user);
}
// 给出用户服务接口的一个实现类
@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. 开发切面
有了连接点,我们还需要一个切面,在切面中我们就可以定义各种通知了,然后Spring AOP会将这些通知织入约定的流程中。
// 创建一个切面类
// Spring是以@Aspect作为切面声明,当以@Aspect作为注解时,Spring就会知道这是一个切面
@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......");
}
// 返回通知
@AfterReturning("execution(*
com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(..))")
public void afterReturning(){
System.out.println("afterReturning......");
}
// 异常通知
@AfterThrowing("execution(*
com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(..))")
public void afterThrowing(){
System.out.println("afterThrowing......");
}
}
3. 切点定义
切点的作用就是向Spring描述哪些类的哪些方法需要启用AOP编程。有了切点的概念,我们便可以简化上面的代码,去除掉那些冗余的正则式。
@Aspect
public class MyAspect{
/*
* 表示任意返回类型的方法
com.springboot.chapter4.aspect.service.impl.UserServiceImpl 表示目标对象的全类名
printUser 指定目标对象的方法
(..) 表示可以匹配任意参数
*/
@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......");
}
@AfterReturning("pointCut()")
public void afterReturning(){
System.out.println("afterReturning......");
}
@AfterThrowing("pointCut()")
public void afterThrowing(){
System.out.println("afterThrowing......");
}
}
这样Spring就可以通过这个正则式知道你需要对类UserServiceImpl的printUser方法进行AOP编程,Spring就会将正则式匹配的方法和对应切面中的方法织入约定的流程当中,从而完成AOP编程。
4. 测试AOP
上面完成了连接点、切面和切点的定义之后,我们可以来测试AOP,为此需要先搭建一个Web开发环境,编写一个Controller。
@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);
return user;
}
}
然后我们编写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);
}
}
5. 环绕通知
环绕通知是所有通知中最为强大的通知,强大也意味着难以控制。一般而言,使用它的场景是在你需要大幅度修改原有目标对象的服务逻辑时,否则都尽量使用其他的通知。环绕通知是一个取代原有目标对象方法的通知,当然它也提供了回调原有目标对象的方法。
我们可以在上面的切面定义中加入环绕通知:
@Around("pointCut()")
public void around(ProceedingJoinPoint jp) throws Throwable{
System.out.println("around before......");
// 回调目标对象的原有方法
jp.proceed();
System.out.println("around after......");
}
这样我们就加入了一个环绕通知,并且在它之前和之后都加入了我们自己的打印内容,而它拥有一个ProceedingJoinPoint类型的参数。这个参数的对象有一个proceed方法,通过这个方法可以回调原有目标对象的方法。
6. 引入
指引入新的类和其方法,用来增强现有Bean的功能。
7. 通知获取参数
在上述的通知中,大部分我们都没有给通知传递参数。有时候我们希望能够传递参数给通知,这也是允许的。
例如,在前置通知中获取参数
// 将连接点(目标对象方法)中名称为user的参数传递进来
@Before("pointCut() && args(user)")
public void beforeParam(JoinPoint point, User user){
// 通过JoinPoint类型的参数point调用getArgs方法可以获取传递进来的参数user
Object[] args = point.getArgs();
System.out.println("before......");
}
三、多个切面
Spring可以支持多个切面的运行。在组织多个切面时,我们往往需要指定多个切面的顺序,这时我们可以使用注解@Order。例如,我们指定MyAspect1的顺序为1,就在该切面上添加注解@Order(1)
。