springboot动态添加aop切面

需求:在不停止服务的情况下,通过上传一个jar包然后捕获某方法的异常进行处理

思路:

使用springaop实现

  • 定义一个切入点为service包下面的所以方法

  • 将jar文件加载到classLoader

  • 动态添加切入点到指定的方法

至于为什么要定义一个切入点到service包下面的所以方法,感兴趣的可以研究一下springAop的源码,里面有个postProcessBeforeInstantiation方法,会返回代理对象,如果没有则不会返回代理对象。
当然还有一种思路,就是在动态添加切入点的时候把spring容器中的对象替换成自己的代理对象(没有实验过,在非单例模式的时候有问题,这里不深入研究)。

引入aop的starter:

1
2
3
4
5
6

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第一步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

@Aspect
@Configuration
public class TestAop {
/*
* 定义一个大的切入点
*/
@Pointcut("execution(* com.cdinit.spring.demo..*(..))")
public void initAllAop(){}
@Before("initAllAop()")
public void initAllAop1(){
}
}

第二步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

//核心逻辑 实例化jar包里面的类
private Advice buildAdvice(PluginConfig config) throws Exception {

if (adviceCache.containsKey(config.getClassName())) {
return adviceCache.get(config.getClassName());
}

File jarFile = new File(config.getLocalUrl());

// 将本地jar 文件加载至 classLoader
URLClassLoader loader = (URLClassLoader) getClass().getClassLoader();
URL targetUrl = jarFile.toURI().toURL();
boolean isLoader = false;
for (URL url : loader.getURLs()) {
if (url.equals(targetUrl)) {
isLoader = true;
break;
}
}
if (!isLoader) {
Method add = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class });
add.setAccessible(true);
add.invoke(loader, targetUrl);
}
// Advice 实例化
Class<?> adviceClass = loader.loadClass(config.getClassName()); //上传的jar文件的类
if (!Advice.class.isAssignableFrom(adviceClass)) {
throw new RuntimeException("无法实例化非" + Advice.class.getName() + "的实例");
}
adviceCache.put(adviceClass.getName(), (Advice) adviceClass.newInstance());
return adviceCache.get(adviceClass.getName());
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

//核心逻辑 根据切入点动态切入
private ApplicationContext applicationContext; // 应用上下文
private Map<String, Advice> adviceCache = new HashMap<>();

private PluginConfig pluginConfig = new PluginConfig()
.setId("1")
.setName("test")
.setClassName("CountingBeforeAdvice")
.setLocalUrl("E:\\aop-fix-zero\\target\\aop-fix-zero-1.0-SNAPSHOT.jar")
.setActive("true")
// .setExp("execution(* *.test(..))") // 加入切入点到切面
.setExp("execution(* test(..))")
.setVersion("1.0");

public void activePlugin(String pluginId) {

PluginConfig config = pluginConfig; // TODO 这里应该从数据库里面查询config的配置

for (String name : applicationContext.getBeanDefinitionNames()) {
Object bean = applicationContext.getBean(name);
if (bean == this)
continue;
if (!(bean instanceof Advised)) // 如果bean不是Advised类型则跳过
continue;
if (findAdvice(config.getClassName(), (Advised) bean) != null) // 如果bean已经注册了Advised则跳过
continue;

Advice advice = null;
try {
advice = buildAdvice(config); //初始化 Plugin Advice 实例化
// 包一层 advisor
AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
advisor.setExpression(config.getExp());
advisor.setAdvice(advice);
((Advised) bean).addAdvisor(advisor);
} catch (Exception e) {
throw new RuntimeException("安装失败", e);
}
}
}

private Advice findAdvice(String className, Advised advised) {
for (Advisor a : advised.getAdvisors()) {
if (a.getAdvice().getClass().getName().equals(className)) {
return a.getAdvice();
}
}
return null;
}


jar包怎么写?只需要实现对应的切面方法就行了

1
2
3
4
5
6
7
8
9
10
11
12

public class ServerLogPlugin implements MethodBeforeAdvice {

public void before(Method method, Object[] args, Object target) throws Throwable {
String result = String.format("%s.%s() 参数:%s", method.getDeclaringClass().getName(), method.getName(),
Arrays.toString(args));
System.out.println(result);
}

}


通常有方法前拦截,方法后拦截,以及异常拦截。通过在这些拦截中编写自己的业务处理,可以达到特定的需求。

  • MethodBeforeAdvice

  • MethodBeforeAdvice

  • ThrowsAdvice

execution表达式

1
2
3
4
5
6
7
8
9

匹配所有类public方法  execution(public * *(..))
匹配指定包下所有类方法 execution(* com.baidu.dao.*(..)) 不包含子包
execution(* com.baidu.dao..*(..))  ..*表示包、子孙包下所有类
匹配指定类所有方法 execution(* com.baidu.service.UserService.*(..))
匹配实现特定接口所有类方法
    execution(* com.baidu.dao.GenericDAO+.*(..))
匹配所有save开头的方法 execution(* save*(..))