涉及内容:
- 注解
- jdk动态代理
- 编译与反编译
引言
java和c/c++不同,c/c++在编译的时候有一个预处理功能,java没有,从java文件到class文件之后所编写的代码就固定了。
在下面即将讲述的场景如下,不同环境的数据库可能不一样,但是表名字一样,这时候在注解里面写死就不满足当前的需求
直接上代码,demo如下
/**
* @author authorZhao
* @date 2020年05月09日
*/
public class TestAnno {
//简单解析${}或者#{
private static final String SPEL_$START = "${";
private static final String SPEL_START = "#{";
private static final String SPEL_END = "}";
@Test
public void test1(){
System.setProperty("eshop","eshop_dev");
User userDev = getUserById(18);
System.out.println("==============分割线=============");
System.setProperty("eshop","eshop_Prod");
User user = getUserById(20);
}
private User getUserById(int i) {
Table table = User.class.getAnnotation(Table.class);
//直接重新创建一个新的注解类
Table newTable = getNewTable(table);
table = newTable;
String sql = "SELECT * FROM `"+table.dataBasesName()+"`."+table.tableName()+" WHERE id = " +i;
System.out.println(sql);
User user = new User();
user.setId(i);
user.setName("渣渣辉");
return user;
}
//重新创建新的注解对象
private Table getNewTable(Table table) {
return new Table(){
@Override
public Class<? extends Annotation> annotationType() {
return table.annotationType();
}
@Override
public long value() {
return table.value();
}
@Override
public String dataBasesName() {
String key = imitateSpel(table.dataBasesName());
return System.getProperty(key,key);
}
@Override
public String tableName() {
return table.tableName();
}
};
}
//模拟解析el表达式
private static String imitateSpel(String spel){
if(StringUtils.isEmpty(spel)){
return spel;
}
boolean start = spel.startsWith(SPEL_$START) || spel.startsWith(SPEL_START);
if( start && spel.endsWith(SPEL_END) ){
return spel.substring(2,spel.length()-1);
}else{
return spel;
}
}
}
@Table( value = 18,dataBasesName = "${eshop}",tableName = "${eshop_product}")
class User{
private Integer id;
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface Table{
long value();
String dataBasesName();
String tableName();
}
通过idea反编译的User类,可以看到里面的@Table注解已经是一个固定的值,这时候使用jdk知道的方法获得的值也是固定的
package com.utopa.test.anno;
@Table(
value = 18L,
dataBasesName = "${eshop}",
tableName = "${eshop_product}"
)
class User {
private Integer id;
private String name;
User() {
}
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
说明:
- 注解其实是一个接口
- 替换注解的值实际上是重新创建一个匿名类,实现注解里面的方法,上述代码将注解对象的引用该我自己实现的达到改变注解的值得方法
- 注解不能改变值的原因如下分析
- 1.注解接口的语法就没有提供设置值的方法(这么说你就不能在编辑器,比如idea里面直接用什么什么set方法改变值了,只能通过反射)
- 2.值的访问权限
jdk有一个注解处理类AnnotationInvocationHandler,在通过Class、Method、等获取注解的实例时或通过jdk动态代理产生一个注解实例,AnnotationInvocationHandler实现了InvocationHandler,并且里面保存看了一个map
private final Map<String, Object> memberValues;
在每次取值的时候实际上是从这里面获取,想要改值实际上就是往这个map里面存数据
困难:
1.怎么通过注解拿到AnnotationInvocationHandler
2.AnnotationInvocationHandler不是一个公开类
在解决上面两个困难的时候可以先分析一下Table这个类的真实面目
1.Table是动态生成的class,我们没有源码想到获得必须依靠工具
2.jdk自带一个ProxyGenerator类,可以将jdk动态代理的class输出到文件
->jdk8和jdk11可以在启动的时候这只参数-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true然后去target目录里面找一下
-> 因为jdk11采用了模块化,很多类不再能够随便访问,所以我直接将ProxyGenerator的源码复制了出来,相当于自我实现,源码来自jdk11,为了使jdk8和11都能够使用,在输出文件的时候不使用Path.of方法采用的commons.io的工具方法
byte[] tableImpls = ProxyGenerator.generateProxyClass("TableImpl", new Class[]{Table.class});
ProxyGenerator.writeClassToFile("/usr/local/src/TableImpl.class",tableImpls);
拿到TableImpl.class之后使用idea自带的反编译工具看一下源码
import com.git.test.anno.Table;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class TableImpl extends Proxy implements Table {
private static Method m1;
private static Method m5;
private static Method m2;
private static Method m6;
private static Method m4;
private static Method m0;
private static Method m3;
public TableImpl(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String tableName() throws {
try {
return (String)super.h.invoke(this, m5, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final Class annotationType() throws {
try {
return (Class)super.h.invoke(this, m6, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String dataBasesName() throws {
try {
return (String)super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final long value() throws {
try {
return (Long)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m5 = Class.forName("com.utopa.test.anno.Table").getMethod("tableName");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m6 = Class.forName("com.git.test.anno.Table").getMethod("annotationType");
m4 = Class.forName("com.git.test.anno.Table").getMethod("dataBasesName");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m3 = Class.forName("com.git.test.anno.Table").getMethod("value");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
TableImpl继承了Proxy实现了Table接口,Table就是我们自己写的注解,每次从注解里面取值实际上是调用h的invoker方法
通过上面的一顿分析,想要改变注解的值可以通过双重反射
- 获取Proxy对象
- 获取AnnotationInvocationHandler对象
- 获取AnnotationInvocationHandler的memberValues值,这是一个map,往里面put就行
最后一个demo
Table table = User.class.getAnnotation(Table.class);
try {
//拿到Proxy
Field h = table.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
//拿到AnnotationInvocationHandler对象
Object o = h.get(table);
Field memberValues = o.getClass().getDeclaredField("memberValues");
memberValues.setAccessible(true);
Map<String,Object> map = (Map) memberValues.get(o);
map.put("dataBasesName","sbsb");
} catch (Exception e) {
e.printStackTrace();
}
改变注解的方法总结:
1.重新实现注解接口
2.反射在反射
3.利用编译期处理(未实现,本人思路利用maven插件将自己生成class文件替换某个class文件)
附录: 将jdk动态代理的class输出到文件的方法,简单的方法前文已经叙述过
参考文章:
1.https://stackoverflow.com/里面的一个贴子,
具体路径modify-a-class-definitions-annotation-string-parameter-at-runtime这里面采用本文前面的直接实现注解接口
2.jdk11的源码ProxyGenerator类