一、需求背景
最近项目(OSGI框架,目前在做与Spring的兼容)在运行过程中发现日志过大,撑爆硬盘,在随后的分析中,发现是由于之前在项目的接口性能分析中,日志埋点过多,所以在某个业务并发量过大时,出现日志过多,所以考虑是否控制仅在需要进行性能分析时,才进行耗时的打印,而平常正常业务执行时,不记录日志信息。
二、实践代码
本实践主要原理是利用Spring提供的AOP能力,实现所谓热插拔的能力,高手绕行!
1.插件工程
该工程为简单的一个maven工程,由于使用Spring的AOP切面功能,所以需要引入相关的依赖。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.3</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
功能实现是利用环绕通知:
public class MethodCostAdvise implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
long current = System.currentTimeMillis();
Object proceed = null;
try {
proceed = methodInvocation.proceed();
} catch (Throwable e) {
throw e;
} finally {
String costString = String.format("[%s] invoke cost:%d ms.", methodInvocation.getMethod().getName(), System.currentTimeMillis() - current);
System.out.println(costString);
}
return proceed;
}
}
2.业务工程
业务工程中,提供了四个接口,分别是:
- http://localhost:8080/active/plugin/1 激活指定插件
- http://localhost:8080/deActive/plugin/1 去激活指定插件
- http://localhost:8080/dog/shut-up 模拟业务接口
- http://localhost:8080/list/plugin 插件明细查询
该工程是一个简单的SpringBoot的web应用,首先是插件信息加载:
plugins.list[0].id=1 编号
plugins.list[0].cls=com.zte.sdn.plugin.method.MethodCostAdvise 插件类名
plugins.list[0].name=Time Cost by Method 插件名
plugins.list[0].enable=false 插件状态
plugins.list[0].jar=method-time-cost-plugin-1.0.0-SNAPSHOT.jar 插件JAR包
@Data
@Configuration
@ConfigurationProperties(prefix = "plugins")
public class PluginConfig {
private List<Plugin> list;
}
@Data
@ToString
public class Plugin {
private int id;
private String cls;
private String name;
private boolean enable;
private String jar;
}
基于AOP进行热插拔进行进行,其前提是Bean是可以插拔的,即是Advise,所以需要定义切面和切入点
@Aspect
@Component
public class DogAspect {
@Pointcut("execution(public * com.zte.sdn.dog.web.contorl..*.*(..))")
public void as() {
}
@Before("as()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
}
}
Rest实现,具体参考注释
package com.zte.sdn.dog.web.rest;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.zte.sdn.dog.web.config.Plugin;
import com.zte.sdn.dog.web.config.PluginConfig;
import com.zte.sdn.dog.web.contorl.DogShutUp;
@Slf4j
@RestController
public class PluginControlRest {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private PluginConfig pluginConfig;
@Autowired
private DogShutUp dogShutUp;
private String pluginDir = "file:E:/00code/";
private Map<Integer, Advice> adviceMap = new HashMap<>();
@GetMapping("/list/plugin")
public String getAllPlugins() {
return pluginConfig.getList().toString();
}
@GetMapping("/active/plugin/{id}")
public boolean activePlugin(@PathVariable(value = "id") int id) {
Optional<Plugin> pluginOpt = pluginConfig.getList().stream().filter(p -> p.getId() == id).findFirst();
if (!pluginOpt.isPresent()) {
throw new RuntimeException("not find [" + id + "] plugin config");
}
Plugin plugin = pluginOpt.get();
//查询所有Bean实例,并判断是否已被切面过
for (String bdn : applicationContext.getBeanDefinitionNames()) {
Object bean = applicationContext.getBean(bdn);
if (bean == this || !(bean instanceof Advised)) {
continue;
}
//可增强 且未增加过当前指定的advice
if (!hasAdviced((Advised) bean, plugin.getId())) {
//借助JAR包装配并构建Advice
Advice advice = wrapAdviseById(plugin);
if (advice != null) {
((Advised) bean).addAdvice(advice);
} else {
return false;
}
} else {
return false;
}
}
return true;
}
@GetMapping("/deActive/plugin/{id}")
public boolean deActivePlugin(@PathVariable(value = "id") int id) {
for (String bdn : applicationContext.getBeanDefinitionNames()) {
Object bean = applicationContext.getBean(bdn);
if (bean == this || !(bean instanceof Advised)) {
continue;
}
if (adviceMap.containsKey(id)) {
((Advised) bean).removeAdvice(adviceMap.get(id));
}
}
return true;
}
@GetMapping("/dog/shut-up")
public String dogShutUp() {
return dogShutUp.shutUp();
}
private boolean hasAdviced(Advised bean, int id) {
Advice advice = adviceMap.get(id);
if (advice != null) {
for (Advisor advisor : ((Advised) bean).getAdvisors()) {
if (advisor.getAdvice() == advice) {
return true;
}
}
}
return false;
}
//根据插件信息查找构建advice
private Advice wrapAdviseById(Plugin plugin) {
int id = plugin.getId();
if (adviceMap.containsKey(id)) {
return adviceMap.get(id);
}
try {
boolean loadJar = false;
//定位到JAR文件位置
URL url = new URL(pluginDir + plugin.getJar());
URLClassLoader loader = (URLClassLoader) getClass().getClassLoader();
for (URL loaderURL : loader.getURLs()) {
if (loaderURL.equals(url)) {
loadJar = true;
break;
}
}
if (!loadJar) {
Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class});
addURL.setAccessible(true);
addURL.invoke(loader, url);
}
//加载MethodInterceptor类
Class<?> aClass = loader.loadClass(plugin.getCls());
adviceMap.put(id, (Advice) aClass.newInstance());
return adviceMap.get(id);
} catch (Exception e) {
log.error("wrap advise error.", e);
}
return null;
}
}
三、演示效果
执行正常调用:
执行加载插件:
再次执行接口调用,控制台会记录并打印接口调用的时间戳
去激活插件后,接口调用耗时打印随即消失
通过上述的思路与实现,可以定义N多个支持热插拔的插件,在运行期启用关闭相关业务功能。