java高级:动态代理

动态代理介绍、准备功能

这节课我们学习一个Java的高级技术叫做动态代理。首先我们认识一下代理长什么样?

假设现在有一个明星坤坤,它有唱歌和跳舞的本领,作为明星是要用唱歌和跳舞来赚钱的,但是每次做节目,唱歌的时候要准备话筒、收钱,再唱歌;跳舞的时候也要准备场地、收钱、再唱歌。明星觉得我擅长的做的事情是唱歌,和跳舞,但是每次唱歌和跳舞之前或者之后都要做一些繁琐的事情,有点烦。于是就找个一个经济公司,请了一个代理人,代理明星处理这些事情,如果有人想请明星演出,直接找代理人就可以了。如下图所示

请添加图片描述
我们说明星的代理是中介公司派的,那中介公司怎么知道,要派一个有唱歌和跳舞功能的代理呢?

解决这个问题,Java使用的是接口,明星想找代理,在Java中需要明星实现了一个接口,接口中规定要唱歌和跳舞的方法。Java就可以通过这个接口为明星生成一个代理对象,只要接口中有的方法代理对象也会有。

请添加图片描述

接下来我们就先把有唱歌和跳舞功能的接口,和实现接口的大明星类定义出来,声明接口是为了声明大明星类中有的方法代理对象也会有,注意,大明星类也需要实现Star接口,这是java生成代理的一个约定。

请添加图片描述

生成动态代理对象

有了上面的准备工作,下面我们需要写一个为BigStar生成动态代理对象的工具类ProxyUtil代表中介机构。使用工具类产生代理则需要用Java为开发者提供的一个生成代理对象的类叫Proxy类。注意Proxy类有多个,我们需要选择java.lang.reflect中的Proxy

通过Proxy类的newInstance(…)方法可以为实现了同一接口的类生成代理对象。 调用方法时需要传递三个参数,该方法的参数解释可以查阅API文档,如下。

请添加图片描述

请添加图片描述

这里代码逻辑比较抽象,所以写了大量的注释来解释逻辑,需要仔细阅读。

public class ProxyUtil {
    //因为是工具类所以可以定义一个静态方法来产生代理,产生谁的代理可以通过方法参数传递
    //这个方法生成的代理肯定是实现了Star接口的对象 所以这里可以将Star作为返回值
    public static Star createProxy(BigStar bigStar){//我们要为bigStar创造代理并返回
       /* newProxyInstance(ClassLoader loader,
                Class<?>[] interfaces,
                InvocationHandler h)
                参数1:用于指定一个类加载器 用于加载生成的代理类 写法是固定的 背就行 一般用当前类的类加载器
                参数2:一个接口数组 指定生成的代理长什么样子 也就是有哪些方法 我们这里只有一个接口 把它包装成数组传进去即可
                参数3:用来指定生成的代理对象要干什么事情 这里传递的是一个InvocationHandler接口
                因为接口不能直接创建对象 所以一般是传递一个匿名内部类对象来指定代理对象干什么事情重写invoke方法就行
                */

        /* invoke方法是个回调方法 会被谁回调呢? 假设代理写好了 调用时是会写这样的代码的:
         * Star starProxy = ProxyUtil.createProxy(s);//得到一个s的代理对象
         * starProxy.sing("好日子") starProxy.dance() 而sing和dance会调用invoke方法!
         * 因为代理干什么事情用invoke决定 invoke需要三个参数 所以sing和dance也会传进这三个参数
         * 比如starProxy.sing("好日子") starProxy是第一个参数 sing是第二个 "好日子"是第三个
         * 第一个参数java把代理对象当做一个Object 也就是starProxy 第二个参数是调用的方法
         * 如果是sing调用 method代表的就是sing方法 第三个参数args会把方法的参数通过一个object数组传进来
         * 比如sing调用时就会把"好日子"传进数组 这就是invoke三个参数的含义
         */
        //newProxyInstance返回的是Object 所以需要强转
        Star starProxy = (Star) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
                new Class[]{Star.class}, new InvocationHandler() {
                    @Override // 重写invoke回调方法
                    //invoke是重点 代理干什么事情其实是由它决定
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 代理对象要做的事情,会在这里写代码
                        if(method.getName().equals("sing")){
                            System.out.println("准备话筒,收钱20万");
                        }else if(method.getName().equals("dance")){
                            System.out.println("准备场地,收钱1000万");
                        }
                        //代理做完事情 再让明星做他该做的事
                        return method.invoke(bigStar, args);//bigStar代表明星 再把调用方法参数传进来
                        //注意这里的invoke不是这里写的invoke 而是反射里Method提供的invoke方法!
                        //调用sing 则会返回"谢谢大家!
                    }
                });
        return starProxy;//返回代理对象
    }
}

在写一个Test类调用我们写好的ProxyUtil工具类,为BigStar对象生成代理对象:

public class Test {
    public static void main(String[] args) {
        BigStar s = new BigStar("大明星坤坤");
        Star starProxy = ProxyUtil.createProxy(s);

        String rs = starProxy.sing("好日子");
        System.out.println(rs);

        starProxy.dance();
    }
}

运行结果:

请添加图片描述




动态代理应用

学习完动态代理的基本使用之后,接下来我们再做一个应用案例。

请添加图片描述

现有如下代码

/**
 *  用户业务接口
 */
public interface UserService {
    // 登录功能
    void login(String loginName,String passWord) throws Exception;
    // 删除用户
    void deleteUsers() throws Exception;
    // 查询用户,返回数组的形式。
    String[] selectUsers() throws Exception;
}

下面有一个UserService接口的实现类,下面每一个方法中都有计算方法运行时间的代码。

/**
 * 用户业务实现类(面向接口编程)
 */
public class UserServiceImpl implements UserService {
    @Override
    public void login(String loginName, String passWord) throws Exception {
        long time1 = System.currentTimeMillis();
        if ("admin".equals(loginName) && "123456".equals(passWord)) {
            System.out.println("您登录成功,欢迎光临本系统~");
        } else {
            System.out.println("您登录失败,用户名或密码错误~");
        }
        Thread.sleep(1000);
        long time2 = System.currentTimeMillis();
        System.out.println("login方法耗时:" + (time2 - time1) / 1000.0 + "s");
    }

    @Override
    public void deleteUsers() throws Exception {
        long time1 = System.currentTimeMillis();
        System.out.println("成功删除了1万个用户~");
        Thread.sleep(1500);
        long time2 = System.currentTimeMillis();
        System.out.println("deleteUsers方法耗时:" + (time2 - time1) / 1000.0 + "s");
    }

    @Override
    public String[] selectUsers() throws Exception {
        long time1 = System.currentTimeMillis();
        System.out.println("查询出了3个用户");
        String[] names = {"张全蛋", "李二狗", "牛爱花"};
        Thread.sleep(500);
        long time2 = System.currentTimeMillis();
        System.out.println("selectUsers方法耗时:" + (time2 - time1) / 1000.0 + "s");
        return names;
    }
}

我们会发现每一个方法中计算耗时的代码都是重复的,况且这些重复的代码并不属于UserSerivce的主要业务代码。所以接下来我们打算,把计算每一个方法的耗时操作,交给代理对象来做。

先在UserService类中把计算耗时的代码删除,代码如下

public class UserServiceImpl implements UserService {
    @Override
    public void login(String loginName, String passWord) throws Exception {
        if ("admin".equals(loginName) && "123456".equals(passWord)) {
            System.out.println("您登录成功,欢迎光临本系统~");
        } else {
            System.out.println("您登录失败,用户名或密码错误~");
        }
        Thread.sleep(1000);
    }

    @Override
    public void deleteUsers() throws Exception {
        System.out.println("成功删除了1万个用户~");
        Thread.sleep(1500);
    }

    @Override
    public String[] selectUsers() throws Exception {
        System.out.println("查询出了3个用户");
        String[] names = {"张全蛋", "李二狗", "牛爱花"};
        Thread.sleep(500);
        return names;
    }
}

然后为UserService生成一个动态代理对象,在动态代理中调用目标方法,在调用目标方法之前和之后记录毫秒值,并计算方法运行的时间。代码如下

public class ProxyUtil {
    public static UserService creatProxy(UserService userService){
        UserService userServiceProxy = (UserService) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
                new Class[]{UserService.class}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        String methodName=method.getName();
                        if(methodName.equals("login")||methodName.equals("deleteUsers")||methodName.equals("selectUsers")){
                            long startTime = System.currentTimeMillis();
                            Object rs = method.invoke(userService,args);

                            long endTime = System.currentTimeMillis();
                            System.out.println(methodName + "方法执行耗时:"+(endTime-startTime)/1000.0+"s");
                            return rs;
                        }else{
                            Object rs = method.invoke(userService,args);
                            return rs;
                        }
                    }
                });
        return userServiceProxy;
    }
}

然后在测试类中为UserService创建代理对象:

public class Test {
    public static void main(String[] args) throws Exception{
        // 1、创建用户业务对象。
        UserService userService = ProxyUtil.createProxy(new UserServiceImpl());

        // 2、调用用户业务的功能。
        userService.login("admin", "123456");
        System.out.println("----------------------------------");

        userService.deleteUsers();
        System.out.println("----------------------------------");

        String[] names = userService.selectUsers();
        System.out.println("查询到的用户是:" + Arrays.toString(names));
        System.out.println("----------------------------------");

    }
}

执行结果如下所示:

请添加图片描述





另一种动态代理写法

在之前的例子中,我们使用工具类生成动态代理对象,在写生成动态代理对象的newProxyInstance方法时传入的第一个参数使用的是工具类的类加载器,在大多数情况下它们很可能是由同一个类加载器加载的,能够正常运行。

但是,在某些情况下,例如你的项目使用了OSGi、Jboss Modules或者其他模块化技术,或者你的项目在特殊的环境下运行(例如Web服务器或者应用服务器),那么即使两个类在同一个模块中,它们也可能会被不同的类加载器加载,工具类的代码就出问题了。

如果你想要确定这两个类是否在同一个类加载器下,你可以在运行时打印出这两个类的类加载器,然后比较它们是否相等。

ClassLoader classLoader1 = ProxyUtil.class.getClassLoader();
ClassLoader classLoader2 = BigStar.class.getClassLoader();

if (classLoader1.equals(classLoader2)) {
    System.out.println("ProxyUtil和BigStar在同一个类加载器下");
} else {
    System.out.println("ProxyUtil和BigStar不在同一个类加载器下");
}

后来我在写一个RPC项目时还学习到了另一种动态代理的写法,这种方式的优点是,无论你的目标类在哪个类加载器中,这种方式都能正常工作,因为类加载器能够访问它自己加载的类以及其父类加载器加载的类。

1.定义发送短信的接口

public interface SmsService {
	String send(String message);
}

2.实现发送短信的接口

public class SmsServiceImpl implements SmsService {
	public String send(String message) {
		System.out.println("send message:" + message);
		return message;
	}
}

3.定义一个 JDK 动态代理类

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class DebugInvocationHandler implements InvocationHandler {
    /**
     * target代表将来创建的代理类对象
     */
    private final Object target;
    public DebugInvocationHandler(Object target) {
        this.target = target;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws
            InvocationTargetException, IllegalAccessException {
//调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
//调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        return result;
    }
}

invoke() 方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是invoke() 方法,然后 invoke() 方法代替我们去调用了被代理对象的原生方法。


4.获取代理对象的工厂类

public class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 目标类的类加载
                target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个
                new DebugInvocationHandler(target) // 代理对象对应的自定义InvocationHandler
        );
    }
}

getProxy() :主要通过Proxy.newProxyInstance() 方法获取某个类的代理对象


5.实际使用运行

SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new
SmsServiceImpl());
smsService.send("java");

//运行上述代码之后,控制台打印出:
//before method send
//send message:java
//after method send

对于动态代理和类加载器本人不太熟悉,尤其是类加载器,所以这一节可能说的存在问题,欢迎指正。

JDK动态代理的缺点与CGLIB 动态代理

JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。

CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了CGLIB, 例如 Spring中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

在 CGLIB 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心。你需要自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法。

public interface MethodInterceptor extends Callback{
	// 拦截被代理类中的方法
	public Object intercept(Object obj, java.lang.reflect.Method method,Object[] args,MethodProxy proxy) throws Throwable;
}
1. obj :被代理的对象(需要增强的对象)
2. method :被拦截的方法(需要增强的方法)
3. args :方法入参
4. methodProxy :用于调用原始方法

你可以通过 Enhancer 类来动态获取被代理类,当代理类调用方法的时候,实际调用的是MethodInterceptor 中的intercept 方法。

CGLIB 动态代理类使用步骤

1. 定义一个需要被代理的类;
2. 自定义 MethodInterceptor 并重写intercept 方法, intercept 用于拦截增强被代理类的方
法,和 JDK 动态代理中的 invoke 方法类似;
3. 通过Enhancer 类的create() 创建代理类;

下面给出案例:

如果你要使用它的话,需要手动添加相关依赖。

<dependency>
	<groupId>cglib</groupId>
	<artifactId>cglib</artifactId>
	<version>3.3.0</version>
</dependency>

1.实现一个使用阿里云发送短信的类

public class AliSmsService {
	public String send(String message) {
		System.out.println("send message:" + message);
		return message;
	}
}

2.自定义 MethodInterceptor (方法拦截器)

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

/**
 * 自定义MethodInterceptor
 */
public class DebugMethodInterceptor implements MethodInterceptor {
    /**
     * @param o           代理对象(增强的对象)
     * @param method      被拦截的方法(需要增强的方法)
     * @param args        方法入参
     * @param methodProxy 用于调用原始方法
     */
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        //调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        Object object = methodProxy.invokeSuper(o, args);
        //调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        return object;
    }
}

3.获取代理类

import net.sf.cglib.proxy.Enhancer;
public class CglibProxyFactory {
	public static Object getProxy(Class<?> clazz) {
		// 创建动态代理增强类
		Enhancer enhancer = new Enhancer();
		// 设置类加载器
		enhancer.setClassLoader(clazz.getClassLoader());
		// 设置被代理类
		enhancer.setSuperclass(clazz);
		// 设置方法拦截器
		enhancer.setCallback(new DebugMethodInterceptor());
		// 创建代理类
		return enhancer.create();
	}
}

4.实际使用

AliSmsService aliSmsService = (AliSmsService)CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("java");

//运行上述代码之后,控制台打印出:
//before method send
//send message:java
//after method send

总结:

1. JDK 动态代理只能只能代理实现了接口的类,而 CGLIB 可以代理未实现任何接口的类。 
2. 另外,CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为final 类型的类和方法。
3. 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值