1 AOP前言
散布于应用中多处的功能(日志、安全、事务管理等)被称为横切关注点。
把横切关注点与业务逻辑分离是AOP要解决的问题。
1.1 编程范式
面向过程编程(Normal普遍)
面向对象编程(Java OOP)
函数式编程(Java JDK8)
事件驱动编程(Java GUI、或者Anroid手机编程)
面向切面编程(Spring AOP)
1.2 AOP的初衷
DRY:Don't Repeat Yourself
Soc:Separation of Concerns
-水平分离:展示层->服务层->持久层
-垂直分离:模块划分(订单、库存)
-切面分离:分离功能性需求与非功能性需求
1.3 AOP的应用场景
权限控制(@PreAuthorize)
缓存控制(@Cacheable)
事务控制(@Transactional)
审计日志
性能监控
分布式跟踪
异常处理
1.4 AOP简介
- AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统 OOP(Object-Oriented Programming, 面向对象编程) 的补充.
- AOP 的主要编程对象是切面(aspect), 而切面模块化横切关注点(即对象里放入的是一个个横切关注点的方法).
- 在应用 AOP 编程时, 仍然需要定义公共功能, 但可以明确的定义这个功能在哪里, 以什么方式应用, 并且不必修改受影响的类. 这样一来横切关注点就被模块化到特殊的对象(切面)里.
-
AOP 的好处: 每个事物逻辑位于一个位置, 代码不分散, 便于维护和升级 业务模块更简洁, 只包含核心业务代码.
-
-
这里的一个个需求,例如验证参数、日志功能等都是横切关注点,我们将横切关注点抽取出来
- 通过切面和业务逻辑的结合实现目标功能,即为面向切面编程。
1.5 AOP术语
- 切面(Aspect): 横切关注点(一个个具体需求)(跨越应用程序多个模块的功能)被模块化的特殊对象
- 通知(Advice): 切面必须要完成的工作(比如说切面需要完成验证,即切面中的每一个方法)
- 目标(Target): 被通知的对象(即业务逻辑)
- 代理(Proxy): 向目标对象应用通知之后创建的对象(将切面和目标混合)
- 连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为 ArithmethicCalculator#add(); 方位为该方法执行前的位置
- 切点(pointcut):每个类都拥有多个连接点:例如 ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
2 AOP注解应用
2.1 @Aspect
2.2 @PointCut
2.2.1 切面表达式Pointcut expression
1)designators指示器:
- 匹配方法:execution(
modifier-pattern?
ret-type-pattern
declaring-type-pattern?
name-pattern(param-pattern)
throws-pattern?
) 必会,问号代表可无
- 匹配注解:@target()/@args()/@within()/@annotation() 了解
- 匹配包、类型:within() 了解
- 匹配对象:this()/bean()/target() 了解
- 匹配参数:args() 必会
2)Wildcards通配符:
- * 匹配任意数量的字符
- .. 匹配任意数的子包或参数
- + 匹配指定类及其子类
3)operators操作符:
- && 与
- || 或
- !非
例子:
匹配包、类型(了解)
//匹配ProductService类里头的所有方法 @Pointcut("within(com.imooc.service.ProductService)") //匹配com.imooc包及子包下所有类的方法 @Pointcut("within(com.imooc..*)")
匹配对象(了解)
//匹配AOP对象的目标对象为指定类型的方法,即LogService的aop代理对象的方法 @Pointcut("this(com.imooc.log.Loggable)") //匹配实现Loggable接口的目标对象(而不是aop代理后的对象)的方法 @Pointcut("target(com.imooc.log.Loggable)") //this 可以拦截 DeclareParents(Introduction) //target 不拦截 DeclareParents(Introduction) //匹配所有以Service结尾的bean里头的方法 @Pointcut("bean(*Service)")
匹配注解(了解),终于知道@Transactional是怎样通过AOP管理事务的
//匹配方法标注有AdminOnly的注解的方法 @Pointcut("@annotation(com.imooc.anno.AdminOnly) && within(com.imooc..*)") //匹配标注有NeedSecured的类底下的方法 //class级别 @Pointcut("@within(com.imooc.anno.NeedSecured) && within(com.imooc..*)") //匹配标注有NeedSecured的类及其子类的方法 //runtime级别 //在spring context的环境下,二者没有区别 @Pointcut("@target(com.imooc.anno.NeedSecured) && within(com.imooc..*)") //匹配传入的参数类标注有Repository注解的方法 @Pointcut("@args(com.imooc.anno.NeedSecured) && within(com.imooc..*)")
匹配参数(必会)
//匹配任何以find开头而且只有一个Long参数的方法 @Pointcut("execution(* *..find*(Long))") //匹配任何以find开头的而且第一个参数为Long型的方法 @Pointcut("execution(* *..find*(Long,..))") //匹配任何只有一个Long参数的方法 @Pointcut("within(com.imooc..*) && args(Long)") //匹配第一个参数为Long型的方法 @Pointcut("within(com.imooc..*) && args(Long,..)")
匹配方法(必会)
//匹配任何公共方法 @Pointcut("execution(public * com.imooc.service.*.*(..))") //匹配com.imooc包及子包下Service类中无参方法 @Pointcut("execution(* com.imooc..*Service.*())") //匹配com.imooc包及子包下Service类中的任何只有一个参数的方法 @Pointcut("execution(* com.imooc..*Service.*(*))") //匹配com.imooc包及子包下任何类的任何方法 @Pointcut("execution(* com.imooc..*.*(..))") //匹配com.imooc包及子包下返回值为String的任何方法 @Pointcut("execution(String com.imooc..*.*(..))") //匹配异常 execution(public * com.imooc.service.*.*(..) throws java.lang.IllegalAccessException)
2.2.2 通知Advice 参数与结果绑定
@Before前置通知
@After后置通知,方法执行完之后
@AfterThrowing异常通知,抛出异常之后
@AfterReturning返回通知,成功执行之后
@Around 环绕通知,一定要有返回值Object,否则程序返回值为null
package com.example.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ControllerAspect {
@Pointcut("execution(String com.example.demo.service.*.*(String))")
public void pointcut(){}
@Before("pointcut() && args(para)")
public void before(String para){
System.out.println("before:"+para);
}
@After("pointcut() && args(para)")
public void after(String para){
System.out.println("after:"+para);
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
Object obj = null;
try {
obj = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
String methodName = proceedingJoinPoint.getSignature().getName();
System.out.println("methodName:"+methodName);
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("around:"+args);
return obj;
}
@AfterReturning(value="pointcut()",returning="ret")
public void afterReturning(String ret){
System.out.println("afterReturning:"+ret);
}
@AfterThrowing(value="pointcut()",throwing="e")
public void afterThrowing(Exception e){
System.out.println("afterThrowing:"+e.getMessage());
}
}
2.2.3 Restful记录日志与异常处理
package com.hrhx.springboot.aop;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.hrhx.springboot.exception.CheckException;
import com.hrhx.springboot.jopo.ResultBean;
import com.hrhx.springboot.util.JsonUtil;
@Aspect
@Component
public class ControllerAOP {
//private final Logger logger = Logger.getLogger(getClass());
private static final Logger logger = LoggerFactory.getLogger(ControllerAOP.class);
@Pointcut("execution(public com.hrhx.springboot.jopo.ResultBean *(..))")
public void webLog(){}
@Around("webLog()")
public Object handlerControllerMethod(ProceedingJoinPoint pjp) {
long startTime = System.currentTimeMillis();
ResultBean<?> result;
try {
result = (ResultBean<?>) pjp.proceed();
logger.info(pjp.getSignature() + "use time:" + (System.currentTimeMillis() - startTime));
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
logger.info("URL : " + request.getRequestURL().toString());
logger.info("HTTP_METHOD : " + request.getMethod());
logger.info("IP : " + request.getRemoteAddr());
logger.info("CLASS_METHOD : " + pjp.getSignature().getDeclaringTypeName() + "." + pjp.getSignature().getName());
logger.info("ARGS : " + Arrays.toString(pjp.getArgs()));
} catch (Throwable e) {
result = handlerException(pjp, e);
}
return result;
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
logger.info("RESPONSE : " + new JsonUtil().toStrings(ret));
}
@SuppressWarnings("rawtypes")
private ResultBean<?> handlerException(ProceedingJoinPoint pjp, Throwable e) {
ResultBean<?> result = new ResultBean();
// 已知异常
if (e instanceof CheckException) {
result.setMsg(e.getLocalizedMessage());
result.setCode(ResultBean.FAIL);
} else {
logger.error(pjp.getSignature() + " error ", e);
result.setMsg(pjp.getSignature() + e.toString());
result.setCode(ResultBean.FAIL);
// 未知异常是应该重点关注的,这里可以做其他操作,如通知邮件,单独写到某个文件等等。
}
return result;
}
}
2.2.4 日志切面优先级
/**
* 日志切面
* @author Megustas
*
*/
//把这个类声明为一个切面:首先需要把该类放入到IOC容器中,通过注解@Component、再声明为一个切面,通过注解@Aspect,并且在配置文件中加入配置
//通过Order注解来指定切面的优先级,优先级数字越小代表优先级越高,越先执行
@Order(2)
@Aspect
@Component
public class LoggingAspect {
//这个方法在哪些类的哪些方法前执行,通过注解来规定
//声明该方法是一个前置通知,在目标方法开始之前执行,".add"方法说明在add方法之前执行,".*"则表示在包下所有方法之前执行
@Before("execution(public int com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.add(int, int))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("The method " + methodName + " begins with " + args);
}
//后置通知:在目标方法执行后(无论是否发生异常),执行的通知
//在后置通知中还不能访问目标目标方法执行的结果,执行结果在返回通知中进行访问
@After("execution(* com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.*(int, int))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " end ");
}
/*
* 返回通知:在方法正常结束后执行的代码,返回通知是可以访问到方法的返回值的
*/
@AfterReturning(value="execution(public int com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.add(int, int))",
returning="result")
public void afterReturning(JoinPoint joinPoint,Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends with " + result);
}
/**
* 在目标方法出现异常时会执行的代码
* 可以访问到异常对象,且可以指定在出现特定异常时再执行通知代码
* 例如Exception ex,NullPointerException ex,可以指定不同种类的异常,当是指定的异常种类时执行
* @param joinPoint
* @param ex
*/
@AfterThrowing(value="execution(public int com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.div(int, int))",
throwing="ex")
public void afterThrowing(JoinPoint joinPoint,Exception ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs excetion " + ex);
}
/**
* 环绕通知需要携带ProceedingJoinPoint类型的参数
* 环绕通知类似于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法
* 并且环绕通知必须有返回值,返回值即为目标方法的返回值(类似于动态代理)
* 最强的,前置、后置、返回与异常通知都可以,但是并不代表是最常用的
* @param pjd
*/
@Around("execution(* com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.add(int, int))")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;
String methodName = pjd.getSignature().getName();
//执行目标方法
try {
//前置通知
System.out.println("The method " + methodName + "begins with" + Arrays.asList(pjd.getArgs()));
result = pjd.proceed();
//后置通知
System.out.println("The method " + methodName + " ends with " + result);
} catch (Throwable e) {
// 异常通知
System.out.println("The method " + methodName + " occurs excetion " + e);
}
//后置通知
System.out.println("The method " + methodName + " ends" );
return result;
}
}
3 原理概述:织入的时机
编译期(AspectJ)
类加载时(AspectJ 5+)
运行时(Spring AOP):静态代理:代理模式;动态代理:JDK接口代理、Cglib继承代理
多个AOP链式调用,Order(0):责任链模式
@EnableDiscoveryClient
@SpringBootApplication
//强制使用cglib代理
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Application{
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
4 使用SpringAOP的注意事项/坑
不宜把重要的业务逻辑放到aop中处理
无法拦截static、final、private方法
无法拦截内部方法调用:SpringAOP用的是代理模式,所以spring容器中的对象才能被代理,被切面。