专栏首页java技术爱好者一篇文章搞懂代理模式

一篇文章搞懂代理模式

代理模式

代理模式的定义:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗点说,就是一个中介,比如有一个广州人,是个本地人,有两套房,他要租出去收租,但是除了收租,他还要去找租客,带租客看房,还要准备租房合同,核算水电费等等,很麻烦。这个本地人他也不想这么折腾,他只想完成他的核心业务(收钱),其他杂七杂八的事情就不想管,但是总要有人去做,那就找租房中介,也就是二手房东。二手房东就代理这个广州本地人把房子租给租客。这个道理就是这么简单。

因为很多广州本地人都有这个需求,干脆就搞一个中介公司来专门去做租房子的事情。

代理模式,运用在编程里,也是这个道理,有一些非核心业务的代码,在很多地方都需要用到的逻辑,可以交给代理对象完成,程序员只需要关心核心业务的逻辑即可。

实现代理模式的三种方式

项目就基于上一篇模板模式以及实战应用的项目进行试验。

静态代理

假设原来有一个接口UserService,controller层调用userServicegetAllUser()方法。如下所示:

public interface UserService {
    /**
     * 获取所有用户信息
     * @return List
     * @date 2020/4/12
     */
    List<User> getAllUser() throws Exception;
}
@RestController
@RequestMapping("/xiaoniu")
public class UserController {

    @Resource(name = "userService")
    private UserService userService;

    @RequestMapping("/getAllUser")
    public List<User> getAllUser()throws Exception{
        return userService.getAllUser();
    }
}

如果用静态代理实现记录日志信息,怎么记录呢?

首先创建一个代理类UserServiceProxy,实现UserService接口,然后在UserServiceProxy里面创建一个成员变量userService,再写一个有参构造器来初始化userService。代码如下:

public class UserServiceProxy implements UserService {

    private UserService userService;

    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public List<User> getAllUser() throws Exception {
        System.out.println("记录日志:执行getAllUser()方法前");
        List<User> userList = userService.getAllUser();
        System.out.println(userList);
        System.out.println("记录日志:执行getAllUser()方法后");
        return userList;
    }
}

所以在controller层调用的方式就要改一下,是用代理类UserServiceProxy调用getAllUser()方法。如下:

@RestController
@RequestMapping("/xiaoniu")
public class UserController {

    @Resource(name = "userService")
    private UserService userService;

    @RequestMapping("/getAllUser")
    public List<User> getAllUser()throws Exception{
        return new UserServiceProxy(userService).getAllUser();
    }
}

然后启动项目,调用一下接口,就可以看到控制台打印如下日志:

/*
记录日志:执行getAllUser()方法前
[User{id=1, name='大司马', age=36, job='厨师'}, User{id=2, name='朴老师', age=36, job='主播'}, User{id=3, name='王刚', age=30, job='厨师'}, User{id=4, name='大sao', age=32, job='美食up主'}, User{id=5, name='姚大秋', age=35, job='主持人'}]
记录日志:执行getAllUser()方法后
*/

这就是静态代理的实现思路,很简单。但是一般我们肯定是不用这种方式。因为这种方式太笨了,很容易就可以看出几个缺点。

1.要实现接口,也就是目标的方法要定义一个接口方法,实际上是运用了java多态的特性

2.第一点还不是致命的,因为JDK动态代理也是必须要定义接口;致命的是每一个你想代理的接口你都要去创建一个代理类去实现,假设有很多要代理的接口,那就创建很多代理类,这样显得很臃肿

假设还是不理解为什么要动态代理,不妨我们再多加一个支付接口PayService,这个支付接口我们也要加上日志记录。

用静态代理怎么做?很简单呀,再创建一个PayServiceProxy类不就完了吗,如果还有OrderService(订单),

WarehouseService(仓库)等等。那就要创建很多XXXServiceProxy类。如果使用动态代理,就没必要创建这么多代理类,创建一个代理类就够了!

动态代理就是为了解决静态代理的这个缺点产生的。

JDK动态代理

JDK本身就带有动态代理,必须要满足一个条件,就是要有接口。原理其实和静态代理是一样的,也是用代理类去实现接口,但是代理类不是一开始就写好的,而是在程序运行时通过反射创建字节码文件然后加载到JVM。也就是动态生成的代理类对象。

下面就是用JDK动态代理实现代理模式。

public class LogRecordProxy<T> implements InvocationHandler {

    private T target;

    public LogRecordProxy(T t) {
        this.target = t;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("记录日志:执行" + method.getName() + "方法前");
        Object result = method.invoke(target, args);
        System.out.println(result);
        System.out.println("记录日志:执行" + method.getName() + "方法后");
        return result;
    }

    /**
     * 获取代理对象的方法
     * */
    @SuppressWarnings("unchecked")
    public <T> T getProxy() throws Exception {
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
}

在controller层,就要改成这样。

@RestController
@RequestMapping("/xiaoniu")
public class UserController {

    @Resource
    private UserService userService;

    @RequestMapping("/getAllUser")
    public List<User> getAllUser() throws Exception {
        //获取代理对象
        UserService userServiceProxy = new LogRecordProxy<>(userService).getProxy();
        return userServiceProxy.getAllUser();
    }
}

假设有一个PayService也要做日志记录,就可以直接使用。

    @Resource(name = "payService")
    private PayService payService;    

    @RequestMapping("/pay")
    public String pay(@RequestParam(name = "channel") String channel,
                      @RequestParam(name = "amount") String amount
    )throws Exception{
        //获取代理对象,实际上就在构造器上改一下传入的参数即可
        PayService payServiceProxy = new LogRecordProxy<>(payService).getProxy();
        return payServiceProxy.pay(channel,amount);
    }

很多文章给的例子都不带泛型,也可以,就是获取的代理对象需要强转一下,强转成对应的接口类。

注意:这里一定要用接口接收代理对象,不能用实现类!

因为返回的对象已经不是实现类的对象,而是和实现类有共同的接口类的代理类对象,所以当然只能用接口类去接收。

这也是为什么一再强调要面向接口编程的原因,因为面向接口编程可以做更多的扩展。假设是面向实现类去编程,那就不能用JDK动态代理去扩展了!

CGLB动态代理

那如果有些场景真的没有接口呢,我们怎么运用代理模式?

首先引入maven配置

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

然后创建一个方法拦截器LogRecordInterceptor,要实现MethodInterceptor类,如下:

public class LogRecordInterceptor implements MethodInterceptor {

    private Object target;

    public LogRecordInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("记录日志:执行" + method.getName() + "方法前,参数:" + Arrays.toString(args));
        Object result = method.invoke(target, args);
        System.out.println(result);
        System.out.println("记录日志:执行" + method.getName() + "方法后,参数:" + Arrays.toString(args));
        return result;
    }
}

然后再创建一个工厂类InterceptorFactory,用于创建代理对象。

public class InterceptorFactory {

    @SuppressWarnings("unchecked")
    public static <T> T getInterceptor(Class<T> clazz, MethodInterceptor methodInterceptor) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(methodInterceptor);
        return (T) enhancer.create();
    }
}

接着我们就可以创建一个没有接口的类,我这里就创建一个数学工具类进行测试

public class MathUtil {
    /**
     * 获取一个数的平方
     * */
    public String getSquare(int num) {
        return String.valueOf(num * num);
    }
}

然后在controller层定义一个接口来测试

@RequestMapping("/getSquare")
    public String getSquare(@RequestParam(name = "num") Integer num) throws Exception {
        MathUtil mathUtil = InterceptorFactory.getInterceptor(MathUtil.class, new LogRecordInterceptor(new MathUtil()));
        return mathUtil.getSquare(num);
    }

用浏览器或者POSTMAN工具调用接口,就可以在控制台看到以下输出:

/*
记录日志:执行getSquare方法前,参数:[2]
4
记录日志:执行getSquare方法后,参数:[2]
*/

这样就实现没有定义接口也可以实现动态代理!

实际上,定义接口的也可以用这种方法来进行扩展,比如上面的userService接口

@RestController
@RequestMapping("/xiaoniu")
public class UserController {

    @Resource
    private UserService userService;

    @RequestMapping("/getAllUser")
    public List<User> getAllUser() throws Exception {
        UserServiceImpl userServiceProxy = InterceptorFactory
            .getInterceptor(UserServiceImpl.class,
                            new LogRecordInterceptor(userService));
        return userServiceProxy.getAllUser();
    }
}

调用接口我们在控制台也是可以看到以下输出日志:

/*
记录日志:执行getAllUser方法前,参数:[]
[User{id=1, name='大司马', age=36, job='厨师'}, User{id=2, name='朴老师', age=36, job='主播'}, User{id=3, name='王刚', age=30, job='厨师'}, User{id=4, name='大sao', age=32, job='美食up主'}, User{id=5, name='姚大秋', age=35, job='主持人'}]
记录日志:执行getAllUser方法后,参数:[]
*/

总结

以上就是代理模式的一些通俗的解释,还有三种实现的方式的学习

多说几句,我们都知道Spring框架有两个核心技术,一个叫控制反转IOC,另一个叫切面编程AOP。切面编程大家都很熟悉,用的就是代理模式,那么AOP实现的代理模式用的是JDK动态代理还是CLB动态代理

答曰:两个都用!

最简单的,我们看Spring的事务管理,就是用代理模式实现的,如果有兴趣,其实我们自己也可以通过JDK动态代理手写实现事务管理,其实不是很难。篇幅有限,以后可以单独写一篇文章详细说明Spring的事务管理,敬请期待。更多的设计模式实战经验的分享,就关注java技术小牛吧。

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

本文分享自微信公众号 - java技术爱好者(yehongzhi_java),作者:牛九木

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Netty进阶之粘包和拆包问题

    发送端为了将多个发给接收端的数据包,更有效地发送到接收端,会使用Nagle算法。Nagle算法会将多次时间间隔较小且数据量小的数据合并成一个大的数据块进行发送。...

    java技术爱好者
  • 装饰者模式与IO流的应用

    装饰者模式是一种对象结构型模式。动态地给一个对象添加一些额外的职责,就增加功能来说,装饰者模式比生成子类更为灵活。

    java技术爱好者
  • 原型模式以及克隆技术

    原型模式是一种创建型设计模式,Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节,工作原理是:通过将一个原型对象传给那...

    java技术爱好者
  • 文档管理平台flow办公工作流程完整展示效果

    hotqin888
  • .NET 状态机Automatonymous快速入门

    Automatonymous是.NET开发人员的状态机库。它提供了一种流畅的语法来声明状态机,包括状态,事件(支持触发器和数据事件)以及状态/事件活动。尽管Au...

    李明成
  • 如何处理跨域时的 OPTIONS 请求?

    最近在公司项目中与后端联调时遇到了一个很奇怪的问题,前端发出的 DELETE 方法的 Ajax 请求传到服务端就变成了 OPTIONS 请求。由于服务端没有针对...

    逆葵
  • Java基础05 实施接口

    在Java基础04 封装与接口中,private关键字封装了对象的内部成员。经过封装,产品隐藏了内部细节,只提供给用户接口(interface)。

    Java团长
  • 博客从ASP.NET 迁移到了ASP.NET Core 2.0

    在迁移之前,本站点是基于 ASP.NET MVC 4构建且部署在Linux+Mono环境下,Web服务器使用的是Jexus,在 .NET Core出来之前,这是...

    KenTalk
  • Java基础05 实施接口

    在封装与接口中,private关键字封装了对象的内部成员。经过封装,产品隐藏了内部细节,只提供给用户接口(interface)。 接口是非常有用的概念,可以辅助...

    Vamei
  • 高性能NIO框架Netty-对象传输

    上篇文章高性能NIO框架Netty入门篇我们对Netty做了一个简单的介绍,并且写了一个入门的Demo,客户端往服务端发送一个字符串的消息,服务端回复一个字符串...

    猿天地

扫码关注云+社区

领取腾讯云代金券