赞
踩
最近项目(OSGI框架,目前在做与Spring的兼容)在运行过程中发现日志过大,撑爆硬盘,在随后的分析中,发现是由于之前在项目的接口性能分析中,日志埋点过多,所以在某个业务并发量过大时,出现日志过多,所以考虑是否控制仅在需要进行性能分析时,才进行耗时的打印,而平常正常业务执行时,不记录日志信息。
本实践主要原理是利用Spring提供的AOP能力,实现所谓热插拔的能力,高手绕行!
该工程为简单的一个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; } }
业务工程中,提供了四个接口,分别是:
该工程是一个简单的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多个支持热插拔的插件,在运行期启用关闭相关业务功能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。