废话不多说,相信点进这篇博客的看客需求肯定是需要动态的使用代码进行修改注解的值,可能看了很多博客,尝试了很多方法,都没有实现。那么恭喜你,马上就可以实现了,只要你耐心的花上一点点时间看我这篇博客即可。
为了更好的说明,这里使用一个例子进行说明:使用AOP环绕通知进行请求日志收集在这里插入图片描述
- @Pointcut:切入点
- @Around:环绕通知,可以指定切入点,包括使用带@Pointcut注解的方法
上面的代码可以在我访问我自己Controller中的方法的时候进行日志收集,如:
上面都是简单的一个AOP的功能。在注解中,如上面的@Pointcut注解,他的属性值必须是是常量,如果是变量的话就会报错:attribute value must be constant
那如果我不想在代码里面写:execution(* www.likuncheng.com.core.Controller..*.*(..)) 这样的表达式呢,我只是想在配置文件中写我包的全类名即可完成拦截,那这样拿到配置文件中的配置的话,那肯定就不是常量了啊。这怎么办呢?--世界之大,无奇不有,只要你想,怎么不可以实现呢。相信自己,可能是打开方式不对。
那么就引入今天的正题:怎么修改注解中的属性,又或者说怎么办常量变为变量,我们可以动态的提供。
首先说网上的错误的方法:使用单纯的反射进行实现,这里我们先还原一下使用反射进行动态的修改注解值。
/**
* 请求日志
*/
@Slf4j
@Aspect
@Component
public class RequestLog {
@Pointcut("execution(* www.likuncheng.com.entity..*.*(..))")
public void webControllerPointcut(){
}
//环绕通知
@Around("webControllerPointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
public void saveAttributeValue() throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException {
//获取class文件
Class requestLogClass = RequestLog.class;
//获取方法
Method webControllerPointcut = requestLogClass.getMethod("webControllerPointcut");
//获取注解对象
Pointcut annotation = webControllerPointcut.getAnnotation(Pointcut.class);
//获取注解值
String oldValue = annotation.value();
System.out.println("修改之前的注解值:"+oldValue);
//获取该对象的代理对象
InvocationHandler invocationHandler = Proxy.getInvocationHandler(annotation);
//通过反射获取代理对象里面的memberValues--->这里面就是保存的注解值
Field memberValues = invocationHandler.getClass().getDeclaredField("memberValues");
//允许修改
memberValues.setAccessible(true);
//获取对象 并且设置新的值
Map map = (Map) memberValues.get(invocationHandler);
map.put("value","execution(* www.likuncheng.com.core.Controller..*.*(..))");
String newValue = annotation.value();
System.out.println("修改之后的注解值:"+newValue);
}
我们在annotation.value();处打上断点,然后Debug启动。我们进入该方法后发现:
然后我们发现是进入到了AnnotationInvocationHandler代理类中,切经过断电发现所有的注解值信息都保存在memberValues这个字段中。所以我们后续使用反射获取InvocationHandler代理,切获取该memberValues属性。如果想看更细的这部分怎么分析的请点击我:通过反射,动态修改注解属性值
这个时候我们看注解属性值是否被修改:
发现我们的注解值确实被修改了,那如果这个时候的切面是:execution(* www.likuncheng.com.core.Controller..*.*(..)),那么按道理说这个时候我继续访问之前的请求,控制面板可以打印之前的信息。
然而并没有出现该日志信息,只是出现了初始化dispatcherServlet的日志信息。这是为什么呢?
之前网上的方法也就到了这一步就没有了,从控制面板上看确实是修改了注解的属性值,但是为什么没有生效呢?原因很简单,这里修改的注解属性值,只是当前修改了注解属性值而已,但是这个时候的RequestLog类已经初始化完成,且已经被编译成为了class文件,所以这个时候我们修改的值只能算是临时修改。而在编译之前我们的切面是:execution(* www.likuncheng.com.entity..*.*(..)),怎么验证呢?
验证方法:找到项目src统级的target文件夹(该文件夹就是保存的被编译之后的class文件等),我们打开target文件夹,找到我们的aop类,RequestLog.class
这里我们可以很清晰的看到,我们虽然在java代码中修改了切面地址,但是实际上我们修改的时候已经是被编译之后了,这个时候在进行修改,已经不管用了,程序运行的是不是java文件,而是class文件,这里我们修改的实际上是java文件,而程序运行是class文件,这也就是为什么我们修改了切面地址,且控制面板显示的也是已经被修改了,但是没有效果
网上方法失败原因:使用反射是修改了java文件,而且是在编译之后,java运行是认的class文件,但是在编译之前的是我Entity包
解决思路:
至于第一种怎么实现我不知道,我们讲解一下第二种,修改字节码,也是俗称的class文件,修改字节码文件又很多种方式:javassits,asm等都可以修改字节码文件,但是asm对开发人员要求较高,需要懂得原生的jvm汇编语言,javassits是一个日本大学教授开发且在jboss开源的一款使用java代码进行修改字节码文件的框架。
下面就使用javassits进行实现我们需要的效果:
//这个一定要加入,交给Spring进行管理
@Bean
public RequestLog requestLog() throws NotFoundException,
NoSuchMethodException, IllegalAccessException, InvocationTargetException,
InstantiationException, IOException, CannotCompileException {
ClassPool aDefault = ClassPool.getDefault();
//使用类的全类名
CtClass ctClass = aDefault.get("www.likuncheng.com.common.WebLog.RequestLog");
//输入方法名获取方法对象
CtMethod ctMethod = ctClass.getDeclaredMethod("webControllerPointcut");
//方法示例
MethodInfo methodInfo = ctMethod.getMethodInfo();
//常量池
ConstPool constPool = methodInfo.getConstPool();
//新增切面
saveAnnotation(constPool, methodInfo, "org.aspectj.lang.annotation.Pointcut", "value",
"execution(* www.likuncheng.com.core.Controller..*.*(..))");
CtMethod around = ctClass.getDeclaredMethod("around");
MethodInfo aroundMethodInfo = around.getMethodInfo();
ConstPool aroundMethodInfoConstPool = aroundMethodInfo.getConstPool();
//设置环绕切面
saveAnnotation(aroundMethodInfoConstPool, aroundMethodInfo, "org.aspectj.lang.annotation.Around",
"value", "webControllerPointcut()");
// System.getProperty("user.dir")
//获取当前i项目系统路径
File file = new File(".");
//找到class文件地址
String canonicalPath = file.getCanonicalPath() + "\\target\\classes\\www\\likuncheng\\com\\common\\WebLog\\RequestLog.class";
byte[] bytes = ctClass.toBytecode();
//写入到文件夹 --->这样就可以修改编译之后的class文件了
FileOutputStream fileOutputStream = new FileOutputStream(canonicalPath);
fileOutputStream.write(bytes);
fileOutputStream.close();
//使用反射进行实例化对象
Class requestLogClass = RequestLog.class;
//得到方法
Method webControllerPointcut = requestLogClass.getMethod("webControllerPointcut");
Pointcut pointcut = webControllerPointcut.getAnnotation(Pointcut.class);
String value = pointcut.value();
log.info("日志记录拦截包:" + value);
Constructor constructor = requestLogClass.getConstructor();
//实例化对象
RequestLog requestLog = constructor.newInstance();
return requestLog;
}
//设置注解
public void saveAnnotation(ConstPool constPool, MethodInfo methodInfo, String typeName, String key, String value) {
AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
//这个必须还是输入全类名
Annotation annotation = new Annotation(typeName, constPool);
annotation.addMemberValue(key, new StringMemberValue(value, constPool));
annotationsAttribute.addAnnotation(annotation);
methodInfo.addAttribute(annotationsAttribute);
}
通过上面的方法即可进行修改我们之前编译好的class文件,即可达到我们需要的效果。记住需要修改切面类的那几个注解。因为我们自己通过javassist进行添加了注解,且使用bean进行了注入容器。
加下来验证下是否完成效果:
发现我们的class文件已经修改成功了,接下来我们发起请求测试下效果:
发现了我们的日志信息已经添加成功了。
总结:使用反射修改注解值,是修改的j在编译之后的ava文件,但是运行的却是class文件,这就是为什么我们修改了注解值,但是还是没有效果,所以我们需要使用javassist进行修改编译之后的class文件,我们使用javassist进行添加注解。