SpringBoot系列-AOP 面向切面
原创
©著作权归作者所有:来自51CTO博客作者Jaemon的原创作品,请联系作者获取转载授权,否则将追究法律责任
基本概念
- Advice(通知、切面): 某个连接点所采用的处理逻辑,也就是向连接点注入的代码, AOP在特定的切入点上执行的增强处理。
- @Before: 标识一个前置增强方法,相当于BeforeAdvice的功能。
- @After: final增强,不管是抛出异常或者正常退出都会执行。
- @AfterReturning: 后置增强,似于AfterReturningAdvice, 方法正常退出时执行。
- @AfterThrowing: 异常抛出增强,相当于ThrowsAdvice。
- @Around: 环绕增强,相当于org.aopalliance.intercept.MethodInterceptor。
- JointPoint(连接点):程序运行中的某个阶段点,比如方法的调用、异常的抛出等。
- Pointcut(切入点): JoinPoint的集合,是程序中需要注入Advice的位置的集合,指明Advice要在什么样的条件下才能被触发,在程序中主要体现为书写切入点表达式。
- Advisor(增强): PointCut和Advice的综合体,完整描述了一个advice将会在pointcut所定义的位置被触发。
- @Aspect(切面): 通常是一个类的注解,类中可以定义切入点和通知。
- AOP Proxy:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。
Pointcut
表示式(expression)和签名(signature)
// Pointcut表示式
@Pointcut("execution(* com.jaemon.controller.*Controller.*(..))")
// Point签名
private void log(){}
由下列方式来定义或者通过 &&、 ||、 !、 的方式进行组合:
- execution:用于匹配方法执行的连接点;
- within:用于匹配指定类型内的方法执行;
- this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;
- target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
- args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;
- @within:用于匹配所以持有指定注解类型内的方法;
- @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;
- @args:用于匹配当前执行的方法传入的参数持有指定注解的执行;
- @annotation:用于匹配当前执行方法持有指定注解的方法;
格式
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
其中后面跟着“?”的是可选项
括号中各个pattern分别表示:
- 修饰符匹配(modifier-pattern?)
- 返回值匹配(ret-type-pattern): 可以为*表示任何返回值, 全路径的类名等
- 类路径匹配(declaring-type-pattern?)
- 方法名匹配(name-pattern):可以指定方法名 或者 代表所有, set 代表以set开头的所有方法
- 参数匹配((param-pattern)):可以指定具体的参数类型。多个参数间用“,”隔开,各个参数也可以用"*" 来表示匹配任意类型的参数,"…"表示零个或多个任意参数。
eg. (String)表示匹配一个String参数的方法;(*,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型。
举例
- 任意公共方法的执行:execution(public * *(…))
- 任何一个以“set”开始的方法的执行:execution(* set*(…))
- AccountService 接口的任意方法的执行:execution(* com.xyz.service.AccountService.*(…))
- 定义在service包里的任意方法的执行: execution(* com.xyz.service..(…))
- 定义在service包和所有子包里的任意类的任意方法的执行:execution(* com.xyz.service….(…))
- 第一个表示匹配任意的方法返回值, …(两个点)表示零个或多个,第一个…表示service包及其子包,第二个表示所有类, 第三个*表示所有方法,第二个…表示方法的任意参数个数
- 定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行:execution(* com.test.spring.aop.pointcutexp…JoinPointObjP2.*(…))")
- pointcutexp包里的任意类: within(com.test.spring.aop.pointcutexp.*)
- pointcutexp包和所有子包里的任意类:within(com.test.spring.aop.pointcutexp…*)
- 实现了Intf接口的所有类,如果Intf不是接口,限定Intf单个类:this(com.test.spring.aop.pointcutexp.Intf)
当一个实现了接口的类被AOP的时候,用getBean方法必须cast为接口类型,不能为该类的类型 - 带有@Transactional标注的所有类的任意方法:
- @within(org.springframework.transaction.annotation.Transactional)
- @target(org.springframework.transaction.annotation.Transactional)
- 带有@Transactional标注的任意方法:@annotation(org.springframework.transaction.annotation.Transactional)
@within和@target针对类的注解,@annotation是针对方法的注解
- 参数带有@Transactional标注的方法:@args(org.springframework.transaction.annotation.Transactional)
- 参数为String类型(运行是决定)的方法: args(String)
JoinPoint
当使用@Around处理时,需要将第一个参数定义为ProceedingJoinPoint类型,该类是JoinPoint的子类。
常用的方法:
- Object[] getArgs:返回目标方法的参数
- Signature getSignature:返回目标方法的签名
- Object getTarget:返回被织入增强处理的目标对象
- Object getThis:返回AOP框架为目标对象生成的代理对象
ProceedingJoinPoint获取当前方法
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
// 上面这种方式获取到的方法是接口的方法而不是具体的实现类的方法,因此是错误的。
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = null;
if (!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("该注解只能用于方法");
}
methodSignature = (MethodSignature) signature;
Object target = joinPoint.getTarget();
Method currentMethod = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
实战演练
入门案例
添加jar包依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
切面配置
@Component
@Aspect
@Slf4j
public class JaemonAop {
/**
* 前置通知, 方法调用前被调用
* */
@Before("executeService()")
public void doBeforeAdvice(JoinPoint joinPoint){
log.info("doBeforeAdvice: {}." + joinPoint.getSignature().getName());
}
/**
* 匹配 com.jaemon.controller包及其子包下的所有类的所有方法
*
* execution() 表达式的主体
* 第一个“*”符号 表示返回值的类型任意
* com.jaemon.controller AOP所切的服务的包名,即,需要进行横切的业务类
* 包名后面的“..” 表示当前包及子包
* 第二个“*” 表示类名,*即所有类
* .*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型
* */
@Pointcut("execution(* com.jaemon.controller..*.*(..))")
public void executeService(){
}
/**
* 后置最终通知, 目标方法只要执行完了就会执行后置通知方法
* */
@After("executeService()")
public void doAfterAdvice(JoinPoint joinPoint){
log.info("doAfterAdvice: {}.", joinPoint.getSignature().getName());
}
/**
* 后置返回通知
* 这里需要注意的是:
* 如果参数中的第一个参数为JoinPoint, 则第二个参数为返回值的信息
* 如果参数中的第一个参数不为JoinPoint, 则第一个参数为returning中对应的参数
* returning 限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知, 否则不执行
* 对于returning对应的通知方法参数为Object类型将匹配任何目标返回值
* */
@AfterReturning(value = "execution(* com.jaemon.controller..*.*(..))", returning = "response")
public void doAfterReturningAdvice(JoinPoint joinPoint, Object response){
log.info("doAfterReturningAdvice: {} {}.", joinPoint.getSignature().getName(), JSON.toJSON(response));
}
/**
* 后置异常通知
* 第二个参数为需要切点匹配的异常类型, 如(JoinPoint joinPoint, NullPointerExceptionrowable exception), 则只会匹配抛出空指针异常的方法
* 对于throwing对应的通知方法参数为Throwable类型将匹配任何异常
* */
@AfterThrowing(value = "executeService()", throwing = "exception")
public void doAfterThrowingAdvice(JoinPoint joinPoint, Throwable exception) {
log.info("doAfterThrowingAdvice: {}.", joinPoint.getSignature().getName());
if (exception instanceof BusinessException) {
log.info("exception type BusinessException");
}
}
/**
* 环绕通知
* 环绕通知非常强大, 可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
* */
@Around("execution(* com.jaemon.controller..*.*(..))")
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
log.info("AOP method name: {}, method modifiers: {}, method args: {}.",
proceedingJoinPoint.getSignature().getName(), proceedingJoinPoint.getSignature().getModifiers(), proceedingJoinPoint.getArgs());
try {
return proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
自定义注解切面
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD
})
@Documented
public @interface WebLog {
String type() default "";
String sign() default "";
}
切面配置
@Aspect
@Component
@Slf4j
public class LogAspect {
@Pointcut(value = "@annotation(com.jaemon.app.aop.WebLog)")
private void pointCut() {
}
@Around(value = "pointCut() && @annotation(webLog)")
public Object around(ProceedingJoinPoint proceedingJoinPoint, WebLog webLog) {
log.info("type=[{}], sign=[{}].", webLog.type(), webLog.sign());
try {
return proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
接口代码
@RequestMapping("/menu")
@WebLog(sign = "queryMenu", type = "all")
public Response menu() {
// ...
return Response.success();
}
为请求参数统一分配请求ID
@Component
@Aspect
@Slf4j
public class RequestAop {
@Around("execution(* com.jaemon.contorller..*.*(..))")
public Object doAroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
boolean distribute = false;
String uniqueId = "SCS-" + UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
// 优先为请求参数分配
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof Request) {
Request request = (Request) arg;
request.setRequestId(uniqueId);
distribute = true;
}
}
if (!distribute) {
httpServletRequest.setAttribute("REQUEST_UNIQUE_ID_KEY", uniqueId);
}
return joinPoint.proceed();
}
}
日志打印请结合SpringBoot为Logback日志框架统一添加日志跟踪ID
注解方式内部接口权限控制
权限注解
package com.jaemon.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ManagerApi {
}
AOP切面
@Component
@Aspect
@Slf4j
// 控制多个aop的执行顺序
@Order(2)
public class ManageApiAop {
@Autowired
private SdkProperties sdkProperties;
@Autowired
private RedisClient redisClient;
// 针对类和方法上加了注解 ManagerApi 进行切面
@Pointcut(value = "@within(com.jaemon.annotation.ManagerApi) || @annotation(com.jaemon.annotation.ManagerApi)")
private void pointcut() {
}
@Around("pointcut()")
public Object managerApi(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
String requestId = (String) request.getAttribute(REQUEST_UNIQUE_ID_KEY);
// 获取 PathVariable 注解的参数值
Map<String, String> map = (Map)request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
String authKey = map.get("authKey");
// 如果当前运行为master服务, 则进行authKey权限认证
if (sdkProperties.isMaster() && StringUtils.isNotEmpty(authKey)) {
Serializable flag = redisClient.getKV(MANAGE_AUTH_KEY + authKey);
if (flag != null) {
return proceedingJoinPoint.proceed();
}
}
log.info("#{}# authKey={}无效.", requestId, authKey);
// 如果应用程序当前运行环境为slaver或authKey不匹配则无访问权限
Response response = Response.failed(ResponseEnum.NONE_ACCESS_AUTH);
return response;
}
}
接口验证
@RestController
@Slf4j
@RequestMapping("/manage/{authKey}")
// 类上加注解, 类中的所有接口进行权限控制
@ManagerApi
public class ManageController {
}
@RestController
@Slf4j
@RequestMapping("/manage/{authKey}")
public class ManageController {
// 方法上加注解, 只对当前接口进行权限控制
@ManagerApi
@PostMapping("/create")
public Response create() {
}
}
配置文件方式配置AOP
<!-- 默认采用jdk代理, proxy-target-class="true"为CGLIB代理 -->
<aop:aspectj-autoproxy/>
<!-- 设置切面 -->
<aop:config>
<!--<aop:pointcut id="myPointcut" expression="execution(* com.xxx.DemoService.call(..))"/>-->
<!-- 基于注解设置切面 -->
<aop:pointcut id="myPointcut" expression="@annotation(com.xxx.HystrixCommandTag)"/>
<aop:advisor advice-ref="hystrixAspect" pointcut-ref="myPointcut"/>
</aop:config>
@Aspect
@Component
public class HystrixAspect implements MethodBeforeAdvice, AfterReturningAdvice, MethodInterceptor, ThrowsAdvice {
private static final Logger logger = LoggerFactory.getLogger(HystrixAspect.class);
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 代理的方法, 无法获取对应的注解信息
Method method = invocation.getMethod();
// 实际方法
Method actualMethod = invocation.getThis().getClass().getMethod(method.getName(), method.getParameterTypes());
// 走熔断流程
if (SystemConfig.HystrixEnabled() && actualMethod.isAnnotationPresent(HystrixCommandTag.class)) {
HystrixCommandTag hystrixCommand = actualMethod.getAnnotation(HystrixCommandTag.class);
if (hystrixCommand.enable()) {
return new PvsHystrixCommand(invocation, actualMethod, hystrixCommand).execute();
}
}
// 走正常流程
return invocation.proceed();
}
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
}
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
}
// 这个方法是必须的
public void afterThrowing(Method method, Object[] objects, Object target, Throwable throwable) {
logger.error(throwable.getMessage());
}
}
Reference