bytebuddy解決spring AOP切面同一個類嵌套方法不生效問題
sping AOP切面註解使用中的坑中詳細介紹了spring AOP切面註解的同一個類嵌套方法不生效問題和產生的原因,這篇實際是爲了完美的解決打印方法運行時間的問題。
bytebuddy是字節碼生成庫,可以生成和操作java的字節碼,在Java應用程序的運行期間可以使用該字節碼工程庫來修改當前要執行的代碼,甚至包括自己的代碼。 由於是使用植入字節碼,而不是spring AOP的代理的模式,所以不會有嵌套不生效的問題。
1.首先在一個項目pom.xml中引入bytebuddy:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.5.7</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.5.7</version>
</dependency>
2.接着需要書寫功能代碼,和使用spring AOP的方式一樣,需要先聲明一個註解:
//@TimeLog註解
package com;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(value = RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface TimeLog {
}
3.然後實現這個註解的“切面”邏輯(bytebuddy的advice相當於是切面,其實也可以使用另一種interceptor實現):
package com;
import com.LoggingUtils
/*
*LoggingUtils自己寫的一個輸出切點類日誌,而不是當前切面類的log小工具。
*可以修改爲一個普通log或者控制檯打印方法。後面我會附LoggingUtils代碼,介紹原因,給大家研究。
*/
import net.bytebuddy.asm.Advice;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import java.lang.reflect.Method;
public class TimeLogAdvice {
@Advice.OnMethodEnter //方法進入前記錄開始時間
static long enter(@Advice.AllArguments Object args[], @Advice.Origin Method method) {
return System.currentTimeMillis();
}
//方法退出後,結算運行時間,並按照格式輸出
@Advice.OnMethodExit(onThrowable = Exception.class)
static void exit(@Advice.Enter long startTime,
@Advice.Return(typing=Assigner.Typing.DYNAMIC) Object result,
@Advice.Origin Method method,
@Advice.Thrown Throwable t
) {
if(t!=null){
LoggingUtils.error(method.getDeclaringClass(), method.getName(),t);
}else{
LoggingUtils.info(method.getDeclaringClass(), "[TIMELOG][{}.{}()]: {} ms", method.getDeclaringClass(),
method.getName(), (System.currentTimeMillis() - startTime));
}
}
}
4.和spring AOP不同的時,bytebuddy需要定義注入規則,一般是通過AgentBuilder方法,生成一個Agent。如:
package com;
import com.LoggingUtils;
import com.TimeLogAdvice;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
public class Agent {
//生成一個agent單例
private static Agent instance = new Agent();
private Agent() {}
public static Agent getInstance(){ return instance; }
public void install() {
synchronized (Agent.class) {
ByteBuddyAgent.install();
new AgentBuilder.Default().ignore( //忽略的包,這些包不會注入
nameStartsWith("org.aspectj."))
.or(nameStartsWith("org.groovy."))
.or(nameStartsWith("com.sun."))
.or(nameStartsWith("sun."))
.or(nameStartsWith("jdk."))
.or(nameStartsWith("org.springframework.asm."))
.or(nameStartsWith("com.p6spy."))
.or(nameStartsWith("net.bytebuddy."))
.type(ElementMatchers.nameStartsWith("com.")) //以com.開頭的包會注入
//定義規則,將TimeLogAdvice和註解TimeLog綁定
.transform((builder, type, classLoader, module) ->
builder.visit(Advice.to(TimeLogAdvice.class).on(ElementMatchers.isAnnotatedWith(named("com.TimeLog")))))
.with(new AgentBuilder.Listener(){
@Override
public void onDiscovery(String s, ClassLoader classLoader, JavaModule javaModule, boolean b) {
}
@Override
public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded, DynamicType dynamicType) {
}
@Override
public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, boolean loaded) {
}
@Override
public void onError(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded, Throwable throwable) {
LoggingUtils.error(Agent.class,typeName,throwable);
}
@Override
public void onComplete(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {
}
})
.installOnByteBuddyAgent();
LoggingUtils.info(Agent.class, "agent install finished");
}
}
}
5.最後就是需要是合適的時機注入,一般要在程序初始化的時候,太晚是不行的。網上有很多都是在premain中,我覺得不太方便。如果使用spring mvc的,我建議監聽ApplicationEnvironmentPreparedEvent,在這個事件發生時也是可以注入成功的。例:
package com;
import com.LoggingUtils;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;
public class AppEnvListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent applicationEnvironmentPreparedEvent) {
Agent.getInstance().install(); //監聽程序準備事件,注入
}
}
就是不要忘了這個監聽器是需要在spring.factories裏註冊的。
日誌打印模擬結果:
[INFO][2019-11-17][com.TestController:37] [TIMELOG][class com.TestController.test()]: 22 ms
[INFO][2019-11-17][com.TestController:37] [TIMELOG][class com.TestController.test2()]: 68 ms
最後附上LoggingUtils實現,這個類的主要作用是,如果單純使用log.info打印,顯示的類會一直是TimeLogAdvice,比如上例就不會是com.TestController,而這個方法運行時間的註解顯然是想確定被註解的方法的類名,所以需要一個小技巧轉化一下:
package com;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoggingUtils {
private static Logger logger = LoggerFactory.getLogger(LoggingUtils.class);
public static boolean isDebugEnabled(){
return logger.isDebugEnabled();
}
public static boolean isInfoEnabled(){
return logger.isInfoEnabled();
}
public static boolean isErrorEnabled(){
return logger.isErrorEnabled();
}
public static void debug(Class clazz, String msg, Object... args) {
if (clazz != null) {
logger = LoggerFactory.getLogger(clazz);
}
logger.debug(msg, args);
}
public static void info(Class clazz, String msg, Object... args) {
if (clazz != null) {
logger = LoggerFactory.getLogger(clazz);
}
logger.info(msg, args);
}
public static void error(Class clazz,String msg,Throwable throwable) {
if (clazz != null) {
logger = LoggerFactory.getLogger(clazz);
}
logger.error(msg, throwable);
}
}
這裏是將Class當做入參傳入,而且需要是靜態方法。