环境说明:Windows10、IntelliJ IDEA、SpringBoot
准备工作:在pom.xml中引入依赖
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
注:下面的实例代码中还涉及到阿里的fastjson依赖,也需要引入;这里不再给出。
现有Controller:
编写AOP:
方式一:通过表达式(模糊)匹配来指定切点。
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Map;
/**
* AOP切面
*
* 以下几个增强的执行顺序是:
* 1.aroundAdvice(.proceed();语句之前的代码)
* 2.beforeAdvice
* 3.被增强方法
* 4.aroundAdvice(.proceed();语句之后的代码)
* 5.afterAdvice
* 6.afterReturningAdvice
* 7.afterThrowingAdvice(有异常时才会走,无异常不会走此方法)
*
* 注: 当被增强方法 或 afterAdvice 或 afterReturningAdvice抛出异常时,会被afterThrowingAdvice
* 捕获到异常,进而短路走 afterThrowingAdvice方法
*
* @author JustryDeng
* @date 2018/12/17 20:25
*/
@Aspect
@Configuration
public class AopConfig {
/**
* 将表达式【* com.aspire.controller.AopController.*(..))】所匹配的所有方法标记为切点,
* 切点名为 executeAdvice()
*
* 注:execution里的表达式所涉及到的类名(除了基本类以外),其它的要用全类名;干脆不管是不
* 是基础类,都推荐使用全类名
* 注:如果有多个表达式进行交集或者并集的话,可以使用&&、||、or,示例:
* @Pointcut(
* "execution(* com.szlzcl.laodeduo.common.CommonResponse com.szlzcl.laodeduo.*.controller..*(..)) "
* + " || "
* + "execution(* com.szlzcl.laodeduo.common.CommonResponse com.szlzcl.laodeduo.config.common..*(..))"
* )
*
* @author JustryDeng
* @date 2018/12/18 13:43
*/
@Pointcut("execution(* com.aspire.controller.AopController.*(..))")
/**
* 使用注解来定位AOP作为节点的方法们
*/
// @Pointcut("@annotation(com.aspire.annotation.AdviceOne)")
public void executeAdvice() {
}
/**
* 切点executeAdvice()的前置增强方法
*
* @author JustryDeng
* @date 2018/12/18 13:47
*/
@Before(value = "executeAdvice()")
public void beforeAdvice(JoinPoint joinPoint) {
Object[] paramArray = joinPoint.getArgs();
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " -> beforeAdvice获取到了被增强方法的参数了,为:"
+ Arrays.toString(paramArray));
}
/**
* 切点executeAdvice()的后增强方法
*
* @author JustryDeng
* @date 2018/12/18 13:47
*/
@After("executeAdvice()")
public void afterAdvice() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " -> 后置增强afterAdvice执行了");
}
/**
* 切点executeAdvice()的后增强方法
*
* 注:当被增强方法 或 afterAdvice正常执行时,才会走此方法
* 注: returning指明获取到的(环绕增强返回的)返回值
*
* @author JustryDeng
* @date 2018/12/18 13:47
*/
@AfterReturning(value = "executeAdvice()", returning = "map")
public void afterReturningAdvice(Map<String, Object> map) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " -> afterReturningAdvice获得了返回结果 map -> " + map);
}
/**
* 当被增强方法 或 afterAdvice 或 afterReturningAdvice抛出异常时,会被afterThrowingAdvice
* 捕获到异常,进而短路走 afterThrowingAdvice方法
*
* @author JustryDeng
* @date 2018/12/18 13:57
*/
@AfterThrowing(value = "executeAdvice()", throwing ="ex")
public void afterThrowingAdvice(Exception ex) {
System.out.println("AfterThrowing捕获到了 --->" + ex);
}
/**
* 环绕增强 会在 被增强方法执行完毕后 第一个执行,
* 所以在绝大多数时候,我们都直接返回thisJoinPoint.proceed();的返回值;
* 如果此方法返回null,那么@AfterReturning方法获取到的返回值 就会是null
*
* @throws Throwable 当目标方法抛出异常时
*
* @author JustryDeng
* @date 2018/12/18 13:57
*/
@Around("executeAdvice()")
public Object aroundAdvice(ProceedingJoinPoint thisJoinPoint) throws Throwable {
String threadName = Thread.currentThread().getName();
System.err.println(threadName + " -> 环绕增强aroundAdvice --> before proceed()执行了");
// 执行被增强方法,并获取到返回值
// 类似于 过滤器的chain.doFilter(req,resp)方法
Object obj = thisJoinPoint.proceed();
System.err.println(threadName + " -> 环绕增强aroundAdvice --> after proceed()执行了");
return obj;
}
}
测试一下:
以debug模式启动项目,使用postman访问测试;依次放行断点,控制台输出:
注:之所以要以debug方式运行访问,来查看控制台输出;是因为如果直接运行访问的话,可能控制台打印出的就不是真实的
运行顺序,这是因为CPU会对无关的语句进行重排序。
方式二:通过注解来指定切点。
第一步:自定义一个注解。
注:这是一个开关性的注解,因此不需要任何属性。
第二步:在要被增强的方法上使用该注解。
第三步:定义切点时,使用注解来定义。
注:此时即为:被@annotation(com.aspire.annotation.AdviceOne注解标注的方法,作为切点excudeAdvice()。
注:相比起使用execution来匹配方法,将匹配上的方法作为节点的方式;使用注解@annotation来标记方法的方式更为灵活。
第四步:编写切面具体逻辑。
注:此处示例的切面的逻辑与上面给出的文字版逻辑一样,这里就不再赘述了。
第五步:使用测试(测试方式同上),控制台输出:
由此可见,这几个增强器的执行顺序依次为:
AOP注意事项:
- 确保被AOP的方法所在的Bean已进行了IoC。
- 调用方法的实例,必须是通过Spring注入的,不能是自己new的。
- 被AOP的方法的修饰符最好是public的。因为如果修饰符是protected或默认的,那么只有在同包或者子类中,才能调用到; 而在实际调用时,调用者可能与AOP方法所在的类处于不同的包。即:被protected修饰符(或默认修饰符)修饰的方法,可能导致AOP失效。被private修饰符修饰的方法AOP会失效。
注:如果是自己写的方法调用,那么即便是private修饰符修饰的方法,也可以通过设置Method#setAccessible(true)后,进行调用。 - A方法直接调用同类下的B方法,不会走B方法的AOP;如果非要内部调用且要求走AOP的话,可以通过AopContext获取到当前代理,然后使用该代理调用B方法。
拓展 - 灵活使用且(&&)、或(||)、非(!)定位切点(示例):
现有以下两种注解:
- (定位切点的)注解RecordParameters:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 记录 入参、出参
*
* @author JustryDeng
* @date 2019/12/4 13:53
*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE, ElementType.METHOD})
public @interface RecordParameters {
/** 日志级别 */
LogLevel logLevel() default LogLevel.INFO;
/** 日志策略 */
Strategy strategy() default Strategy.INPUT_OUTPUT;
/**
* 日志级别枚举
*
* P.S. 对于入参、出参日志来讲,debug、info、warn这三种级别就够了
*/
enum LogLevel {
/** debug */
DEBUG,
/** info */
INFO,
/** warn */
WARN
}
/**
* 日志记录策略枚举
*/
enum Strategy {
/** 记录入参 */
INPUT,
/** 记录出参 */
OUTPUT,
/** 既记录入参,也记录出参 */
INPUT_OUTPUT
}
}
- (忽略切点的)注解IgnoreRecordParameters:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 开关注解 - 忽略RecordParameters注解的功能
*
* @author JustryDeng
* @date 2019/12/4 13:53
*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface IgnoreRecordParameters {
}
案例一:实现当(定位切点的)注解加在类上时,该类下的所有方法都走aop。
@Pointcut(
"@within(com.szlaozicl.actuator.annotation.RecordParameters)"
)
public void executeAdvice() {
}
案例二:实现当(定位切点的)注解加在方法上时,该方法走aop。
@Pointcut(
"@annotation(com.szlaozicl.actuator.annotation.RecordParameters)"
)
public void executeAdvice() {
}
案例三:实现(定位切点的)注解既可加在类上,又可加在方法上。
@Pointcut("@within(com.szlaozicl.actuator.annotation.RecordParameters)"
+ " || "
+ "@annotation(com.szlaozicl.actuator.annotation.RecordParameters)"
)
public void executeAdvice() {
}
案例四:实现(定位切点的)注解既可加在类上,又可加在方法上;同时,若某方法上存在另一个自定义的
标志性注解时,则该方法不会走aop。
@Pointcut(
"("
+ "@within(com.szlaozicl.actuator.annotation.RecordParameters)"
+ " || "
+ "@annotation(com.szlaozicl.actuator.annotation.RecordParameters)"
+ ")"
+ " && "
+ "!@annotation(com.szlaozicl.actuator.annotation.IgnoreRecordParameters)"
)
public void executeAdvice() {
}
案例五:注解与表达式一起混合使用。
/**
* 【@within】: 当将注解加在类上时,等同于 在该类下的所有方法上加上了该注解(即:该类的所有方法都会被aop)。
* 注意:注解必须写在类上,不能写在接口上。
* 【@annotation】: 当将注解加在某个方法上时,该方法会被aop。
* 【execution】: 这里:
* 第一个*, 匹配所有返回类型
* 第二个..*,匹配com.szlaozicl.demo.controller包下的,所有的类(含其子孙包下的类)
* 最后的*(..), 匹配任意方法任意参数。
*/
@Pointcut(
"("
+ "@within(com.szlaozicl.demo.annotation.RecordParameters)"
+ " || "
+ "@annotation(com.szlaozicl.demo.annotation.RecordParameters)"
+ " || "
+ "execution(* com.szlaozicl.demo.controller..*.*(..))"
+ ")"
+ " && "
+ "!@annotation(com.szlaozicl.demo.annotation.IgnoreRecordParameters)"
)
public void executeAdvice() {
}
拓展 - 使用AOP记录入参/出参:
提示:下面的示例,涉及到的注解,在上一个拓展里有给出。
import com.alibaba.fastjson.JSON;
import com.szlaozicl.demo.annotation.RecordParameters;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 方法 入参、出参 记录
*<p>
* 注: 根据此AOP的逻辑, 若注解与表达式同时匹配成功,那么 注解的优先级高于表达式的优先级。
*<p>
* <b>特别注意:<b/>这里借助了RecordParametersAdvice的logger来记录其它地方的日志。即: 相当于其它地方将记录日志的动
* 作委托给RecordParametersAdvice的logger来进行, 所以此logger需要能打印所有地方最下的日志级别(一般为debug)。
* 即:需要在配置文件中配置<code>logging.level.com.szlaozicl.demo.aop.RecordParametersAdvice=debug</code>
* 以保证此处有“权限”记录所有用到的日志级别的日志。
*
* @author JustryDeng
* @date 2019/12/4 13:57
*/
@Slf4j
@Order
@Aspect
@Configuration
@RequiredArgsConstructor
public class RecordParametersAdvice {
/** 栈帧局部变量表参数名侦查器 */
private static final LocalVariableTableParameterNameDiscoverer PARAMETER_NAME_DISCOVER = new LocalVariableTableParameterNameDiscoverer();
/** 无返回值 */
private static final String VOID_STR = void.class.getName();
/** 判断是否是controller类的后缀 */
private static final String CONTROLLER_STR = "Controller";
private final AopSupport aopSupport;
/**
* 【@within】: 当将注解加在类上时,等同于 在该类下的所有方法上加上了该注解(即:该类的所有方法都会被aop)。
* 注意:注解必须写在类上,不能写在接口上。
* 【@annotation】: 当将注解加在某个方法上时,该方法会被aop。
* 【execution】: 这里:
* 第一个*, 匹配所有返回类型
* 第二个..*,匹配com.szlaozicl.demo.controller包下的,所有的类(含其子孙包下的类)
* 最后的*(..), 匹配任意方法任意参数。
*/
@Pointcut(
"("
+ "@within(com.szlaozicl.demo.annotation.RecordParameters)"
+ " || "
+ "@annotation(com.szlaozicl.demo.annotation.RecordParameters)"
+ " || "
+ "execution(* com.szlaozicl.demo.controller..*.*(..))"
+ ")"
+ " && "
+ "!@annotation(com.szlaozicl.demo.annotation.IgnoreRecordParameters)"
)
public void executeAdvice() {
}
/**
* 环绕增强
*/
@Around("executeAdvice()")
public Object aroundAdvice(ProceedingJoinPoint thisJoinPoint) throws Throwable {
// 获取目标Class
Object targetObj = thisJoinPoint.getTarget();
Class<?> targetClazz = targetObj.getClass();
String clazzName = targetClazz.getName();
// 获取目标method
MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
// 获取目标annotation
RecordParameters annotation = targetMethod.getAnnotation(RecordParameters.class);
if (annotation == null) {
annotation = targetClazz.getAnnotation(RecordParameters.class);
// 如果是通过execution触发的,那么annotation可能为null, 那么给其赋予默认值即可
if (annotation == null && clazzName.endsWith(CONTROLLER_STR) ) {
annotation = (RecordParameters) AnnotationUtils.getDefaultValue(RecordParameters.class);
}
}
// 是否需要记录入参、出参
boolean shouldRecordInputParams;
boolean shouldRecordOutputParams;
RecordParameters.LogLevel logLevel;
boolean isControllerMethod;
if (annotation != null) {
shouldRecordInputParams = annotation.strategy() == RecordParameters.Strategy.INPUT
||
annotation.strategy() == RecordParameters.Strategy.INPUT_OUTPUT;
shouldRecordOutputParams = annotation.strategy() == RecordParameters.Strategy.OUTPUT
||
annotation.strategy() == RecordParameters.Strategy.INPUT_OUTPUT;
logLevel = annotation.logLevel();
isControllerMethod = clazzName.endsWith(CONTROLLER_STR);
// 此时,若annotation仍然为null, 那说明是通过execution(* com.szlaozicl.demo.controller.*.*(..)触发切面的
} else {
shouldRecordInputParams = shouldRecordOutputParams = true;
logLevel = RecordParameters.LogLevel.INFO;
isControllerMethod = true;
}
final String classMethodInfo = "Class#Method → " + clazzName + "#" + targetMethod.getName();
if (shouldRecordInputParams) {
preHandle(thisJoinPoint, logLevel, targetMethod, classMethodInfo, isControllerMethod);
}
Object obj = thisJoinPoint.proceed();
if (shouldRecordOutputParams) {
postHandle(logLevel, targetMethod, obj, classMethodInfo, isControllerMethod);
}
return obj;
}
/**
* 前处理切面日志
*
* @param pjp
* 目标方法的返回结果
* @param logLevel
* 日志级别
* @param targetMethod
* 目标方法
* @param classMethodInfo
* 目标类#方法
* @param isControllerMethod
* 是否是controller类中的方法
* @date 2020/4/10 18:21:17
*/
private void preHandle(ProceedingJoinPoint pjp, RecordParameters.LogLevel logLevel,
Method targetMethod, String classMethodInfo, boolean isControllerMethod) {
StringBuilder sb = new StringBuilder(64);
sb.append("\n【the way in】");
if (isControllerMethod) {
sb.append("request-path[").append(aopSupport.getRequestPath()).append("] ");
}
sb.append(classMethodInfo);
Object[] parameterValues = pjp.getArgs();
if (parameterValues != null && parameterValues.length > 0) {
String[] parameterNames = PARAMETER_NAME_DISCOVER.getParameterNames(targetMethod);
if (parameterNames == null) {
throw new RuntimeException("parameterNames must not be null!");
}
sb.append(", with parameters ↓↓");
int iterationTimes = parameterValues.length;
for (int i = 0; i < iterationTimes; i++) {
sb.append("\n\t").append(parameterNames[i]).append(" => ").append(aopSupport.jsonPretty(parameterValues[i]));
if (i == iterationTimes - 1) {
sb.append("\n");
}
}
} else {
sb.append(", without any parameters");
}
aopSupport.log(logLevel, sb.toString());
}
/**
* 后处理切面日志
*
* @param logLevel
* 日志级别
* @param targetMethod
* 目标方法
* @param obj
* 目标方法的返回结果
* @param classMethodInfo
* 目标类#方法
* @param isControllerMethod
* 是否是controller类中的方法
* @date 2020/4/10 18:21:17
*/
private void postHandle(RecordParameters.LogLevel logLevel, Method targetMethod,
Object obj, String classMethodInfo, boolean isControllerMethod) {
StringBuilder sb = new StringBuilder(64);
sb.append("\n【the way out】");
if (isControllerMethod) {
sb.append("request-path[").append(aopSupport.getRequestPath()).append("] ");
}
sb.append(classMethodInfo);
Class<?> returnClass = targetMethod.getReturnType();
sb.append("\n\treturn type → ").append(targetMethod.getReturnType());
if (!VOID_STR.equals(returnClass.getName())) {
sb.append("\n\treturn result → ").append(aopSupport.jsonPretty(obj));
}
sb.append("\n");
aopSupport.log(logLevel, sb.toString());
}
@Component
static class AopSupport {
private static Class<?> logClass = log.getClass();
private static Map<String, Method> methodMap = new ConcurrentHashMap<>(8);
@PostConstruct
private void init() throws NoSuchMethodException {
String debugStr = RecordParameters.LogLevel.DEBUG.name();
String infoStr = RecordParameters.LogLevel.INFO.name();
String warnStr = RecordParameters.LogLevel.WARN.name();
Method debugMethod = logClass.getMethod(debugStr.toLowerCase(), String.class, Object.class);
Method infoMethod = logClass.getMethod(infoStr.toLowerCase(), String.class, Object.class);
Method warnMethod = logClass.getMethod(warnStr.toLowerCase(), String.class, Object.class);
methodMap.put(debugStr, debugMethod);
methodMap.put(infoStr, infoMethod);
methodMap.put(warnStr, warnMethod);
}
/**
* 记录日志
*
* @param logLevel
* 要记录的日志的级别
* @param markerValue
* formatter中占位符的值
* @date 2020/4/11 12:57:21
*/
private void log(RecordParameters.LogLevel logLevel, Object markerValue){
try {
methodMap.get(logLevel.name()).invoke(log, "{}", markerValue);
} catch (IllegalAccessException|InvocationTargetException e) {
throw new RuntimeException("RecordParametersAdvice$AopSupport#log occur error!", e);
}
}
/**
* json格式化输出
*
* @param obj
* 需要格式化的对象
* @return json字符串
* @date 2019/12/5 11:03
*/
String jsonPretty(Object obj) {
return JSON.toJSONString(obj);
}
/**
* 获取请求path
*
* @return 请求的path
* @date 2020/4/10 17:13:06
*/
String getRequestPath() {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
log.warn("obtain request-path is empty");
return "";
}
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
return request.getRequestURI();
}
}
}
拓展 - 采坑提醒:
对于环绕增强@Around,不建议try-catch其.proceed(),考虑下面这种情况:
- .proceed()对应的方法出现了异常A,导致@Around方法返回的obj为null。
- 此时@AfterReturning方法受到的返回值就会是null。null打点调用方法,就会出现NullPointerException。
- 假设我们有个全局异常处理器:
从上图可知,由于本来是抛出的异常A,但是在AOP那里发生了异常劫持,抛出了一个空指针异常,这就会导致我们全局异常处理器会走不同的逻辑,进而导致返回给前端的信息不是我们想要的信息。因此,得出以下结论:
使用环绕增强时,需要根据自己的需要,考虑是否try-catch掉.proceed();一般情况下,不建议try-catch其.proceed()。
^_^ 如有不当之处,欢迎指正