一、AOP之基础理论
1.1 什么是AOP
在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程(aop)。即将复杂的需求分解出不同方面,将散布在系统中的公共功能集中解决,在不改变原程序的基础上对代码段进行增强处理,增加新的功能。
1.2 AOP实现原理
AOP分为静态AOP和动态AOP。
静态AOP是指AspectJ实现的AOP,在编译期、类加载期织入。
动态AOP是指运行时生成代理对象来织入(Spring AOP的jdk动态代理和cglib代理)。
1.3 AOP相关术语
Aspect(切面)
aspect
由 pointcount
和 advice
组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义。
AOP的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作:
- 如何通过
pointcut
和advice
定位到特定的joinpoint
上 - 如何在 advice 中编写切面代码.
可以简单地认为, 使用 @Aspect
注解的类就是切面.
advice(增强/通知)
由 aspect
添加到特定的 join point
(即满足 point cut 规则的 join point) 的一段代码.
连接点(join point)
程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理.
切点(point cut)
Advice
是和特定的 point cut
关联的, 并且在 point cut
相匹配的 join point
中执行.
在 Spring 中, 所有的方法都可以认为是 joinpoint
, 但是我们并不希望在所有的方法上都添加 Advice
, 而 pointcut
的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint
, 给满足规则的 joinpoint
添加 Advice
.
关于join point 和 point cut 的区别
在 Spring AOP 中, 所有的方法执行都是 join point
. 而 point cut
是一个描述信息, 它修饰的是 join point
, 通过 point cut
, 我们就可以确定哪些 join point
可以被织入 Advice
. 因此 join point
和 point cut
本质上就是两个不同纬度上的东西.
advice
是在 join point
上执行的, 而 point cut
规定了哪些join point
可以执行哪些 advice
目标对象(Target)
织入 advice
的目标对象. 目标对象也被称为 advised object
.
织入(Weaving)
将 aspect
和其他对象连接起来, 并创建 adviced object
的过程.
根据不同的实现技术, AOP织入有三种方式:
- 编译器织入, 这要求有特殊的Java编译器.
- 类装载期织入, 这需要有特殊的类装载器.
- 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式.
Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.
1.4 advice 的类型
before advice
,在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常
after return advice
, 在一个 join point 正常返回后执行的 advice
after throwing advice
, 当一个 join point 抛出异常后执行的 advice
after(final) advice
, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
around advice
, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.
二、@AspectJ 支持
2.1 定义 aspect(切面)
@Component
@Aspect
public class MyTest {
}
注意, 仅仅使用@Aspect 注解, 并不能将一个 Java 对象转换为 Bean, 因此我们还需要使用类似 @Component 之类的注解。
注意, 如果一个 类被@Aspect 标注, 则这个类就不能是其他 aspect 的 **advised object** 了, 因为使用 @Aspect 后, 这个类就会被排除在 auto-proxying 机制之外
.
2.2 声明 pointcut
一个 pointcut
的声明由两部分组成:
- 一个方法签名, 包括方法名和相关参数
- 一个
pointcut
表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice).
在@AspectJ 风格的 AOP 中, 我们使用一个方法来描述 pointcut
, 即:
@Pointcut("execution(* com.xys.service.UserService.*(..))") // 切点表达式
private void dataAccessOperation() {
} // 切点前面
这个方法必须无返回值.
上面我们简单地定义了一个pointcut
, 这个pointcut
所描述的是: 匹配所有在包 com.xys.service.UserService
下的所有方法的执行.
2.3 切点标志符(designator)
AspectJ5 的切点表达式由标志符(designator)
和操作参数
组成. 如 “execution( greetTo(…))” 的切点表达式, execution
就是 标志符, 而圆括号里的 greetTo(..)
就是操作参数
-
execution
匹配join point
的执行, 例如"execution(* hello(..))"
表示匹配所有目标类中的 hello() 方法. 这个是最基本的pointcut
标志符. -
within
匹配特定包下
的所有 join point, 例如within(com.xys.*)
表示com.xys
包中的所有连接点, 即包中的所有类的所有方法. 而within(com.xys.service.*Service)
表示在com.xys.service
包中所有以Service
结尾的类的所有的连接点. -
bean
匹配bean 名字为指定值的 bean
下的所有方法, 例如:
bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法
bean(myService) // 匹配名字为 myService 的 bean 下的所有方法
- args
匹配参数满足要求
的的方法.
@Pointcut("within(com.xys.demo2.*)")
public void pointcut2() {
}
@Before(value = "pointcut2() && args(name)")
public void doSomething(String name) {
logger.info("---page: {}---", name);
}
@Service
public class NormalService {
private Logger logger = LoggerFactory.getLogger(getClass());
public void someMethod() {
logger.info("---NormalService: someMethod invoked---");
}
public String test(String name) {
logger.info("---NormalService: test invoked---");
return "服务一切正常";
}
}
当 NormalService.test
执行时, 则 advice doSomething
就会执行,test 方法
的参数 name
就会传递到 doSomething
中.
- @annotation
匹配由指定注解所标注的方法
, 例如:
@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {
}
则匹配由注解 AuthChecker
所标注的方法.
三、SpringBoot中AOP实战
3.1使用aop来完成全局请求日志处理
3.1.1 导入依赖
3.1.2 创建一个 Controller
@RestController
public class FirstController {
@RequestMapping("/first")
public Object first() {
return "first controller";
}
@RequestMapping("/doError")
public Object error() {
return 1 / 0;
}
}
3.1.3 创建一个aspect切面类
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(public * com.example.controller.*.*(..))")
public void webLog(){}
@Before("webLog()")
public void deBefore(JoinPoint joinPoint) throws Throwable {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
System.out.println("URL : " + request.getRequestURL().toString());
System.out.println("HTTP_METHOD : " + request.getMethod());
System.out.println("IP : " + request.getRemoteAddr());
System.out.println("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
System.out.println("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
System.out.println("方法的返回值 : " + ret);
}
//后置异常通知
@AfterThrowing("webLog()")
public void throwss(JoinPoint jp){
System.out.println("方法异常时执行.....");
}
//后置最终通知,final增强,不管是抛出异常或者正常退出都会执行
@After("webLog()")
public void after(JoinPoint jp){
System.out.println("方法最后执行.....");
}
//环绕通知,环绕增强,相当于MethodInterceptor
@Around("webLog()")
public Object arround(ProceedingJoinPoint pjp) {
System.out.println("方法环绕start.....");
try {
Object o = pjp.proceed();
System.out.println("方法环绕proceed,结果是 :" + o);
return o;
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}
3.1.4 启动项目
访问http://localhost:8080/first,看控制台结果:
方法环绕start.....
URL : http://localhost:8080/first
HTTP_METHOD : GET
IP : 0:0:0:0:0:0:0:1
CLASS_METHOD : com.example.controller.FirstController.first
ARGS : []
方法环绕proceed,结果是 :first controller
方法最后执行.....
方法的返回值 : first controller
模拟出现异常时的情况,访问http://localhost:8080/doError,看控制台结果:
方法环绕start.....
URL : http://localhost:8080/doError
HTTP_METHOD : GET
IP : 0:0:0:0:0:0:0:1
CLASS_METHOD : com.example.controller.FirstController.error
ARGS : []
java.lang.ArithmeticException: / by zero
....
方法最后执行.....
方法的返回值 : null
3.2 Http接口鉴权
设计思路:
- 提供一个特殊的注解
AuthChecker
, 这个是一个方法注解, 有此注解所标注的Controller
需要进行调用方权限的认证. - 利用 AOP, 以
@annotation
切点标志符来匹配有注解AuthChecker
所标注的joinpoint
. - 在
advice
中, 简单地检查调用者请求中的Cookie
中是否有我们指定的token
, 如果有, 则认为此调用者权限合法,允许调用, 反之权限不合法, 范围错误.
3.2.1 导入依赖
3.2.2 创建注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthChecker {
}
3.2.3 切面类
@Component
@Aspect
public class HttpAopAdviseDefine {
// 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {
}
// 定义 advise
@Around("pointcut()")
public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
// 检查用户所传递的 token 是否合法
String token = getUserToken(request);
if (!token.equalsIgnoreCase("123456")) {
return "错误, 权限不合法!";
}
return joinPoint.proceed();
}
private String getUserToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return "";
}
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase("user_token")) {
return cookie.getValue();
}
}
return "";
}
}
3.2.4 创建Http接口进行模拟(Controller)
@RestController
public class DemoController {
@RequestMapping("/aop/http/alive")
public String alive() {
return "服务一切正常";
}
@AuthChecker
@RequestMapping("/aop/http/user_info")
public String callSomeInterface() {
return "调用了 user_info 接口.";
}
}
3.3 方法耗时统计
设计:
首先我们可以使用 around advice
, 然后在方法调用前, 记录一下开始时间, 然后在方法调用结束后, 记录结束时间, 它们的时间差就是方法的调用耗时.
切面类:
@Component
@Aspect
public class ExpiredAopAdviseDefine {
private Logger logger = LoggerFactory.getLogger(getClass());
// 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
@Pointcut("within(SomeService)")
public void pointcut() {
}
// 定义 advise
// 定义 advise
@Around("pointcut()")
public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 开始
Object retVal = pjp.proceed();
stopWatch.stop();
// 结束
// 上报到公司监控平台
reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());
return retVal;
}
public void reportToMonitorSystem(String methodName, long expiredTime) {
logger.info("---method {} invoked, expired time: {} ms---", methodName, expiredTime);
//
}
}
借鉴:
https://www.cnblogs.com/bigben0123/p/7779357.html
https://segmentfault.com/a/1190000007469982
https://segmentfault.com/a/1190000007469968