【Java】Java中的动态代理以及在框架中的应用
一、静态代理&动态代理
1. 静态代理
我们先假设现在有怎么一个需求,要求你在不改动原有代码的情况下在所有类的方法前后打印日志。我们很容易想到静态代理,具体做法如下:
-
为现有的所有类都编写一个对应的代理类,并且还需要让代理类与原有类实现相同的接口;
-
在创建代理对象时,通过构造器传入一个目标对象,然后在代理对象的方法内部调用目标对象同名方法,并且在调用方法的前后打印日志。换而言之,代理对象=增强代码+原对象。有了代理对象后,我们在客户端就不再使用源对象,而是使用代理对象了。
静态代理的缺陷:从上面的静态代理实现方式上,我们很容易发现静态代理的缺陷。假设我们现在有很多类,那么就需要手动去实现很多个代理类,这样并不现实,那么我们应该考虑将这个任务交由计算机完成,接下来我们就来讨论动态代理的实现。
2. 动态代理
在讲解动态代理实现之前,我们先来回顾一下对象的创建过程。
从上面我们可以看出,创建一个对象并不仅仅是写一行 new 这么简单,底层还是隐含了许多信息的。不过我们至少可以了解到,一个对象的生成至少经历了以下这几个阶段:
那么到这里我们应该有大概的思路了。我们或许可以不写代理类,然后通过拦截器得到我们要代理的Class对象,之后再根据它加上反射机制创建代理实例(JDK动态代理的实现);或者让代理对象的class文件加载进来,然后通过修改其字节码来生成一个子类从而完成我们要做到的效果(CGLIB动态代理的实现)。
二、动态代理的实现
1. JDK动态代理
JDK动态代理的实现是利用拦截器(这个拦截器需要实现InvocationHandler
接口),以及反射机制最终实现一个代理接口的匿名类。
所以在JDK中,提供了java.lang.reflect.InvocationHandler
接口,此外还有一个比较重要的类java.lang.reflect.Proxy
类。利用这两个类之间的相互配合完成动态代理的配置。那接下来我们来看代码实现:
首先定义一个接口和一个实现类:
public interface UserService {
void addUser(String username, String password);
}
---------------------------------------------------
public class UserServiceImpl implements UserService{
@Override
public void addUser(String username, String password) {
System.out.println("UserService.addUser(String, String)方法被调用");
}
}
接下来我们就需要去定义一个拦截器去实现InvocationHandler
以实现我们的业务逻辑,代码如下:
public class JDKProxyHandler implements InvocationHandler {
// 被代理的原对象
private Object targetObject;
/**
* 传入一个目标对象,生成一个代理对象并返回
*
* @param targetObject 原对象(目标对象)
* @return 代理对象
*/
public Object newProxy(Object targetObject) {
this.targetObject = targetObject;
// 返回一个代理对象
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
targetObject.getClass().getInterfaces(),
this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 调用日志打印
log("权限校验中...");
// 声明方法的返回值
Object ret = null;
// 调用invoke方法,所返回的值赋值给ret
ret = method.invoke(targetObject, args);
return ret;
}
/**
* 模拟日志打印
*
* @param message 信息
*/
private void log(String message) {
System.out.println("【" + new SimpleDateFormat("yy-MM-dd hh:mm:ss").format(new Date()) + "】"
+ message);
}
}
接下来我们就可以在客户端进行测试了:
public class Client {
public static void main(String[] args) {
JDKProxyHandler jdkProxyHandler = new JDKProxyHandler();
UserService userService = (UserService) jdkProxyHandler.newProxy(new UserServiceImpl());
userService.addUser("jo", "8820");
}
}
最终结果如下:
2. CGLIB动态代理
CGLIB采用了非常底层的字节码技术,其原理是通过目标类(原来的类)的字节码创建一个新的子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势植入增强代码,所以代理类会将目标类作为自己的父类并为其中每个方法创建两个方法:
-
一个是于目标方法签名相同的类,它在方法中通过调用super来调用目标类中的方法;
-
以及另外一个Callback回调方法,它会判断这个方法是否绑定了拦截器(即实现了MethodInterceptor接口的对象),若存在则将调用intercept方法对目标方法进行代理,也就是在前后加上一些增强逻辑。intercept中就会调用上面介绍的签名相同的方法。
简而言之,就是CGLIB底层使用了ASM字节码处理框架,来修改字节码并生成新的类。那么接下来我们就用CGLIB来实现动态代理。
首先接口和实现接口的业务类还是复用上面的代码,不过我们还需要引入cglib的依赖,如下:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
实现一个代理类处理类:
public class CGLIBProxyHandler implements MethodInterceptor {
// CGLIB 需要代理的目标对象
private Object targetObject;
/**
* 创建一个代理对象
*
* @param targetObject 目标类
* @return 代理对象
*/
public Object crateProxyObject(Object targetObject) {
this.targetObject = targetObject;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetObject.getClass());
enhancer.setCallback(this);
Object proxyObj = enhancer.create();
return proxyObj;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object ret = null;
// 过滤方法
if ("addUser".equals(method.getName())) {
// 打印日志
log("权限检验中...");
}
ret = method.invoke(targetObject, objects);
return ret;
}
/**
* 模拟日志打印
*
* @param message 信息
*/
private void log(String message) {
System.out.println("【" + new SimpleDateFormat("yy-MM-dd hh:mm:ss").format(new Date()) + "】" + message);
}
}
客户端调用:
public class Client {
public static void main(String[] args) {
CGLIBProxyHandler cglibProxyHandler = new CGLIBProxyHandler();
UserService userService = (UserService) cglibProxyHandler.crateProxyObject(new UserServiceImpl());
userService.addUser("jo", "8820");
}
}
最终执行结果如下:
以上就是JDK以及CGLIB两种实现动态代理方式的演示了。
三、CGLIB和JDK两种动态代理的应用与区别
1. 两者间区别
其中最主要的区别莫过于JDK是针对接口类生成代理,而不是针对类。而CGLIB则是针对类实现的动态代理。除此之外,上面我们提到CGLIB实现是通过目标类的字节码生成一个子类,所以我们可以很明显知道,这种方式不能适用与被final
修饰的类。
2. Spring中的动态代理
2.1 Spring何时使用JDK/CGLIB实现AOP
- 如果目标对象实现了接口,默认情况下Spring会采用JDK的动态代理实现AOP(不过可以通过配置强制使用CGLIB实现);
- 如果目标对象没有实现接口,那么Spring就只会采用CGLIB库来完成动态代理。
2.2 如何强制使用CGLIB
-
添加CGLIB库的引用(aspectjrt-xxx.jar、aspectjweaver-xxx.jar、cglib-nodep-xxx.jar);
-
在Spring配置文件中加入
<aop:aspectj-autoproxy proxy-target-class="true"/>
。