什么是AOP
- AOP是指面向切面编程,能够解决OOP纵向编程中会出现像权限认证、日志、事务处理外围事务导致核心业务代码混乱冗余的问题。
- 将外围事务封装为一个可重用的模块,命名为切面,降低耦合度提高可维护性
AOP的原理
AOP的实现原理在于代理模式,分为静态代理和动态代理。像AspectJ就是静态代理,SpringAOP就是动态代理
- AspectJ是静态代理,也称为编译时增强,会在编译期间生成AOP代理类,并将切面织入Java字节码中,运行的时候就是增强之后的AOP对象
- SpringAOP使用的是动态代理,所谓动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP代理对象,这个代理对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法(反射invoke)
SpringAOP实现动态代理的方式(原理)
JDK动态代理
- jdk动态代理只提供接口代理,不支持类的代理,被代理的类必须要实现接口,好让代理类重写接口方法生成代理对象
- jdk动态代理的核心是
invocationHandler
接口和proxy
类,在获取代理对象时使用Proxy
类来动态创建目标代理类 - 当代理对象调用真实对象的方法时,会自动跳转到代理对象关联的
invocationHandler
对象,其会通过invoke()
方法反射来调用目标类中的方法,动态地把业务横切进去
invocationHandler中invoke(Object proxy,Method method,Object[] args)
- proxy是指生成代理的对象
- method是指目标对象的方法,通过反射调用
- args是指目标对象方法的参数
例子
- 通过实现invocationHandler接口创建自己的调用处理器
- 为Proxy类提供要代理对象的类加载器、接口、处理器来创建动态代理对象
- 当代理对象调用真实对象的方法时,会自动跳转到代理对象关联的
invocationHandler
对象,其会通过invoke()
方法反射来调用目标类中的方法,动态地把业务横切进去
创建接口
public interface Subject {
public void SayHello(String name);
}
创建接口实现类
public class SubjetcImpl implements Subject{
@Override
public void SayHello(String name) {
System.out.println(name+"嘿嘿");
}
}
继承invocationHandler实现自己的调用器处理器
public class InvocationHandlerImpl implements InvocationHandler {
/**
* 要代理的真实对象
*/
private Object subject;
public InvocationHandlerImpl(Object subject) {
this.subject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//代理之前
System.out.println("调用之前");
System.out.println("Method: "+method);
//代理中
Object returnValue = method.invoke(subject, args);
//代理之后
System.out.println("调用结束");
return returnValue;
}
}
测试,通过proxy类生成动态代理对象
public class Main {
public static void main(String[] args) {
//要被代理的对象
Subject realSubject=new SubjetcImpl();
//要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法
InvocationHandler handler = new InvocationHandlerImpl(realSubject);
ClassLoader classLoader = realSubject.getClass().getClassLoader();
Class[] interfaces = realSubject.getClass().getInterfaces();
//该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
Subject subject =(Subject)Proxy.newProxyInstance(classLoader, interfaces, handler);
System.out.println("动态代理类的类型"+subject.getClass().getName());
subject.SayHello("aciu");
}
}
CGLIB
- 如果代理类没有实现接口,那么AOP就会选择使用CGLIB,用继承方式做动态代理。若该类为final类,则没法用CGLIB
- CGLIB(Code Generation Library)是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象。并覆盖其中特定方法并添加增强代码,从而实现AOP。
AOP名词概念
- 连接点(
Join Point
):指程序运行过程中执行的方法。SpringAOP中一个连接点总代表一个方法的执行 - 切入点(
Pointcut
): 匹配连接点的断言。通知与切入点表达式相关联,并在切入点匹配的任何连接点上运行(例如,具有特定名称的方法的执行),与切入点表达式匹配的连接点的概念是 AOP 的核心。用来描述我们要在哪些地方执行,也可以说成是用表达式匹配(正则断言)的切入点 - 切面(
Aspect
):被抽取出来的公共模块,可以用来横切多个对象。Aspect切面可以看成Poincut
切点和Advice通知的结合,一个切面可以由多个切点和通知组成(SpringAOP中用@Aspect) - 通知(
Advice
):指要在连接点(Join Point
)上执行的操作,即增强的逻辑,比如权限的校验和日志的记录等。通知有各种类型,包括Around
、Before
、After
、After returning
、After throwing
。 - 目标对象(
Target
):包含连接点的对象,也称作被通知的对象 - 织入(
Weaving
):通过动态代理在目标对象的方法中执行增强逻辑的过程,即在Target的Join point中执行Advice - 引入(
Introduction
):添加额外的方法或字段到被通知的类中
SpringAOP通知类型
- 前置通知(Before Advice):在连接点之前执行通知
- 后置通知(After Advice):在连接点退出的时候执行的通知
- 环绕通知(Around Advice):包围一个连接点的通知,可以在方法调用前后完成自定义行为
- 返回后通知(Afterreturning Advice):在连接点正常完成后执行的通知
- 抛出异常(AfterThrowing Advice):在方法抛出异常退出时执行的通知
执行顺序为,@Around
, @Before
, @After
, @AfterReturning
, @AfterThrowing
,若对同一切面切点要使用同一注解的话可使用@Order
进行排序
SpirngAOP注解定义
这里只记录@Aspect
、@Pointcut
、@Around
,像其他通知类型的注解、@DeclareParents
声明注解查阅官方文档Declaring Advice :: Spring Framework
@Aspect
定义切面,作用于类上。需要注意的是@Aspect
注释不足以在类路径中进行自动检测,需要添加一个单独的@Component
注释(或者根据 Spring 组件扫描器的规则,添加一个符合条件的自定义原型注释)。
@Pointcut
定义切点,有多种表达式,这里只记录一下最常用的execution
,其他的可翻阅文档Declaring a Pointcut :: Spring Framework
执行表达式为
execution(modifiers-pattern?
ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
modifiers-pattern
:修饰符,public
、private
等,省略时匹配任意修饰符
ret-type-pattern
:返回类型,不可省略的参数,使用 *
表示匹配任何返回类型
declaring-type
:声明类型,类名称模式,省略时匹配任意类型。
- 如果有指定,要包含
.
将其连接到方法名称name-pattern
。 ..
表示匹配包及其子包的所有类
name-pattern
:方法名称,使用*
表示全部,匹配任意方法,set*
匹配名称以 set 开头的方法
param-pattern
:匹配参数类型和数量
()
表示匹配没有参数的方法(..)
表示匹配有任意数量参数的方法(*)
表示匹配有一个任意类型参数的方法(*,String)
表示匹配有两个参数的方法,并且第一个为任意类型,第二个为String
类型
//匹配任何公共方法,修饰符+返回类型+方法名称+参数
execution(public * *(..))
//匹配任何以set开头的方法,返回类型+方法名称+参数
execution(* set*(..))
//在AccountService接口定义的任何方法的执行,返回类型+声明类型+方法名+参数
execution(* com.xyz.service.AccountService.*(..))
//服务包中定义的任何方法的执行,返回类型+声明类型+方法名+参数
execution(* com.xyz.service.*.*(..))
//服务包或其子包中定义的任何方法的执行,返回类型+声明类型+方法名称+参数
execution(* com.xyz.service..*.*(..))
@Around
-
Around通知能在方法运行之前和之后执行工作,如果需要在方法执行之前和之后以线程安全的方式共享状态(例如,启动和停止计时器) ,则经常使用 Around 通知。如果不需要前后都进行操作,不建议用Around,要节约性能。
-
@Around
注释的方法应该将Object
声明为其返回类型,并且第一个参数必须ProceedingJoinPoint
类型。 -
在通知方法的主体中,必须在
ProceedingJoinPoint
上调用process ()
,一般无参调用原始方法,可以接受Object[]数组作为参数。 -
ProceedingJoinPoint
为JoinPoint的子类,提供了以下常用方法-
getArgs()
:返回方法的参数 -
getThis()
:返回代理对象 -
getTarget()
:返回目标对象 -
getSignature()
:返回正在通知的方法的说明,可以将Signature
对象转换为MethodSignature
对象,通过MethodSignature
对象可以获取到更加详细的方法签名信息,比如方法返回类型、参数类型、判断是否有特定注解等。-
MethodSignature methodSignature = (MethodSignature) signature; String methodName = methodSignature.getName(); Class<?> returnType = methodSignature.getReturnType(); Class<?>[] parameterTypes = methodSignature.getParameterTypes(); boolean annotationPresent = method.isAnnotationPresent(DisableLog.class);
-
-
toString()
:打印所建议的方法的有用说明
-
@Aspect
public class AroundExample {
@Around("execution(* com.xyz..service.*.*(..))")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
SpringAOP的例子
重试操作
业务服务的执行有时会由于并发问题(例如,死锁失败)而失败。如果重试操作,则下一次尝试可能会成功。对于适合在这种情况下重试的业务服务(不需要返回给用户进行冲突解决的幂等操作) ,我们希望透明地重试该操作,以避免客户机看到 PessimisticLockingfalureException。这是一个明显跨越服务层中多个服务的需求,因此非常适合通过aop横切织入来实现。
@Aspect
@Component
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.CommonPointcuts.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
日志记录
@Slf4j
@Aspect
@Order(1)
@Component
public class CoreAspect {
@Autowired
private HttpServletRequest servletRequest;
@Around("execution(* com.aciu.admin.apis.controller..*.*(..))")
public Object execute(ProceedingJoinPoint pjp) throws Throwable {
Long startTime = System.currentTimeMillis();
//用于logback记录整个调用链路 web->service->dao,日志跟踪id
String traceLogId = SnowFlakeUtil.getId();
MDC.put("traceLogId", traceLogId);
//拿到接口方法信息
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
//如果有该注解,则不记录日志
if(method.isAnnotationPresent(DisableLog.class)) {
return pjp.proceed();
}
Object[] args = pjp.getArgs();
List<Object> logArgs = Lists.newArrayList(args).stream()
.filter(arg -> (!(arg instanceof HttpServletRequest)
&& !(arg instanceof HttpServletResponse)))
.collect(Collectors.toList());
String logArgsJson = JsonUtils.obj2String(logArgs);
StringBuilder logArgsSb = null;
if(logArgsJson!=null) {
if(logArgsJson.length()>4096) {
logArgsSb = new StringBuilder(logArgsJson.substring(0,4096));
}
}
//执行日志
log.debug("Http请求开始: Url:{},HTTP Method:{},IP:{},方法:{},被调用开始,是需要否授权:{},入参:{}",servletRequest.getRequestURI(), servletRequest.getMethod(), IpUtils.getIpAddress(servletRequest),
methodSignature.getDeclaringTypeName() + "." + methodSignature.getName(),
!method.isAnnotationPresent(DisableLoginSecurity.class), logArgsSb==null?logArgsJson:logArgsSb);
//TODO:可以执行检验入参是否符合规范的操作,抛出异常
// 执行方法
Object result = pjp.proceed();
//TODO:可以执行检验响应值是否符合规范的操作,抛出异常
String resultStr = JsonUtils.obj2String(result);
if(resultStr!=null) {
if(resultStr.length()>4096) {
resultStr = resultStr.substring(0,4096);
}
}
//执行日志
log.debug("Http请求【结束】: 请求耗时秒:{},【Url】:{},【HTTP Method】:{},【IP】:{},【方法】:{},【响应报文】:{}",
(System.currentTimeMillis() - startTime) / 1000, servletRequest.getRequestURI(), servletRequest.getMethod(),
IpUtils.getIpAddress(servletRequest), methodSignature.getDeclaringTypeName() + "." + methodSignature.getName(),
resultStr);
return result;
}
}