1. AOP示例描述
我们模拟一个线上调用业务callData, 然后对callData做切面通知, 封装调用方法信息做日志记录的例子; 不过我们不持久化日志, 只做打印:
2. 注解AOP示例
step1: maven依赖
spring-aspects
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<!-- 我这里用到了gson包, 引入一下 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.7</version>
</dependency>
会看到依赖包引入了:
spring-aspects-5.2.7.RELEASE.jar
aspectjweaver-1.9.5.jar
step2: 业务逻辑类:
业务接口: BizRequest.java
和 依赖的 实体类 User.java
可见: 业务类只是调用一个 callData(Map<String, String> params)
方法并返回 String结果(这里是User对象的json字符串);
如果业务参数 params里不包含全部的三要素参数, 会报异常:
package com.niewj.aop;
import com.google.gson.Gson;
import com.niewj.bean.User;
import java.util.Map;
public class BizRequest {
/**
* 请求远程数据: 假如 向某个接口请求一些数据, 然后返回一个用户信息;
*
* @param params 向第三方接口传递的参数
* @return 第三方返回的数据:一般为json字符串
*/
public String callData(Map<String, String> params) {
if (params == null || !params.containsKey("idCard") || !params.containsKey("name") || !params.containsKey("phone")) {
throw new IllegalArgumentException("name/idCard/phone三要素参数是必需的!");
}
User user = new User("niewj", "123456", true);
return new Gson().toJson(user);
}
}
User.java
package com.niewj.bean;
import lombok.Data;
@Data
public class User {
private String name;
private String passwd;
private boolean online ;
public User(String name, String passwd, boolean online){
System.out.println("User-初始化!");
this.name = name;
this.passwd = passwd;
this.online = online;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", passwd='" + passwd + '\'' +
", online=" + online +
'}';
}
}
step3: 定义切面类并注解(@Aspect):
通知类型包括:
1. 前置通知(@Before)
目标方法运行之前执行;
2. 后置通知(@After)
目标方法运行结束之后执行.
3. 返回通知(@AfterReturning)
目标方法正常返回之后执行.
4. 异常通知(@AfterThrowing)
目标方法出现异常之后执行.
5. 环绕通知(@Around)
动态代理, 手动推动目标方法运行: jointPoint.proceed();
定义一个切面类, 用来做切入逻辑: BizRequestLogAspects.java
package com.niewj.aop;
import com.google.gson.Gson;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class BizRequestLogAspects {
/**
* 下面的各个方法切入点医院, 可以抽取一个方法
* 1. 方法名随意, 比如: myPointCut;
* 2. 空实现即可;
*/
@Pointcut("execution(public String com.niewj.aop.BizRequest.*(..))")
public void myPointCut() {
}
@Before("myPointCut()") // 或者 外部用可以写全名: com.niewj.aop.BizRequestLogAspects.myPointCut()
public void logStart(JoinPoint joinPoint) {
long time = System.currentTimeMillis();
String bizMethodName = joinPoint.getSignature().getName();
String bizMethodParamJson = new Gson().toJson(joinPoint.getArgs());
// 现实生产中, 可能会把日志信息封装成一个对象, 可以持久化记录到 mongo 或者 hbase中,
// 以便将来提供消费查询服务
LogInfo logInfo = new LogInfo();
logInfo.setBizMethodName(bizMethodName);
logInfo.setBizMethodParamsJson(bizMethodParamJson);
logInfo.setLogTime(time);
logInfo.setLogType(LogTypeEnum.BEFORE);
System.out.println(logInfo);
}
@After(("myPointCut()"))
public void logStop(JoinPoint joinPoint) {
long time = System.currentTimeMillis();
String bizMethodName = joinPoint.getSignature().getName();
// 现实生产中, 可能会把日志信息封装成一个对象, 可以持久化记录到 mongo 或者 hbase中,
// 以便将来提供消费查询服务
LogInfo logInfo = new LogInfo();
logInfo.setBizMethodName(bizMethodName);
logInfo.setLogTime(time);
logInfo.setLogType(LogTypeEnum.AFTER);
System.out.println(logInfo);
}
@AfterReturning(value = "myPointCut()", returning = "rst")
public void logReturn(JoinPoint joinPoint, Object rst) {
long time = System.currentTimeMillis();
String bizMethodName = joinPoint.getSignature().getName();
String bizMethodParamJson = new Gson().toJson(joinPoint.getArgs());
// 现实生产中, 可能会把日志信息封装成一个对象, 可以持久化记录到 mongo 或者 hbase中,
// 以便将来提供消费查询服务
LogInfo logInfo = new LogInfo();
logInfo.setBizMethodName(bizMethodName);
logInfo.setBizMethodParamsJson(bizMethodParamJson);
logInfo.setLogTime(time);
logInfo.setLogType(LogTypeEnum.AFTER_RETURNING);
logInfo.setReturnJson(String.valueOf(rst));
System.out.println(logInfo);
}
@AfterThrowing(value = "myPointCut()", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
long time = System.currentTimeMillis();
String bizMethodName = joinPoint.getSignature().getName();
String bizMethodParamJson = new Gson().toJson(joinPoint.getArgs());
// 现实生产中, 可能会把日志信息封装成一个对象, 可以持久化记录到 mongo 或者 hbase中,
// 以便将来提供消费查询服务
LogInfo logInfo = new LogInfo();
logInfo.setBizMethodName(bizMethodName);
logInfo.setBizMethodParamsJson(bizMethodParamJson);
logInfo.setLogTime(time);
logInfo.setLogType(LogTypeEnum.AFTER_THROWING);
logInfo.setException(ex);
System.out.println(logInfo);
}
}
封装的 LogInfo
日志类:
package com.niewj.aop;
import com.google.gson.Gson;
import lombok.Data;
@Data
public class LogInfo {
private String bizMethodName;
private String bizMethodParamsJson;
private long logTime;
private LogTypeEnum logType;
private Exception exception;
private String returnJson;
@Override
public String toString() {
return "[" + this.logType.getValue() + "]==>\t" + new Gson().toJson(this);
}
}
顺便声明了个LogTypeEnum
枚举类:
package com.niewj.aop;
public enum LogTypeEnum {
BEFORE("@Before"),
AFTER("@After"),
AFTER_RETURNING("@AfterReturning"),
AFTER_THROWING("@AfterThrowing"),
AROUND("@Around");
private String value;
LogTypeEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
6. 切面类注意小结:
1. @Aspect注解
配置类里只是注解了@Bean
, 需要这里的@Aspect
标注, 容器才会辨别它就是切面类;
此注解相当于开启aspectj自动代理设置–> 类似于之前xml配置:
<aop:aspectj-autoproxy />
2. @Pointcut 的方法
只是对公共的切入点做抽取, 方便下面的各个类型的通知方法引用;
PointCut的方法名可以随意取, 下面引用对即可: 如果是外部引用, 需要全路径的方法签名字符串! 本类中 简写的即可!
3. 方法加参数JoinPoint
必须是第一个;
4. JointPoint类常用方法
获取业务方法名: String bizMethodName = joinPoint.getSignature().getName();
获取业务方法入参: Object[] params = joinPoint.getArgs();
获取方法返回值, 需要 (1). @AfterReturning
指定属性returning
名, (2). 然后加到参数列表里, 在处理方法里调用即可!
获取方法异常, 需要 (1). @AfterThrowing
指定属性throwing
名, (2). 然后加到参数列表里, 在处理方法里调用即可!
step4: 业务类+切面类-注册到spring容器
1. @Bean 注册业务类到容器;
2. @Bean 注册切面类到容器;
3. @EnableAspectJAutoProxy 开启自动代理
配置类: AspectsConfig.java
package com.niewj.config;
import com.niewj.aop.BizRequest;
import com.niewj.aop.BizRequestLogAspects;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class AspectsConfig {
@Bean
public BizRequest bizRequest() {
return new BizRequest();
}
@Bean
public BizRequestLogAspects bizRequestLogAspects() {
return new BizRequestLogAspects();
}
}
测试用例1: 正常返回
@Test
public void testAopReturning() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AspectsConfig.class);
// 打印spring容器中的 BeanDefinition
Stream.of(ctx.getBeanDefinitionNames()).forEach(e -> System.out.println(e));
System.out.println("=============================");
// 正常返回的用例:
BizRequest bizRequest = ctx.getBean(BizRequest.class);
Map<String, String> params = new HashMap<>();
params.put("name", "niewj");
params.put("idCard", "142525199905051111");
params.put("phone", "15215152525");
bizRequest.callData(params);
ctx.close();
}
output: 主要输出: 11-14行
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
aspectsConfig
bizRequest
bizRequestLogAspects
org.springframework.aop.config.internalAutoProxyCreator
=============================
[@Before]==> {"bizMethodName":"callData","bizMethodParamsJson":"[{\"phone\":\"15215152525\",\"idCard\":\"142525199905051111\",\"name\":\"niewj\"}]","logTime":1595004597856,"logType":"BEFORE"}
User-初始化!
[@AfterReturning]==> {"bizMethodName":"callData","bizMethodParamsJson":"[{\"phone\":\"15215152525\",\"idCard\":\"142525199905051111\",\"name\":\"niewj\"}]","logTime":1595004597922,"logType":"AFTER_RETURNING","returnJson":"{\"name\":\"niewj\",\"passwd\":\"123456\",\"online\":true}"}
[@After]==> {"bizMethodName":"callData","logTime":1595004597924,"logType":"AFTER"}
测试用例2: 异常返回:
@Test
public void testAopThrowing() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AspectsConfig.class);
// 打印spring容器中的 BeanDefinition
Stream.of(ctx.getBeanDefinitionNames()).forEach(e -> System.out.println(e));
System.out.println("=============================");
// 正常返回的用例:
BizRequest bizRequest = ctx.getBean(BizRequest.class);
Map<String, String> leakParams = new HashMap<>();
leakParams.put("name", "niewj");
bizRequest.callData(leakParams);
ctx.close();
}
output: 11-15行
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
aspectsConfig
bizRequest
bizRequestLogAspects
org.springframework.aop.config.internalAutoProxyCreator
=============================
[@Before]==> {"bizMethodName":"callData","bizMethodParamsJson":"[{\"name\":\"niewj\"}]","logTime":1595004648830,"logType":"BEFORE"}
[@AfterThrowing]==> {"bizMethodName":"callData","bizMethodParamsJson":"[{\"name\":\"niewj\"}]","logTime":1595004648908,"logType":"AFTER_THROWING","exception":{"detailMessage":"name/idCard/phone三要素参数是必需的!","stackTrace":[],"suppressedExceptions":[]}}
[@After]==> {"bizMethodName":"callData","logTime":1595004648910,"logType":"AFTER"}
java.lang.IllegalArgumentException: name/idCard/phone三要素参数是必需的!
at com.niewj.aop.BizRequest.callData(BizRequest.java:18)
at com.niewj.aop.BizRequest$$FastClassBySpringCGLIB$$b4cd2786.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
..................
3. AOP小结:
主要步骤
1. 业务类(注册到容器) :@Bean
2. 切面类(标注切面+注册到容器): @Bean+@Aspect
3. 配置类(开启AspectJ自动代理) @EnableAspectJAutoProxy
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 hi@niewj.com