共计 9018 个字符,预计需要花费 23 分钟才能阅读完成。
前言
前阵子敌人他老大叫他实现这么一个性能,就是低侵入的记录接口每次的申请响应日志,而后并统计每次申请调用的胜利、失败次数以及响应耗时,过后敌人的实现思路是在每个业务的 controller 的办法上加一个自定义注解,而后写一个 aop,以这个自定义注解为 pointcut 来记录日志。
这种 AOP+ 注解来实现日志记录,应该是很常见的实现形式。然而敌人在落地的时候,发现我的项目要加自定义注解的中央太多。前面我就跟他说,那就不写注解,间接以形如下
execution(* com.github.lybgeek.logaop.service..*.*(..))
这样不行吗?他说他这个性能他老大是心愿给各个项目组应用,像我下面的办法,预计行不通,我就问他说为啥行不通,他说各个我的项目的包名都不一样,如果我那种思路,他就说这样在代码里 poincut 不得要这么写
execution(* com.github.lybgeek.a.service..*.*(..)
|| * com.github.lybgeek.b.service..*.*(..) || * com.github.lybgeek.c.service..*.*(..) )
这样每次新加要日志记录,都得改切面代码,还不如用自定注解来的好。听完他的解释,我一脸黑人问号脸。于是就趁着 5.1 假期期间,写个 demo 实现下面的需要
业务场景
低侵入的记录接口每次的申请响应日志,而后并统计每次申请调用的胜利、失败次数以及响应耗时
这个业务需要应该算是很简略,实现的难点就在于 低侵入,提到低侵入,我首先想到是使用者无需写代码,或者只需写大量代码或者仅需简略配置一下,最好能做到业务无感知。
实现伎俩
我这边提供 2 种思路
- javaagent + byte-buddy
- springboot 主动拆卸 + AOP
javaagent
1、什么是 javaagent
javaagent 是一个简略优雅的 java agent, 利用 java 自带的 instrument 个性 +javassist/byte-buddy 字节码能够实现对类的拦挡或者加强。
javaAgent 是运行在 main 办法之前的拦截器,它内定的办法名叫 premain,也就是说先执行 premain 办法而后再执行 main 办法
2、如何实现一个 javaagent
- a、必须实现 premain 办法
示例:
public class AgentDemo {public static void premain(String agentArgs, Instrumentation inst) {System.out.println("agentArgs :" + agentArgs);
inst.addTransformer(new DefineTransformer(),true);
}
static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {System.out.println("premain load Class:" + className);
return classfileBuffer;
}
}
}
-
b、在 META-INF 目录增加 MANIFEST.MF 文档,内容形如下
Manifest-Version: 1.0 Implementation-Version: 0.0.1-SNAPSHOT Premain-Class: com.github.lybgeek.agent.ServiceLogAgent Can-Redefine-Classes: true
其中 Premain-Class 是必选项。MANIFEST.MF 能够利用 maven 插件进行生成,插件如下
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.github.lybgeek.agent.ServiceLogAgent</Premain-Class>
<Agent-Class>com.github.lybgeek.agent.ServiceLogAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
3、业务代码如何应用 javagent
java -javaagent:agentjar 文件的地位 [= 传入 premain 的参数] -jar 要运行的 jar 文件
注:-javaagent 肯定要在 -jar 之前,不然不会失效
byte-buddy
1、什么是 byte-buddy
Byte Buddy 是一个 JVM 的运行时代码生成器,你能够利用它创立任何类,且不像 JDK 动静代理那样强制实现一个接口。Byte Buddy 还提供了简略的 API,便于手工、通过 Java Agent,或者在构建期间批改字节码
2、byte-buddy 教程
注: 如果再介绍 byte-buddy 应用,则篇幅会比拟长,因而提供以下 2 个 byte-buddy 学习链接,感兴趣的敌人能够点击查看
https://blog.gmem.cc/byte-buddy-study-note
https://notes.diguage.com/byte-buddy-tutorial/
如何利用 javaagent + byte-buddy 实现低侵入记录日志
1、编写 agent 入口类
public class ServiceLogAgent {
public static String base_package_key = "agent.basePackage";
public static void premain(String agentArgs, Instrumentation inst) {System.out.println("loaded agentArgs:" + agentArgs);
Properties properties = PropertiesUtils.getProperties(agentArgs);
ServiceLogHelperFactory serviceLogHelperFactory = new ServiceLogHelperFactory(properties);
serviceLogHelperFactory.getServiceLogHelper().initTable();
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
return builder
.method(ElementMatchers.<MethodDescription>any()) // 拦挡任意办法
.intercept(MethodDelegation.to(new ServiceLogInterceptor(serviceLogHelperFactory))); // 委托
};
AgentBuilder.Listener listener = new AgentBuilder.Listener() {private Log log = LogFactory.getLog(AgentBuilder.Listener.class);
@Override
public void onDiscovery(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) { }
@Override
public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b, DynamicType dynamicType) { }
@Override
public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule, boolean b) { }
@Override
public void onError(String s, ClassLoader classLoader, JavaModule javaModule, boolean b, Throwable throwable) {log.error(throwable.getMessage(),throwable);
}
@Override
public void onComplete(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {}};
new AgentBuilder
.Default()
// 指定须要拦挡的类
.type(ElementMatchers.nameStartsWith(properties.getProperty(base_package_key)))
.and(ElementMatchers.isAnnotatedWith(Service.class))
.transform(transformer)
.with(listener)
.installOn(inst);
}
}
2、编写拦截器
public class ServiceLogInterceptor {private Log log = LogFactory.getLog(ServiceLogInterceptor.class);
private ServiceLogHelperFactory serviceLogHelperFactory;
public ServiceLogInterceptor(ServiceLogHelperFactory serviceLogHelperFactory) {this.serviceLogHelperFactory = serviceLogHelperFactory;}
@RuntimeType
public Object intercept(@AllArguments Object[] args, @Origin Method method, @SuperCall Callable<?> callable) {long start = System.currentTimeMillis();
long costTime = 0L;
String status = ServiceLog.SUCEESS;
Object result = null;
String respResult = null;
try {
// 原有函数执行
result = callable.call();
respResult = JsonUtils.object2json(result);
} catch (Exception e){log.error(e.getMessage(),e);
status = ServiceLog.FAIL;
respResult = e.getMessage();} finally{costTime = System.currentTimeMillis() - start;
saveLog(args, method, costTime, status, respResult);
}
return result;
}
private void saveLog(Object[] args, Method method, long costTime, String status, String respResult) {if(!isSkipLog(method)){ServiceLog serviceLog = serviceLogHelperFactory.createServiceLog(args,method);
serviceLog.setCostTime(costTime);
serviceLog.setRespResult(respResult);
serviceLog.setStatus(status);
ServiceLogHelper serviceLogHelper = serviceLogHelperFactory.getServiceLogHelper();
serviceLogHelper.saveLog(serviceLog);
}
}
private boolean isSkipLog(Method method){ServiceLogProperties serviceLogProperties = serviceLogHelperFactory.getServiceLogProperties();
List<String> skipLogServiceNameList = serviceLogProperties.getSkipLogServiceNameList();
if(!CollectionUtils.isEmpty(skipLogServiceNameList)){String currentServiceName = method.getDeclaringClass().getName() + ServiceLogProperties.CLASS_METHOD_SPITE + method.getName();
return skipLogServiceNameList.contains(currentServiceName);
}
return false;
}
}
3、通过 maven 将 agent 打包成 jar
4、成果演示
首先 idea 在启动类的 vm 参数,退出形如下内容
-javaagent:F:\springboot-learning\springboot-agent\springboot-javaagent-log\target\agent-log.jar=F:\springboot-learning\springboot-agent\springboot-javaagent-log\target\classes\agent.properties
效果图
如何利用主动拆卸 +AOP 实现低侵入记录日志
注: 其实敌人那种形式也差不多能够了,只需把 poincut 的外移到配置文件文件即可
1、编写切面
@Slf4j
public class ServiceLogAdvice implements MethodInterceptor {
private LogService logService;
public ServiceLogAdvice(LogService logService) {this.logService = logService;}
@Override
public Object invoke(MethodInvocation invocation) {long start = System.currentTimeMillis();
long costTime = 0L;
String status = ServiceLog.SUCEESS;
Object result = null;
String respResult = null;
try {
// 原有函数执行
result = invocation.proceed();
respResult = JSON.toJSONString(result);
} catch (Throwable e){log.error(e.getMessage(),e);
status = ServiceLog.FAIL;
respResult = e.getMessage();} finally{costTime = System.currentTimeMillis() - start;
saveLog(invocation.getArguments(), invocation.getMethod(), costTime, status, respResult);
}
return result;
}
private void saveLog(Object[] args, Method method, long costTime, String status, String respResult) {ServiceLog serviceLog = ServiceLog.builder()
.serviceName(method.getDeclaringClass().getName())
.costTime(costTime)
.methodName(method.getName())
.status(status)
.reqArgs(JSON.toJSONString(args))
.respResult(respResult).build();
logService.saveLog(serviceLog);
}
}
2、注入切面 bean
@Bean
@ConditionalOnMissingBean
public AspectJExpressionPointcutAdvisor serviceLogAspectJExpressionPointcutAdvisor(AopLogProperties aopLogProperties) {AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
advisor.setExpression(aopLogProperties.getPointcut());
advisor.setAdvice(serviceLogAdvice());
return advisor;
}
3、编写主动拆卸类
@Configuration
@EnableConfigurationProperties(AopLogProperties.class)
@ConditionalOnProperty(prefix = "servicelog",name = "enabled",havingValue = "true",matchIfMissing = true)
public class AopLogAutoConfiguration {
@Autowired
private JdbcTemplate jdbcTemplate;
@Bean
@ConditionalOnMissingBean
public LogService logService(){return new LogServiceImpl(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean
public ServiceLogAdvice serviceLogAdvice(){return new ServiceLogAdvice(logService());
}
@Bean
@ConditionalOnMissingBean
public AspectJExpressionPointcutAdvisor serviceLogAspectJExpressionPointcutAdvisor(AopLogProperties aopLogProperties) {AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
advisor.setExpression(aopLogProperties.getPointcut());
advisor.setAdvice(serviceLogAdvice());
return advisor;
}
}
4、编写 spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.lybgeek.logaop.config.AopLogAutoConfiguration
5、成果演示
在业务代码做如下配置
- 5.1 在 pom.xml 引入 starter
<dependency>
<groupId>com.github.lybgeek</groupId>
<artifactId>aoplog-springboot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 5.2 在 yml 文件中配置 pointcut
servicelog:
pointcut: execution(* com.github.lybgeek.mock.service.client..*.*(..))
enabled: true
- 5.3 效果图
总结
以上次要列举了通过 javaagent 和 aop 加主动拆卸 2 两种形式来实现低侵入记录日志。其实这两种实现在一些开源的计划用得挺多的,比方 byte-buddy 在 skywalking 和 arthas 就有应用到,比方 MethodInterceptor 在 spring 事务中就有用到。所以多看些源码,在设计方案时,有时候会产生意想不到的火花
demo 链接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-agent