这是一个新的工程。我们有一个接口叫 `MyInterface`,里面定义了三个方法:`func1()`、`func2()` 和 `func3()`。
现在我们有一个需求:每个方法被调用时,都要打印出自己的方法名。最简单的做法是写一个实现类 `NameImpl`:
```java
public class NameImpl implements MyInterface {
@Override
public void func1() {
System.out.println("func1");
}
@Override
public void func2() {
System.out.println("func2");
}
@Override
public void func3() {
System.out.println("func3");
}
}
```
但很快需求变了:不仅要打印方法名,还要打印方法名的长度。于是我们再写一个新类 `NameAndLengthImpl`,同样实现 `MyInterface`,但内部逻辑变成了:
public class NameAndLengthImpl implements MyInterface {
@Override
public void func1() {
String methodName = "func1";
System.out.println(methodName);
System.out.println(methodName.length());
}
@Override
public void func2() {
String methodName = "func2";
System.out.println(methodName);
System.out.println(methodName.length());
}
@Override
public void func3() {
String methodName = "func3";
System.out.println(methodName);
System.out.println(methodName.length());
}
}
问题来了:如果我们需要为不同的行为(比如打印时间、加日志、计数等)创建多个实现类,代码会大量重复,且无法动态扩展。我们能否**在运行时动态生成类**?
答案是可以的。Java 允许我们在程序运行期间生成 Java 源码文件,编译成 `.class` 文件,并加载到 JVM 中使用。
为此,我们创建一个工厂类 `MyInterfaceFactory`,用来专门生成实现了 `MyInterface` 的代理类。
第一步:生成 Java 源文件
我们写一个方法 `createJavaFile()`,它将拼接出一个完整的 Java 类源码字符串,并写入文件:
public class MyInterfaceFactory {
public static File createJavaFile() throws IOException {
String context = "package tech.insight.proxy;\n" +
"\n" +
"public class NameImpl implements MyInterface {\n" +
" @Override\n" +
" public void func1() {\n" +
" System.out.println(\"func1\");\n" +
" }\n" +
"\n" +
" @Override\n" +
" public void func2() {\n" +
" System.out.println(\"func2\");\n" +
" }\n" +
"\n" +
" @Override\n" +
" public void func3() {\n" +
" System.out.println(\"func3\");\n" +
" }\n" +
"}\n";
File javaFile = new File("NameImpl.java");
Files.write(javaFile.toPath(), context);
return javaFile;
}
public static void main(String[] args) throw Exception {
createJavaFile();
}
}
执行后,确实另外生成了一个 `NameImpl.java的文件。java虚拟机不能直接对完成
第二步:编译 Java 文件
Java 提供了 `JavaCompiler` 工具,可以在运行时动态编译 Java 文件:你可以看看Compiler文件
```java
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int result = compiler.run(null, null, null, javaFile.getPath());
```
我们将编译后的 `.class` 文件放在项目的 `target/classes` 目录下,确保类路径可访问。
Compiler.compile(javaFile);
得到了.class文件
接着作者通过自设getClassName(){
return "MyInterface$proxy" + count.incrementAndGet();
}
这样获得某些内容,如何进一步动态呢?
functionBody(Stirng methodName){
return "System.out .println" +methodName +"\");";
}
这样我们可以进一步
String func1Body = functionBody("func1");
String func2Body = functionBody("func2");
String func3Body = functionBody("func3");
String context = "package tech.insight.proxy;\n" +
"\n" +
"public class " + className + " implements MyInterface {\n" +
" @Override\n" +
" public void func1() {\n" +
" " + func1Body + "\n" +
" }\n" +
"\n" +
" @Override\n" +
" public void func2() {\n" +
" " + func2Body + "\n" +
" }\n" +
"\n" +
" @Override\n" +
" public void func3() {\n" +
" " + func3Body + "\n" +
" }\n" +
"}";
第三步:加载并实例化类
private static MyInterface newInstance(String className) throws Exception {
Class<?> aClass = MyInterfaceFactory.class.getClassLoader().loadClass(className);
Constructor<?> constructor = aClass.getConstructor();
MyInterface proxy = (MyInterface) constructor.newInstance();
return proxy;
}
public static MyInterface createProxyObject() throws Exception {
String className = getClassName();
File javaFile = createJavaFile(className);
Compiler.compile(javaFile);
return newInstance("tech.insight.proxy." + className);
}
创建一个main方法,把上面的所有暂时统统私有,创建了本来在虚拟机中的没有的类;
public class Main {
public static void main(String[] args) throws Exception {
MyInterface proxyObject = MyInterfaceFactory.createProxyObject();
proxyObject.func1();
proxyObject.func2();
proxyObject.func3();
}
}使用类加载器将编译好的字节码加载进虚拟机:
```java
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{new File("target/classes").toURI().toURL()});
Class<?> clazz = classLoader.loadClass("MyInterface$Proxy1");
Constructor<?> constructor = clazz.getConstructor();
Object instance = constructor.newInstance();
MyInterface proxy = (MyInterface) instance;
```
这样我们就得到了一个在运行时动态创建的对象。
第四步:让类名和方法体动态化
目前类名和方法体都是硬编码的。我们可以优化:
- 类名通过原子计数器自增,避免冲突,如 `MyInterface$Proxy1`、`MyInterface$Proxy2`。
- 方法体不再固定,而是由外部传入一个“处理器”来决定生成什么代码。
我们定义一个接口 `MyHandler`:
```java
public interface MyHandler {
String getMethodBody(String methodName);
}
```
这个接口的作用是:给定方法名,返回该方法体内要执行的 Java 代码字符串。
但是如何进一步的动态呢?就是把getClassName或者 functionBody 函数也动态,我们新建一个MyHandler的文件,获得一个方法名,返回一个函数体:动态代理的三个参数,加载器,类,handler都齐了
这样我们为原先的两个函数配置传递方法体:
public class MyInterfaceFactory {
private static final AtomicInteger count = new AtomicInteger();
private static File createJavaFile(String className, MyHandler handler) throws IOException {
String func1Body = handler.functionBody("func1");
String func2Body = handler.functionBody("func2");
String func3Body = handler.functionBody("func3");
String context = "package tech.insight.proxy;\n" +
"\n" +
"public class " + className + " implements MyInterface {\n" +
" @Override\n" +
" public void func1() {\n" +
...............................................
选择删掉我们的pxory了
private static MyInterface newInstance(String className) throws Exception {
Class<?> aClass = MyInterfaceFactory.class.getClassLoader().loadClass(className);
Constructor<?> constructor = aClass.getConstructor();
MyInterface proxy = (MyInterface) constructor.newInstance();
return proxy;
}
public static MyInterface createProxyObject(MyHandler myHandler) throws Exception {
String className = getClassName();
File javaFile = createJavaFile(className, myHandler);
Compiler.compile(javaFile);
return newInstance("tech.insight.proxy." + className);
}
然后在生成类时,把 `MyHandler` 传进去,用它来生成每种方法的函数体:
```java
for (Method method : MyInterface.class.getDeclaredMethods()) {
String body = handler.getMethodBody(method.getName());
content.append(" public void ").append(method.getName())
.append("() { ").append(body).append(" }\n");
}
```
例如,我们可以实现一个只打印方法名的处理器:
```java
public class PrintMethodNameHandler implements MyHandler {
@Override
public String getMethodBody(String methodName) {
return "System.out.println(\"" + methodName + "\");";
}
}
也可以实现一个先打印 `"1"` 再打印方法名的处理器:
public class PrintOneThenNameHandler implements MyHandler {
@Override
public String getMethodBody(String methodName) {
StringBuilder sb = new StringBuilder();
sb.append("System.out.println(\"1\");");
sb.append("System.out.println(\"").append(methodName).append("\");");
return sb.toString();
}
}
static class PrintFunctionName1 implements MyHandler {
@Override
public String functionBody(String methodName) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("System.out.println();")
.append(" System.out.println(\"" + methodName + "\");");
return stringBuilder.toString();
}
}
所有的对象都是代理拿到的
P 功能(前置/后置增强)
我们现在想实现类似 Spring AOP 的功能:在方法执行前后自动添加日志。
我们需要生成这样的代码:
```java
public void func1() {
System.out.println("before");
myInterface.func1();
System.out.println("after");
}
```
但这里有个问题:生成的类需要持有一个真实的 `MyInterface` 实例,并调用它的方法。
因此,`MyHandler` 需要能设置目标对象。我们扩展接口:避免空指针,这样
```java
public interface MyHandler {
String getMethodBody(String methodName);
default void setProxy(Object proxy) {}
}
```
public interface MyHandler {
String functionBody(String methodName);
default void setProxy(MyInterface proxy) {
}
}
默认方法表示大多数处理器不需要设置代理对象。
只有像 `LogHandler` 这样的处理器才需要:
```java
public class LogHandler implements MyHandler {
private final MyInterface target;
public LogHandler(MyInterface target) {
this.target = target;
}
@Override
public String getMethodBody(String methodName) {
return "System.out.println(\"before\");" +
"myInterface." + methodName + "();" +
"System.out.println(\"after\");";
}
@Override
public void setProxy(Object proxy) {
try {
Field field = proxy.getClass().getDeclaredField("myInterface");
field.setAccessible(true);
field.set(proxy, target);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
```
static class LogHandler implements MyHandler {
MyInterface myInterface;
public LogHandler(MyInterface myInterface) {
this.myInterface = myInterface;
}
@Override
public void setProxy(MyInterface proxy) {
Class<?> extends MyInterface aClass = proxy.getClass();
Field field = null;
try {
field = aClass.getDeclaredField("myInterface");
field.setAccessible(true);
field.set(proxy, myInterface);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
我们在生成的类中添加字段:
```java
content.append(" private MyInterface myInterface;\n");
```
private static MyInterface newInstance(String className, MyHandler handler) throws Exception {
Class<?> aClass = MyInterfaceFactory.class.getClassLoader().loadClass(className);
Constructor<?> constructor = aClass.getConstructor();
MyInterface proxy = (MyInterface) constructor.newInstance();
handler.setProxy(proxy);
return proxy;
}
public static MyInterface createProxyObject(MyHandler myHandler) throws Exception {
String className = getClassName();
File javaFile = createJavaFile(className, myHandler);
Compiler.compile(javaFile);
return newInstance("tech.insight.proxy." + className, myHandler);
}
并在 `newInstance()` 时调用 `handler.setProxy(proxy)` 完成赋值。
proxyObject= MyInterfaceFactory.createProxyObject(new LogHandler(proxyObject));
最终,我们成功实现了:
- 动态生成实现了 `MyInterface` 的类;
- 方法体内容由 `MyHandler` 控制;
- 支持在方法前后插入逻辑,实现 AOP 增强。
这就是我们写的动态代理 那么再回顾一下 我们是如何写我们的动态代理的呢 首先我们生成了一个文字版的java类 然后我们在运行时把它编译 再用通过class loader去把这个编译出来的字节码 文件加载到虚拟机中 然后再通过反射去创建这个类 然后再调用这个类对应的方法 这就是我们写的动态代理
虽然功能完整,但它有明显缺陷:
1. 只能代理 `MyInterface`,无法泛化;
2. 依赖磁盘 IO 和编译过程,性能差;
3. 生成的是 Java 源码,而不是直接操作字节码。
而 JDK 自带的动态代理(`java.lang.reflect.Proxy`)和 CGLIB 则更高效,它们可以直接在内存中生成字节码,无需写文件和编译。
Spring 的事务管理底层就是基于动态代理。当你在一个加了 `@Transactional` 的方法上执行数据库操作时,Spring 会生成一个代理对象,在方法执行前后自动开启事务、提交或回滚:
```java
try {
开启事务();
target.insert(); // 真实业务逻辑
提交事务();
} catch (RuntimeException e) {
回滚事务();
throw e;
}
```
理解这一点后,就能明白为什么有些情况下事务会失效:
- 如果你捕获了异常并“吞掉”,代理层无法感知异常,就不会回滚;
- 如果抛出的是 checked exception(非 `RuntimeException`),默认不会触发回滚,除非显式配置;
- 如果自调用(this.method()),绕过了代理对象,AOP 不生效。
思考题:
你能基于以上思路,自己实现一个支持 `before`、`after`、`around` 的通用增强处理器吗?
Comments NOTHING