本文分上下两部分,上部为AOP切面技术基础,学习Spring-boot-AOP切面技术。
下部为实践,利用AOP切面技术为主流RESTFul-API接口打造一个统一的访问日志。
有这样一个场景,某公司有一个主数据管理系统目前已经上线运行,但是系统运行不稳定,有时候运行很慢,为了检测到底是哪里出了问题,开发人员需要监控每一个方法的执行时间,再判断问题所在。当问题解决后,还需要把监控移除掉。因为系统已经上线运行,如果手动修改程序方法,那么工作量会非常大,而且这些监控方法以后还要移除掉,费时费力。如果能够在系统运行时动态添加代码,就能很好的解决这个需求。目标:不修改源代码,也能实现监控输出,这时AOP切面编程就是最佳选择。
在系统运行时动态添加代码的方式称为“面向切面编程AOP”。它有非常多的应用场景:
A:系统监控,执行情况分析,性能统计。
B:设置日志,记录登录、执行、参数等信息。
C:拦截,通过AOP快速实现精细化拦截,安全控制。
D:事务处理、统一异常处理。
3.1添加Maven依赖
org.springframework.boot
spring-boot-starter-aop
项目结构如下图所示:
3.2创建一个UserService服务类,一个UserController接口实现类用于调用UserService服务类。
我们在com.example.demohelloworld.AOP.Service包下创建UserService服务类,代码如下:
package com.example.demohelloworld.AOP.Service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
//定义一个简单的查询方法
public String getUserById(Integer id){
System.out.println("get>>>"+id);
return "getuser";
}
//定义一个简单的删除方法
public void deleteUserById(Integer id){
System.out.println("delete>>>");
}
}
我们在com.example.demohelloworld.AOP包下创建UserController接口类,代码如下:
package com.example.demohelloworld.AOP;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
//用于调用UserService和前台执行测试
@RestController
public class UserController {
@Autowired
UserService userservice;
@GetMapping("/getuser/{id}")
public String getUserById(@PathVariable Integer id){
return userservice.getUserById(id);
}
@GetMapping("/deleteuser")
public void deleteUserById(Integer id){
userservice.deleteUserById(id);
}
}
3.3接下来创建AOP切面类AspectLog
我们在com.example.demohelloworld.AOP包下创建AspectLog切面类,代码如下:
package com.example.demohelloworld.AOP;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
//@Aspect注解表明这是一个切面类
@Aspect
public class AspectLog {
//定义切入点,execution 中的第一个*表示方法返回任意值,第二个*表示AOP包下的任意类,第三个*表示类中的任意方法,括号中的两个点表示方法参数任意值
@Pointcut("execution(* com.example.demohelloworld.AOP.Service.*.*(..))")
public void pointcutvoid(){
}
//@Before注解表示这是一个前置通知,该方法在目标方法执行之前执行。通过JoinPoint参数可以获取目标方法的方法名、修饰符等信息
@Before(value = "pointcutvoid()")
public void before(JoinPoint joinPoint){
//获取传入参数
Object[] args =joinPoint.getArgs();
System.out.println(joinPoint.getSignature().getName()+"方法开始执行...传入参数为:"+Arrays.deepToString(args));
}
//@After注解表示这是一个后直通知,该方法在目标方法执行之后执行
@After(value = "pointcutvoid()")
public void after(JoinPoint joinPoint){
System.out.println(joinPoint.getSignature().getName()+"方法执行结束...");
}
//@AfterReturning注解表示这是一个返回通知,在该方法中可以获取目标方法的返回值,returning参数是指返回值的变量名,对应方法的参数。在方法参数中定义了result的类型为Object,表示目标方法的返回值可以是任意类型,若result参数的类型为Long,则该方法只能处理目标方法返回值为Long的情况
@AfterReturning(value = "pointcutvoid()",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result){
System.out.println(joinPoint.getSignature().getName()+"方法返回值为:"+result);
}
//@AfterThrowing注解表示这是一个异常通知,即当目标方法发生异常时,该方法会被调用,异常类型为Exception表示所有异常都会进入该方法中执行,若异常类型为ArithmeticException,则表示只有目标方法抛出的ArithmeticException异常才会进入该方法中处理
@AfterThrowing(value = "pointcutvoid()",throwing = "ex")
public void afterThrowing(JoinPoint joinPoint,Exception ex){
System.out.println(joinPoint.getSignature().getDeclaringTypeName()+joinPoint.getSignature().getName()+"方法异常了,异常是:"+ex.getMessage());
}
//@Around注解表示这是一个环绕通知,绕通知是所有通知里功能最为强大的通知,可以实现前置通知、后置通知、异常通知以及返回通知的功能。目标方法进入环绕通知后,通过调用ProceedingJoinPoint对象的proceed方法使目标方法继续执行,开发者可以在此修改目标方法的执行参数、返回值等,并且可以在此处理目标方法的异常
@Around("pointcutvoid()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
return proceedingJoinPoint.proceed();
}
}
3.3.1 @Aspect注解表明这是一个切面类。
3.3.2 pointcutvoid()方法使用了@Pointcut注解,表明这是一个切入点。
本文中切入点为@Pointcut("execution(* com.example.demohelloworld.AOP.Service.*.*(..))")
execution 中的第一个*表示方法返回任意值,第二个*表示Service包下的任意类,第三个*表示类中的任意方法,括号中的两个点表示方法参数任意值。
我们可以根据需要,从包、类、方法三个层次去定义所要进行切面的内容。
3.3.3 我们还需要先了解一下切面注解的含义:
切面注解 | 注解含义 | 详细含义 |
---|---|---|
@Before | 前置 | 表示这是一个前置通知,该方法在目标方法执行之前执行。通过JoinPoint参数可以获取目标方法的方法名、修饰符等信息 |
@AfterThrowing | 异常抛出 | 表示这是一个异常通知,即当目标方法发生异常时,该方法会被调用,异常类型为Exception表示所有异常都会进入该方法中执行,若异常类型为ArithmeticException,则表示只有目标方法抛出的ArithmeticException异常才会进入该方法中处理 |
@After | 后置 | 表示这是一个后直通知,该方法在目标方法执行之后执行 |
@AfterReturning | 后置增强,执行顺序在@After之后 | 表示这是一个返回通知,在该方法中可以获取目标方法的返回值,returning参数是指返回值的变量名,对应方法的参数。在方法参数中定义了result的类型为Object,表示目标方法的返回值可以是任意类型,若result参数的类型为Long,则该方法只能处理目标方法返回值为Long的情况 |
@Around | 环绕 | 表示这是一个环绕通知,绕通知是所有通知里功能最为强大的通知,可以实现前置通知、后置通知、异常通知以及返回通知的功能。目标方法进入环绕通知后,通过调用ProceedingJoinPoint对象的proceed方法使目标方法继续执行,开发者可以在此修改目标方法的执行参数、返回值等,并且可以在此处理目标方法异常 |
3.3.4 连接点JoinPoint对象封装了AOP切面方法的执行信息(这个就是我们需要提炼的信息,可以作为监控、日志等),所以我们还需了解一下连接点JoinPoint中getSignature()常用方法的含义:
方法 | 方法含义 |
---|---|
joinPoint.getSignature().getDeclaringTypeName() | 获取被切面类的路径及名称,例如在本文中为:com.example.demohelloworld.AOP.UserService |
joinPoint.getSignature().getName() | 获取被切面类中方法的名称,例如在本文中为:getUserById或者deleteUserById |
joinPoint.getArgs() | 获取传入目标方法的参数对象 |
joinPoint.getTarget() | 获取被代理的对象 |
3.4启动项目开始测试切面效果
打开浏览器输入http://localhost:8080/getuser/1,对getUserById进行AOP切面,后台效果如下:
输入http://localhost:8080/deleteuser,对deleteUserById进行AOP切面,后台效果如下:
AOP面向切面编程,指扩展功能且不修改源代码,将功能代码从业务逻辑代码中分离出来。进而实现日志记录,性能统计,安全控制,事务处理,异常处理等功能。
切面技术的应用大大提升了程序的可扩展性、安全性、操作便捷性,且对开发者非常友好,使用较少的代码就能完成监控、日志、拦截等功能,大大提升开发效率。
下一讲,我将通过一个实际案例:利用AOP切面技术为RESTFul-API接口打造一个统一的登录日志而不修改任何原接口代码,让大家进一步了解AOP切面技术的优势。