前言
前陣子朋友他老大叫他實現(xiàn)這么一個功能杨凑,就是低侵入的記錄接口每次的請求響應(yīng)日志敷扫,然后并統(tǒng)計每次請求調(diào)用的成功刽宪、失敗次數(shù)以及響應(yīng)耗時厘贼,當(dāng)時朋友的實現(xiàn)思路是在每個業(yè)務(wù)的controller的方法上加一個自定義注解,然后寫一個aop圣拄,以這個自定義注解為pointcut來記錄日志嘴秸。
這種AOP+注解來實現(xiàn)日志記錄,應(yīng)該是很常見的實現(xiàn)方式。然而朋友在落地的時候岳掐,發(fā)現(xiàn)項目要加自定義注解的地方太多凭疮。后面我就跟他說,那就不寫注解岩四,直接以形如下
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實現(xiàn)上面的需求
業(yè)務(wù)場景
低侵入的記錄接口每次的請求響應(yīng)日志熊咽,然后并統(tǒng)計每次請求調(diào)用的成功莫鸭、失敗次數(shù)以及響應(yīng)耗時
這個業(yè)務(wù)需求應(yīng)該算是很簡單,實現(xiàn)的難點就在于低侵入横殴,提到低侵入被因,我首先想到是使用者無需寫代碼,或者只需寫少量代碼或者僅需簡單配置一下衫仑,最好能做到業(yè)務(wù)無感知梨与。
實現(xiàn)手段
我這邊提供2種思路
- javaagent + byte-buddy
- springboot自動裝配 + AOP
javaagent
1、什么是javaagent
javaagent是一個簡單優(yōu)雅的java agent,利用java自帶的instrument特性+javassist/byte-buddy字節(jié)碼可以實現(xiàn)對類的攔截或者增強(qiáng)文狱。
javaAgent 是運(yùn)行在 main方法之前的攔截器粥鞋,它內(nèi)定的方法名叫 premain ,也就是說先執(zhí)行 premain 方法然后再執(zhí)行 main 方法
2瞄崇、如何實現(xiàn)一個javaagent
- a陷虎、必須實現(xiàn)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文檔杠袱,內(nèi)容形如下
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插件進(jìn)行生成,插件如下
<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楣富、業(yè)務(wù)代碼如何使用javagent
java -javaagent:agentjar文件的位置 [= 傳入 premain的參數(shù) ] -jar 要運(yùn)行的jar文件
注:-javaagent一定要在-jar之前凿掂,不然不會生效
byte-buddy
1、什么是byte-buddy
Byte Buddy是一個JVM的運(yùn)行時代碼生成器,你可以利用它創(chuàng)建任何類庄萎,且不像JDK動態(tài)代理那樣強(qiáng)制實現(xiàn)一個接口踪少。Byte Buddy還提供了簡單的API,便于手工糠涛、通過Java Agent援奢,或者在構(gòu)建期間修改字節(jié)碼
2、byte-buddy教程
注: 如果再介紹byte-buddy使用忍捡,則篇幅會比較長集漾,因此提供以下2個byte-buddy學(xué)習(xí)鏈接,感興趣的朋友可以點擊查看
https://blog.gmem.cc/byte-buddy-study-note
https://notes.diguage.com/byte-buddy-tutorial/
如何利用javaagent + byte-buddy實現(xiàn)低侵入記錄日志
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 {
// 原有函數(shù)執(zhí)行
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參數(shù)驱显,加入形如下內(nèi)容
-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實現(xiàn)低侵入記錄日志
注: 其實朋友那種方式也差不多可以了,只需把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 {
// 原有函數(shù)執(zhí)行
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熔恢、效果演示
在業(yè)務(wù)代碼做如下配置
- 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 效果圖
總結(jié)
以上主要列舉了通過javaagent和aop加自動裝配2兩種方式來實現(xiàn)低侵入記錄日志。其實這兩種實現(xiàn)在一些開源的方案用得挺多的臭笆,比如byte-buddy在skywalking和arthas就有使用到叙淌,比如MethodInterceptor 在spring事務(wù)中就有用到。所以多看些源碼愁铺,在設(shè)計方案時鹰霍,有時候會產(chǎn)生意想不到的火花
demo鏈接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-agent