动态创建 AOP
动态创建 AOP
场景
有些情况下,生产环境查看问题,有些公司不能用 Arthas,需要查看方法参数及请求日志,又不方便加代码重新发布,现在通过给代码动态增加 AOP 功能,实现可以在不影响现有代码情况下查看
解决方案
动态加载 jar 包,并加载 class , 动态增加 AOP 切面增强
aop-plugin 管理
AopPluginController
@RestController
public class AopPluginController {
@Autowired
private AopMetaInfoRepository aopMetaInfoRepository;
/**
* 查看 aop-plugin 列表
* @return
*/
@GetMapping("/aop/list")
public List<AopMetaInfo> list() {
return aopMetaInfoRepository.values();
}
/**
* 保存
* @param metaInfo
* @return
*/
@PostMapping("/aop/save")
public Boolean save(@RequestBody AopMetaInfo metaInfo){
aopMetaInfoRepository.add(metaInfo);
return Boolean.TRUE;
}
/**
* 删除
* @param id
* @return
*/
@GetMapping("/aop/{id}")
public Boolean delete(@PathVariable String id){
aopMetaInfoRepository.delete(id);
return Boolean.TRUE;
}
}
插件定义 AopMetaInfo:
@Data
public class AopMetaInfo {
/**
* jar包地址,本文文件 jar ,或者 http 请求
*/
private String proxyJar;
/**
* 增强类
*/
private String proxyClassName;
/**
* 切点
*/
private String pointCut;
/**
* id名称
*/
private String id;
/**
* 加载 jar包方式
* 0、本地
* 1、远程
*/
private Boolean remote = Boolean.FALSE;
}
插件存储 AopMetaInfoRepository:
@Slf4j
@Repository
public class AopMetaInfoRepository {
/**
* Aop 插件
*/
private Map<String, AopMetaInfo> map = new ConcurrentHashMap<>();
/**
* 已加载的 class
*/
private static Map<String, Class<?>> loadClass = new ConcurrentHashMap<>();
@Autowired
private ApplicationEventPublisher publisher;
public void add(AopMetaInfo metaInfo) {
try {
if (map.containsKey(metaInfo.getId())) {
log.info("aop plugin exists: {}", metaInfo.getId());
return;
}
map.put(metaInfo.getId(), metaInfo);
if (!metaInfo.getRemote()) {
//从文件系统加载jar
loadLocalJar(metaInfo,new File(metaInfo.getProxyJar()));
} else {
File tempFile = null;
try {
HttpGet httpGet = new HttpGet();
httpGet.setURI(URI.create(metaInfo.getProxyJar()));
CloseableHttpResponse execute = HttpClients.createDefault().execute(httpGet);
byte[] byteArray = EntityUtils.toByteArray(execute.getEntity());
tempFile = File.createTempFile(metaInfo.getId(), ".jar");
FileUtil.writeBytes(byteArray, tempFile);
loadLocalJar(metaInfo,tempFile);
} finally {
if (tempFile!=null){
tempFile.delete();
}
}
}
publisher.publishEvent(new AopMetaChangeEvent(this, metaInfo.getId(), DynamicAopHelper.AopType.ADD));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 加载本地jar
* @param metaInfo
* @param jarFile
* @throws Exception
*/
private static void loadLocalJar(AopMetaInfo metaInfo, File jarFile) throws Exception {
URLClassLoader classLoader = new URLClassLoader(new URL[]{jarFile.toURI().toURL()},
Thread.currentThread().getContextClassLoader());
Class<?> clz = classLoader.loadClass(metaInfo.getProxyClassName());
loadClass.put(metaInfo.getProxyClassName(), clz);
}
public static Class<?> getById(String clzName) {
return loadClass.get(clzName);
}
public AopMetaInfo get(String id) {
return map.get(id);
}
public List<AopMetaInfo> values() {
return new ArrayList<>(map.values());
}
public void delete(String id) {
map.remove(id);
publisher.publishEvent(new AopMetaChangeEvent(this, id, DynamicAopHelper.AopType.DEL));
}
}
插件操作监听
public class AopMetaChangeEvent extends ApplicationEvent {
@Getter
private String id;
@Getter
private DynamicAopHelper.AopType aopType;
public AopMetaChangeEvent(Object source, String id, DynamicAopHelper.AopType aopType) {
super(source);
this.id = id;
this.aopType = aopType;
}
}
@Component
@Slf4j
public class AopMetaInfoChangeListener {
@Autowired
private DefaultListableBeanFactory beanFactory;
@EventListener(classes = AopMetaChangeEvent.class)
public void change(AopMetaChangeEvent event) {
log.info("recv event 【{}】 aop plugin id: {}", event.getAopType(), event.getId());
switch (event.getAopType()) {
case ADD:
AopMetaInfoRepository repository = (AopMetaInfoRepository) event.getSource();
AopMetaInfo aopMetaInfo = repository.get(event.getId());
DynamicAopHelper.saveAdvisor(beanFactory, aopMetaInfo);
break;
case DEL:
DynamicAopHelper.removeAdvisor(beanFactory, event.getId());
break;
}
}
}
Aop 操作 DynamicAopHelper:
@Slf4j
public class DynamicAopHelper {
public static enum AopType {
ADD, DEL
}
/**
* Advised: 管理 Advisor
* Advisor: 由 point_cut 和 Advice 组成
* Advice: 增强代码, 如实现 MethodInterceptor
* 创建 advisor
* @param metaInfo
* @param beanFactory
* @return
*/
public static AspectJExpressionPointcutAdvisor getAdvisor(AopMetaInfo metaInfo, DefaultListableBeanFactory beanFactory) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition();
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getBeanDefinition();
beanDefinition.setBeanClass(AspectJExpressionPointcutAdvisor.class);
AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
advisor.setExpression(metaInfo.getPointCut());
advisor.setAdvice(createAdviceInstance(metaInfo.getProxyClassName()));
//提供构造函数
beanDefinition.setInstanceSupplier(() -> advisor);
beanFactory.registerBeanDefinition(metaInfo.getId(), beanDefinition);
return advisor;
}
/**
* 根据 class 创建 Advice
* @param className
* @return
*/
private static MethodInterceptor createAdviceInstance(String className) {
try {
// 动态加载类
Constructor<?> constructor = AopMetaInfoRepository.getById(className).getConstructor();
Object instance = constructor.newInstance();
if (instance instanceof MethodInterceptor) {
return (MethodInterceptor) instance;
} else {
throw new IllegalArgumentException("The provided class does not implement MethodInterceptor.");
}
} catch (Exception e) {
throw new RuntimeException("Failed to create advice instance: " + className, e);
}
}
/**
* 新增 advisor
*
* @param metaInfo
* @param beanFactory
*/
public static void saveAdvisor(DefaultListableBeanFactory beanFactory, AopMetaInfo metaInfo) {
AspectJExpressionPointcutAdvisor advisor = getAdvisor(metaInfo, beanFactory);
//查询 Advised, 管理 advisor
//修改 advisor
processAdvisor(beanFactory, advisor, AopType.ADD);
}
/**
* 删除 advisor
*
* @param beanFactory
* @param id
*/
public static void removeAdvisor(DefaultListableBeanFactory beanFactory, String id) {
if (beanFactory.containsBean(id)) {
AspectJExpressionPointcutAdvisor advisor = beanFactory.getBean(id, AspectJExpressionPointcutAdvisor.class);
processAdvisor(beanFactory, advisor, AopType.DEL);
}
}
private static void processAdvisor(DefaultListableBeanFactory beanFactory, AspectJExpressionPointcutAdvisor advisor, AopType type) {
for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
Object bean = beanFactory.getBean(beanDefinitionName);
if (!(bean instanceof Advised)) {
continue;
}
Advised advisedObj = (Advised) bean;
//pointcut 是否应用于 class
boolean b = AopUtils.canApply(advisor, bean.getClass());
if (!b){
continue;
}
if (type == AopType.ADD) {
advisedObj.addAdvice(advisor.getAdvice());
log.info(">>>>>>>> add advice 【{}】 for 【{}】", advisor.getAdvice().getClass().getName(), advisedObj.getClass().getName());
} else if (type == AopType.DEL) {
//如果存在则移除
if (findInMatch(advisedObj, advisor)) {
advisedObj.removeAdvice(advisor.getAdvice());
log.info(">>>>>>>> remove advice 【{}】 for 【{}】", advisor.getAdvice().getClass().getName(), advisedObj.getClass().getName());
}
}
}
}
/**
* advisor 是否在 Advised 中
* @param advisedObj
* @param advisor
* @return
*/
private static boolean findInMatch(Advised advisedObj, AspectJExpressionPointcutAdvisor advisor) {
for (Advisor advisedObjAdvisor : advisedObj.getAdvisors()) {
if (advisedObjAdvisor.getAdvice().equals(advisor.getAdvice())) {
return true;
}
}
return false;
}
}
创建增强类
创建 一个 maven 工程,自定义增强类,这里工程名称为 aop-svc
@Slf4j
public class HelloInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object rs = invocation.proceed();
stopWatch.stop();
String name = invocation.getMethod().getName();
log.info("method: 【{}】, 耗时: {} ms", name, stopWatch.getTotalTimeMillis());
return rs;
}
}
这里实现增强的逻辑是打印切点方法执行的耗时时间,然后打包,包名为 aop-svc.jar
测试
在目标web工程, 如 user-boot 项目,有以下接口:
@RestController
public class HealthController {
@GetMapping("hello")
public String index() throws InterruptedException {
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
return "hello";
}
}
动态增加统计接口耗时时,我们就可以添加我们的aop插件了
http://localhost:8080/aop/save
,我们从文件系统加载,在生产环境可以把 jar 换成 可以http下载的文件:
{
"proxyJar": "D:\\aop-svc.jar",
//"proxyJar": "http://172.xx.x.:9001/api/v1/download-shared-object/aHR0cDovLzEyNy4wLjAuMTo",
"proxyClassName": "org.example.act.interceptor.HelloInterceptor",//增强类
"pointCut": "execution(* com.xx.controller.HealthController.*(..))",//切点
"remote": 0,
"id": "aop-svc"
}
日志输出:
recv event 【ADD】 aop plugin id: aop-svc
add advice 【org.HelloInterceptor】 fo【com.x.HealthController$$EnhancerBySpringCGLIB$$70c5596c】
查看已经加载的插件,http://localhost:8080/aop/list
{
"code": "1",
"data": [
{
"id": "aop-svc",
"pointCut": "execution(* com.xx.controller.HealthController.*(..))",
"proxyClassName": "org.example.act.interceptor.HelloInterceptor",
"proxyJar": "D:\\aop-svc.jar",
"remote": false
}
],
"link": [],
"msg": "调用成功"
}
请求我们目标接口 http://localhost:8080/hello
,输出:
o.e.act.interceptor.HelloInterceptor : method: 【index】, 耗时: 4002 ms
显然,我们的增强添加成功
通过 http://localhost:8080/aop/aop-svc
删除 加载的插件, 其中 aop-svc
是插件的 id, 输出:
recv event 【DEL】 aop plugin id: aop-svc
remove advice 【org.x.HelloInterceptor】 for【com.x.HealthController$$EnhancerBySpringCGLIB$$75596c】
这样就删除了,既实现了增强,又减少了对代码的入侵
good luck!