橘子学Mybatis03之代理模式

一、什么是代理模式,为啥需要代理模式

1、问题

在JAVAEE的MVC分层开发中,哪个层级对我们来说最重要?

DAO ------> Service --------> Controller

JAVAEE分层开发中,最为重要的是Service层。这个也可以理解,因为Service是完成业务的层,你做项目不就是完成业务吗。

Service层中包含了哪些代码?

Service层中 = 核心功能(几十行上百行代码) + 额外功能(附加功能)
1、核心功能一般是业务逻辑,DAO调用等等。
2、额外功能一般下面三个特点.
	1、不属于业务
	2、可有可无
	3、代码量很小
# 比如像:事务,日志,性能 ...
日志一般就是输出一些流程信息,重要的需要,其余的都不用,所以也是可有可无的,其实也和业务没关。
性能就是输出一些性能信息,没啥。也是有的地方用,与业务无关。
但是事务很多人觉得重要,但是事务首先不是每一个地方都有,其次事务和业务也无关。
以上三个代码量都也不多。

那么额外功能写在Service层中到底好不好?

Service层的调用者的角度上看(也就是Controller):需要在Service层书写额外功能。
因为作为调用者,我们担心的比较多,我们怕出问题,所以想加上事务来维护。
从软件设计者的角度来看:Service层不需要加那些额外功能,因为额外不是必须的,加上了就不好维护,一会要加上,一会又要去掉,设计者不想这么麻烦。
所以这就出现了矛盾。下面我们就来解决矛盾。

2、解决方式

现实生活中的解决方式

其实现实生活中也有这个问题,我们来复盘一下。
比如说租房,有这么几个参与者,房客,房东。
租房这个业务的核心业务就是签合同收钱,额外的业务还有就是广告看房,对于房东来说就想签合同收钱,不想累死累活带着客户看房,对于客户来说你特么房都不看就想收老子钱,你去抢吧。这不就是调用者设计者对于核心业务和额外业务的矛盾出现了。
对于房东这个业务设计者来说,他只想做核心业务,不想做额外功能,对于业务使用者来说,他是需要额外功能的。

那现实是怎么解决这个问题的呢?

现实中增加了一个角色,叫中介(Proxy),你也可以叫做代理。
中介暴露出来的方法和房东那边是一样的,这样客户才会找你,你暴露出来的缺三缺四的谁还找你,代理言外之意就是表面上啥都一样,房东不愿意干的事,代理中可以去做,那些额外方法可以实现在代理中,就像中介带着客户去看房就行。
但是最后签合同这一步属于核心业务,你一个中介是没这个权限的,代理只是为了把额外业务剥离出来,不和核心代码耦合,所以此时核心业务还是要原始类去实现的,怎么实现呢?也很简单就是在中介类里声明原始类的引用,直接调呗。就是中介的类里面有一个房东的类引用,调用核心业务还是房东类调用的。
以后要是觉得代理不行了,再换一个新的代理修改实现就行了。(换个中介公司)
基于上面的分析其实我们的类结构就很清晰了,

  • 核心类:你的业务所在类
  • 代理类:里面有一些额外功能,并且持有了核心类的引用,这个引用去完成核心业务的调用

基于这个设计,于是引出了两种代理设计模式,一个是静态代理,一个是动态代理。

二、静态代理模式

1、概念

上面分析可知,我们得通过代理类为原始类(目标类)增加额外的功能
好处就是利于原始类(目标类)的维护,解开核心代码和额外代码的耦合。解开耦合自然利于维护。

2、名词解释

1、目标类|原始类
	指的是完成核心业务类那些(核心功能所在,-----》业务运算,DAO调用的)
2、目标方法|原始方法
	指的是目标类(原始类)中的方法,就是目标方法(原始方法)
3、额外功能|附加功能
	就是上面说的日志,事务,性能。
代理就是为了原始类增加额外功能的。

3、代理开发的核心要素

我们比如房东的功能类是UserService,因为我们说面向接口编程,我们一般把UserService声明成接口,里面定义核心方法,比如说签合同付钱。
UserServiceImpl implements UserService这个是实现类,就是房东的实现类,就是完成核心功能的。
前面我们说了,代理类(中介)表现出来的要和房东方法得一致,房东有的你得有,然后你才能出去拉客人,这就很自然了,代理类直接也实现原始类就行了,JAVA有这个机制直接保持。
UserServiceProxy implements UserService,然后你自己在这里添加附加功能即可。
# 代理类三要素 = 目标类(原始类)得有  + 额外功能 + 原始类(目标类)实现相同接口

多说无益,上代码。

4、编码实现

实体类

package com.yx.staticProxy;

/**
 * @author: levi
 * @description: TODO
 * @date: 2022-10-3 9:47
 * @version: 1.0
 */
public class User {
  private String name;
  private String password;
  // get set省略
}

核心类的接口,也就是原始类

/**
 * @author: levi
 * @description: 原始类接口
 * @date: 2022-10-3 9:47
 * @version: 1.0
 */
public interface UserService {

  //核心功能,注册
  public void register(User user);
  // 核心功能,登录
  public boolean login(String name,String password);
}

核心类的接口实现

/**
 * @author: levi
 * @description: 原始类实现,里面是核心业务代码
 * @date: 2022-10-3 9:48
 * @version: 1.0
 */
public class UserServiceImpl implements UserService{
  @Override
  public void register(User user) {
    System.out.println("注册这个用户" + user.toString());
  }

  @Override
  public boolean login(String name, String password) {
    System.out.println("登录用户" + name);
    return false;
  }
}

代理类,负责增强核心业务

/**
 * @description:代理类,完成额外代码的实现,同时里面调用核心业务,核心
 * 业务不是代理完成的,只是他在这用了,这样的好处就是,我们核心的代码不变
 * 你额外的想改就去改,维护方便,解开耦合。只要核心代码稳定,这个你不想用额外
 * 的都能去掉。把核心业务和额外的分开维护,互不影响。只是在代理里调用一下。以后核心改了
 * 不影响代理,代理改了不影响核心。
 * @author:levi
 * @version:1.0
 */
public class UserServiceProxy implements UserService {

  // 持有核心类的引用,spring环境的时候可以注入,不要new
  private UserServiceImpl userService = new UserServiceImpl();

  @Override
  public void register(User user) {
    // 增强日志业务
    System.out.println("---------log----------");
    // 核心操作还是核心类去做
    userService.register(user);
  }

  @Override
  public boolean login(String name, String password) {
    System.out.println("---------log----------");
    return userService.login(name, password);
  }
}

调用测试

public class Test {
  public static void main(String[] args) {
    // 这里不能用原始类调,因为客户要的是也能看到额外功能,不然你弄额外干锤子
    UserServiceProxy userServiceProxy = new UserServiceProxy();
    userServiceProxy.login("诸葛亮","123456");
    userServiceProxy.register(new User("曹操","123456"));
  }
}

输出结果为:
---------log----------
登录用户诸葛亮
---------log----------
注册这个用户com.yx.staticProxy.User@60e53b93

我们看到核心的登录是做了,增强的日志是代理类里面加的,这就是我们在没有修改核心类的基础上在代理里面对核心业务做了增强。

5、总结一下

我们至此就得到了这个代理的一个模式,
需要一个核心类,可以称之为目标类,需要被增强。
需要一个核心类里面的核心业务方法,我们需要对这个方法做增强。需要核心类去执行这个方法。
需要代理类,代理类和核心类实现一样的接口,对外暴露,因为你们的功能是要一致的。

6、静态代理存在的问题

1、静态代理类文件数量过多,不利于项目管理。
	每一个原始类起码一个代理类,大项目里几千个类能累死。
2、额外功能维护性差。
	每一个代理类中都有一个输出日志,要是有一天想改下输出方式,那一个个改,那可盖了帽了,当然了我们可以把日志输出抽成一个方法,各处调用即可.但是要是有一天不输出日志了,你还的各处去删除哪个调用。总之就是维护十分麻烦.项目大了或者涉及的面广了基本可以原地去世了。
基于这种弊端,于是动态代理模式随之而来。

三、动态代理模式

动态代理创建类的模式在java这块一般是有两种,一种是jdk的动态代理,一种是cglib的动态代理模式。

1、jdk的动态代理模式

在这里插入图片描述

我来具体解读一下上面的图:

首先我们先明确一个概念:就是代理创建的三要素,静态代理和动态代理同样适用。
1、原始对象
2、额外功能
3、代理对象和原始对象实现相同的接口,是为了迷惑调用者,让调用者看到的是一样的接口,不然人家传参你不一样,那就接不到,你说锤子。

然后我们用jdk来创建代理类,用的是java.lang.reflect.Proxy这个类,创建方法用的是newProxyInstance,就是下面这句代码,我们来分析一下,这句代码是怎么体现这三点要素的,因为没这三要素无法创建代理对象,我们就看看这句话干了什么来体现这三要素。
# Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler);

我们点进源码发现其实InvocationHandler就是处理了原始对象和额外功能的要素。是一个接口,里面有实现方法invoke。如下,
InvocationHandler invocationHandler = new InvocationHandler(){
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("****************before log******************");//额外功能
        Object result = method.invoke(userService.getClass(), args);
        return result;
    }
};
InvocationHandler的作用及时用于书写额外功能的代码,让额外功能运行在原始方法 前 后 环绕 以及抛出异常的时候
其中的返回值Object就是:原始方法的返回值。这个和那个前面的spring的动态代理一个模样。
参数解析:
# Proxy:先忽略掉,这个代表的就是最后创建的代理对象,我们目前还没到那里,先不谈,我们还没创建出来了,你说锤子。

# Method:额外功能所增加给的那个原始方法,jdk把你调用的那个原始方法传了进来,因为你是在代码中先调用方法,然后spring会把代理对象创建出来,然后其实调用这个方法的是代理对象,但是创建代理对象是需要这个方法的,才能把额外方法执行在这个原始方法中,不传进来怎么在前后写额外方法。相当于代理拿到你得原始方法,然后替你调用,此时就能给你加东西了,这就是代理的意思,就是替换了你在人家这里执行,就能前后给你增强的东西了。

# Object[]:原始方法的参数,这个其实很自然,你要调用这个原始方法,自然需要把参数也带过来,不然你调锤子。因为JDK不知道你们会传啥参数,各有各的,所以就用OBJECT的数组去接,数组都接过来。

对应以前拦截器那会的执行原始方法现在是这句话:method.invoke(userService.getClass(), args);我们分析一下,这句话是执行原始方法,你怎么才能执行一个方法呢,第一就是执行方法的对象,第二就是参数,还有就是方法名,这三个构成执行一个方法,所以invoke是方法,还要参数就是userService的对象类型,你把你要创建的那个类的接口对象传进去,这里只能传对象,不然会报错,我之前传了一个.class进去。
args就是执行方法的参数。就像userService.login("lwq","654321");只不过上面是底层的反射实现,更加灵活,可以执行login还可以是别的,你在客户端调啥这里就是啥。

所以你看到InvocationHandler这个参数其实就解决了原始对象和额外功能的两个要素。

# 接下来解决第三个,就是代理类要和原始类实现一样的接口,不然外面调你代理类,你都和人家不一样,你还代理尼玛呢。
这个部分实现靠的是,第二个参数Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler)就是Class<?>[] interfaces,这个就是你得把这个你要实现的代理的类传进来,因为你得告诉它你的接口是啥,他就会按照这个接口去实现,进而实现最后他们实现同一个接口的一个事情。那么怎么传接口呢,你把类.getclass.getInterface就能拿到它实现的接口。

#到此时我们已经解决了代理的三个要素,我们基本提供了所有的三要素的参数,但是还有最后一个问题就是,Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler)其中的第一个参数ClassLoader这个参数还没有解决。我们来按照下图的知识来解释一下。

在这里插入图片描述

我们写了一个类.java文件,后面你编译成.class字节码文件,然后你运行它实际是jvm的classloader把你这个字节码文件加载到jvm中,然后你才能通过classloader new去创建对象。那么问题来了classloader(cl)是怎么来的呢,这个其实就是jvm自动为你创建的,只要你有一个字节码.class文件,jvm就会自动为你创建一个cl。话已至此,下一个问题就出来了,我们前面说,这个动态代理是个动态字节码技术,它其实没有.java 文件(我们前面写的时候至始至终没见过这个代理类的源文件),自然也就没有.class文件,是jvm直接就把那个字节码文件加载进去虚拟机的,从始至终没有字节码文件,(我理解的是你把文件加载进jvm需要cl,但是现在你不是加载的那个流程,所以没创建出对应的cl,你是直接进去的,走后门了没有加载过程)所以jvm也就不会为你创建所谓的cl,这就很伤,你没有拿传什么进去,但是这个东西其实就是传进去实现动态代理了,其实这个没特殊,没有说非得一对一,其实这个东西可以借用一个,既然代理的没有,那找个有的不就行了,本例中我们的UserService肯定是有的,那就用它的。
Proxy.newProxyInstance这就是动态字节码技术的实现。
问题总结就是,这个参数的作用是什么呢,我们说了cl可以用加载进去的字节码文件去创建出类的CLASS对象,就是那个模板,进而new创建对象,所以这个东西传进去的作用其实就是把动态的字节码文件创建成代理对象,这个东西是创建代理对象用的,可见其重要性。而且有趣的是,因为我一开始写错了,最后代理对象没创建出来,但是调用的时候额外方法输出了,原始方法没有,可见这二者真的就是组装起来的,不是一个整体。

2、编码实现

package com.yx.drmanicProxy;

import com.yx.staticProxy.User;
import com.yx.staticProxy.UserService;
import com.yx.staticProxy.UserServiceImpl;

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

/**
 * @author: levi
 * @description: 我们来通过jdk方式创建UserService的代理对象,然后实现增强以及核心业务的调用
 * @date: 2022-10-3 10:06
 * @version: 1.0
 */
public class JdkProxy {
  public static void main(String[] args) {
    // 创建原始对象
    UserServiceImpl userService = new UserServiceImpl();

    // 动态代理的InvocationHandler接口实现
    InvocationHandler invocationHandler = new InvocationHandler(){
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("****************before log******************");
        Object result = method.invoke(userService, args);
        return result;
      }
    };
    // getClassLoader这里可以借用,我随便借一个就行,只要是app的类加载器就行
    UserService proxyInstance = (UserService) Proxy.newProxyInstance(UserService.class.getClassLoader()
      , userService.getClass().getInterfaces()
      , invocationHandler);

    // 调用
    proxyInstance.login("张飞","654321");
    proxyInstance.register(new User("关羽","123456"));
  }
}
运行结果是:
****************before log******************
登录用户张飞
****************before log******************
注册这个用户com.yx.staticProxy.User@d716361
我们看到是可以实现增强的效果的。

3、CGLIB的动态代理模式

这是spring借助第三方库CGLIB的创建。
CGlib创建动态代理的原理:父子集成关系创建代理对象,原始类作为父类,代理类作为子类,这样就可能保证2者方法一致,同时在代理类中提供新的实现(额外功能 + 原始方法)。
对于上面的总结,我这里解释一下,因为我们说代理实现有三要素,1、原始类 2、额外方法 3、代理类要和原始类实现一样的接口
其实这个实现一样的接口就是保证你们有一样的方法,以此来迷惑调用者,调用者只管传参就行,其实底下已经是代理在做了,那么对于那些没有实现接口的人家就该不活了,不能创建代理不成,所以cglib登场,是为了那些没接口的出气,其实你就是为了方法一样,不一定得实现接口,继承也可以,继承就能复写,就能在复写中增加新得功能,这不就得了。

3.1、代码实现

原始类

/**
 * @author: levi
 * @description: 原始目标类
 * @date: 2022-10-3 10:35
 * @version: 1.0
 */
public class UserService {

  public void login(String name,String password){
    System.out.println("----------- cglib login---------------");
  }

  public void register(User user){
    System.out.println("----------- cglib register---------------");
  }
}

cglib代理实现

package com.yx.drmanicProxy.cglib;

import com.yx.staticProxy.User;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * @author: levi
 * @description: cglib创建代理对象
 * @date: 2022-10-3 10:35
 * @version: 1.0
 */
public class CglibProxy {
  public static void main(String[] args) {
    // 原始对象
    UserService userService = new UserService();

        /*
            通过cglib创建动态代理对象
            之前jdk是Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler);
            现在cglib其实高度一致
            Enhancer enhancer = new Enhancer();
            enhancer.setClassLoader(CglibProxy.class.getClassLoader());
            enhancer.setSuperclass(userService.getClass());
            enhancer.setCallback(methodInterceptor);
         */

    // cglib得创建代理得类
    Enhancer enhancer = new Enhancer();
    // 设置cl,可以借一个
    enhancer.setClassLoader(CglibProxy.class.getClassLoader());
    // jdk是取得接口去创建一样得方法,这里是取得父类,那这个userService原始类得父类其实就是自己
    enhancer.setSuperclass(userService.getClass());

    // 设置额外方法得书写,其使用和传参和之前jdk得分析高度一致
    // MethodInterceptor这个之前是说过,但是此处注意,我们用的是cglib得包里类
    MethodInterceptor methodInterceptor = new MethodInterceptor() {
      @Override
      public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("----------------cglib proxy log-----------------");
        // 调用需要指定调用谁得方法,和参数,前面jdk没说,这个methodJ就是你在客户端调得方法,所以这个method就是指定得方法名
        Object result = method.invoke(userService, args);
        return result;
      }
    };
    // 设置参数
    enhancer.setCallback(methodInterceptor);

    // 创建代理类
    UserService userServiceProxy = (UserService)enhancer.create();
    userServiceProxy.login("刘备","123456");
    userServiceProxy.register(new User("孙权","123456"));
  }
}
运行结果:
----------------cglib proxy log-----------------
----------- cglib login---------------
----------------cglib proxy log-----------------
----------- cglib register---------------
我们看到完全没问题的。

四、什么时候使用动态代理

1、需要为你的功能增强额外的功能,一定是额外的,如果增强自身的一般是适配器。
2、远程代理,RPC。
3、无中生有,没有接口实现类,动态代理生成。比如mybatis。

五、总结

1、JDK动态代理:Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler);通过接口创建代理的实现了类。
2、CGlib动态代理:Enhancer 通过继承父类创建代理类。
其实这两种就是spring底层创建代理,aop的实现方式,有接口用jdk,没有接口用cglib。

而且我们看到了代理模式,只要你提供了三要素,不管哪种实现方式,其实都是基于这三要素的一个封装处理实现。而且动态代理我们看到不需要程序员自己创建一个个的代理类了,都交给了底层去处理,可以规避类爆炸的问题。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值