关于后端:十动态代理设计模式

54次阅读

共计 11423 个字符,预计需要花费 29 分钟才能阅读完成。

一、动态代理设计模式

1.1 为什么须要代理设计模式

问题: 在 javaEE 的分层中,哪个档次对于咱们来讲最重要

DAO ---》Service ---》Controller
其实是 Service 比拟重要,这是因为 Service 是解决业务的档次
DAO 其实是辅助 Service 开发的,而 Controller 是面向申请的,最终是给到 Service 的

1.2 Service 层中蕴含了哪些代码?

Service 层中 = 外围性能(几十行或上百行) + 额定性能(附加)
1. 外围性能
    业务运算: 如用户登录.....
    DAO 调用: 操作数据库
2. 额定性能
    1. 不属于业务性能
    2. 可有可无
    3. 代码量很小
    比方: 事务、日志(谁 + 工夫 + 什么事 + 后果)、性能(察看业务调用的工夫)

1.3 问题: 额定性能书写在 Service 层中好不好?

从 Service 层的调用者 Controller 的角度上来看,事务是非常重要的,事务肯定在 Service 中有
从程序的设计者来看,因为额定性能属于可有可无的性能,在某种意义上属于代码入侵,我他妈
不须要它的时候还要删除它,所以不太心愿在 service 层中有额定性能

1.4 事实问题的映射

备注: 房东 (软件设计者):不想做额定性能,老子只想收钱
房客(调用者):须要额定性能,不看广告或看房,老子才不给你钱呢

1.5 事实问题的解决方案

咱们通过引入一个代理类,去实现额定性能,而后再调用原始类的外围性能,这样就大快人心了,留神上面的中介的业务办法要和房东的业务办法名要保持一致(这样房客才会信嘛)

二. 代理设计模式

2.1 概念

通过代理类,为原始类 (指标类) 减少额定的性能
益处: 利于原始类的保护,毕竟我无需过多关怀原始类,对于额定性能我只需关怀中介类即可
并且我能够有多个中介类,这样利于代码的扩展性

2.2 名词解释

1. 指标类 原始类
  指的是 业务类(外围性能 --》业务运算 DAO 调用)
2. 指标办法 原始办法
  指标类 (原始类) 中的办法 就是指标办法
3. 额定性能

2.3 代理开发的外围因素

代理类 = 指标类(原始类) + 额定性能 + 原始类实现雷同的接口
房东 ----》public interface UserService{
                m1;
                m2;
            }
            UserServiceImpl implements UserService{
                m1; ---> 外围性能 DAO 调用
                m2;
            }
            UserServicePorxy implements UserService{
                m1;
                m2;
            }

2.4 编码 — 小试牛刀(动态代理开发)

第一步: 创立接口及其实现类

接口:
 public interface UserService  {
    // 登录
    void login(String name,String pwd);
    // 是否非法
    boolean check(String token);
}   
实现类:该类就为艰深所称的 service 层,用来实现业务和 DAO 调用的
public class UserServiceImpl implements UserService {
    @Override
    public void login(String name, String pwd) {System.out.println("UserServiceImpl.login 业务实现 + DAO 调用");
    }

    @Override
    public boolean check(String token) {System.out.println("UserServiceImpl.check 业务实现 + DAO 调用");
        return false;
    }
}

第二步: 创立代理类并实现同一个接口,该代理类带有一些额定性能,留神在这里创立指标类的对象,因为须要他的指标办法的实现

public class UserServiceProxy implements UserService {// 创立原始类 (指标类) 的对象
    private UserService userService = new UserServiceImpl();

    @Override
    public void login(String name, String pwd) {
        // 代理办法提供额定性能 + 原始类的性能
        System.out.println("-------log-------");
        userService.login(name, pwd);
    }

    @Override
    public boolean check(String token) {System.out.println("-------log-------");
        return userService.check(token);
    }
}

第三步: 测试: 咱们会发现其如期地实现了额定性能和外围性能

小结 : 所谓的动态代理就是一个 Service 类,作为程序员就必须手动给它书写一个对应的代理类
存在的问题:

1》动态类文件过多,不利于项目管理
在下面的案例中
一个 UserServiceImpl 对应一个 UserServiceProxy
一个 OrderServiceImpl 对应一个 OrderServieProxy
并且最狗血的是他们提供的额定性能都是提供日志的性能
那么如果我当前又减少了实现类,那我就就须要又减少其代理类的书写
这显著是有不利于治理的
2》额定性能维护性差
    就拿下面的例子,如果我额定的性能:打日志,我感觉我实现的不够好,想要
    改变它,然而很多中央的代码都耦合了,我要批改的话,那就要动其余中央了
    这显著是很恶心的

三、Spring 动静代理

3.1 动静代理的概念

概念: 通过代理类为原始类 (指标类) 减少额定的性能
益处: 和动态代理类一样,利于原始类的保护,同时对比动态代理类有更加解耦合的优化实现

3.2 动静代理开发步骤

3.2.1 搭建开发环境

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-aop</artifactId>
   <version>5.1.14.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjrt</artifactId>
   <version>1.8.8</version>
</dependency>
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>1.8.3</version>
</dependency>

3.2.2 思考一个问题

从失常的逻辑登程,咱们把一个货色放在另一个货色下面,咱们须要什么?咱们闭上眼睛想想,显然肯定有一个被放物,一个放在被放物的物体上,这里犹如下面所举例的房东和中介,没有了房东,天然不会有中介。有了这个两个物体后,那么进一步的就是“被放物”到底是什么,有哪些货色?
所以一套 aop 的开发流程包含:原始类、切入类及其办法、切入点(即搁置哪个原始类上)
1. 创立原始类对象(指标类对象) 并 注入

public class UserServiceImpl implements UserService {
    @Override
    public void login(String name, String pwd) {System.out.println("UserServiceImpl.login 业务实现 + DAO 调用");
    }

    @Override
    public boolean check(String token) {System.out.println("UserServiceImpl.check 业务实现 + DAO 调用");
        return false;
    }
}
<!--1. 注入一个原始类 --> 
<bean id="user" class="cn.paul.spring.demo.hellospring.service.impl.UserServiceImpl"/>

2. 创立切入点类 (加强类) 并注入
备注: 通过实现 MethodBeforeAdvice 接口的形式
额定性能书写在接口的实现中,运行在原始办法执行之前运行额定的性能

public class before implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {System.out.println("------method before advice log -----"); // 相当于动态代理类的打印日志
    }
}
<!--2. 注入前置加强类 -->
<bean id="before" class="cn.paul.spring.demo.hellospring.service.dynamic.before"/>

3. 定义切入点并关联切入点类

什么是切入点?切入点就是额定性能退出的地位
目标: 因为这里是写配置文件的,所以能很好地通过配置的切换来实现对不同的原始类进行切入
<!--3. 配置切入点(通知 Spring 要对谁进行加强)-->
<aop:config>
    <aop:pointcut id="pc" expression="execution(* *(..))"/>
<!--4. 配置加强类(通知 Spring 你的加强类是什么)-->
    <aop:advisor advice-ref="before" pointcut-ref="pc"/>
</aop:config>

备注: 其实下面的配置能够写成上面
<aop:config>
    <aop:advisor advice-ref="before" pointcut="execution(* *(..))"/>
</aop:config>
那么咱们来想一下,为什么这里要多做一步来对它进行援用?其实很简略,假如我有另外一个切入类,它的切入点地位是另一个表达式,那么我就能够
切换过去啊,并且通过 ref 的利用,这样当前若有多个切入类都是这个地位切入,我就间接
援用即可,无需反复写

4. 测试你的代码吧
蹩脚,报错了

这里报错的起因是因为我疏忽了一些重要细节

1. Spring 工厂通过原始对象的 id 值取得的是代理对象,它和下面的实现类都实现同一个
接口,最多算兄弟关系,所以没有继承关系,无奈实现互转呢!2. 获取代理对象后,能够通过申明接口类型,来对这个代理类对象进行存储

下图通过 debug 的形式发现,通过工厂获取的的确是代理类对象,还是 jdk 的动静代理呢~

小问题: 如果我心愿的是通过 CGLIB 的形式来创立代理类呢?很简略只须要在配置文件的参数配置即可

<aop:config proxy-target-class="true">
    <aop:advisor advice-ref="before" pointcut="execution(* *(..))"/>
</aop:config>

四、动静代理细节剖析

4.1 Spring 创立的动静代理在哪里?

首先咱们回顾一下动态代理,动态代理是每创立一个实现类就要有一个对应的代理类与之对应
那么 Spring 的动静代理类在哪里呢?其实任何程序跑的都不是.java 文件,而是其编译的后果.class 文件,简称机器码文件
在 Spring 框架在运行时,通过动静字节码技术,在 JVM 创立动静代理类对象,该对象是
运行在 JVM 外部的,当程序完结了,会和 JVM 一起隐没

4.2 什么叫动静字节码技术

所谓的动静字节码技术就是通过动静字节码框架(如下图),在 JVM 中创立对应类的字节码,进而创立
对象,留神创立的对象是通过字节码去做的,当虚拟机完结,动静字节码跟着隐没

4.3 动静代理比动态代理优于哪里?

动静代理不须要定义类文件,都是 JVM 运行过程中动态创建的,所以不会造成像动态代理那样
代理类文件过多,影响项目管理的问题
其次,动静代理还有一个比拟不便的点就是,只须要关怀原始类,在额定性能不变的前提下,间接通过注入对象,即可实现
最初,请你思考一个问题:如果我感觉这个代理类不好用了,我想换其余实现,应该怎么办呢?其实咱们不须要删除原来的办法,咱们只须要定义一个新的代理类即可,因为这样才合乎开闭准则

五、MethodBeforeAvice 详解

public class before implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {System.out.println("------method before advice log -----"); // 相当于动态代理类的打印日志
        if ((objects[0]) instanceof User) {User user = (User) (objects[0]);
            System.out.println("user.getPwd() =" + user.getPwd());
            System.out.println("user =" + user);
        }
    }
}
参数解释
Method:示意原始办法或指标办法,比方 login 办法、register 办法
Objects[]:原始办法的参数,如 String name,String password/User user
Object: 额定性能所减少给的按个原始对象 UserServiceImpl、OrderServiceImpl

六、MethodInterceptor 详解

从下面咱们晓得实现 MethodBeforeAdvice 办法能够实现在原始办法执行之前实现执行实现额定性能,然而如果我想执行工夫是之后呢?
其实这个接口的实现有点像盘绕加强,执行的机会点在之前后之后都是能够的

6.1 实现步骤

第一步: 定义一个类实现 MethodIntercetor 接口,并实现其办法(留神看外面的正文)

public class Around implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {System.out.println("---- 这里写运行在原始办法之前的额定性能 -----");
        Object ret = methodInvocation.proceed();// 该行即为原始办法的运行,返回值为原始办法的返回值,如果办法没有返回值,则为 null
        System.out.println("---- 这里写运行在原始办法之后的额定性能 -----");
        return ret;
    }
}
备注:
MethodInvocation 有点相似 MethodBeforeAdvice 办法的参数 Method, 外面蕴含一些对原始办法的属性的封装
Object:为原始办法的返回值

第二步: 配置文件注入,并组装

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--1. 注入一个原始类 -->
    <bean id="user" class="cn.paul.spring.demo.hellospring.service.impl.UserServiceImpl"/>
    <!--2.1 测试盘绕加强类 -->
    <bean id="around" class="cn.paul.spring.demo.hellospring.service.dynamic.Around"/>
    <!--3. 配置切入点(通知 Spring 要对谁进行加强)-->
    <aop:config>
        <aop:pointcut id="pc" expression="execution(* *(..))"/>
    <!--4. 配置加强类(通知 Spring 你的加强类是什么)-->
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>
</beans>

第三步: 测试 (还记得吗?调用工厂的 getBean 办法即可获取其对应的代理类)

@Test
void test02() {ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationAopContext.xml");
    UserService userService = ((UserService) ctx.getBean("user"));
    //userService.check("token");
    User user = new User();
    user.setPwd("abcd");
    userService.checkPerson(user);
}

第四步: 输入后果

---- 这里写运行在原始办法之前的额定性能 -----
运行 checkPerson 办法.....
---- 这里写运行在原始办法之后的额定性能 -----

6.2 问题阐释

6.2.1 什么样的额定性能 运行在原始办法执行之前,之后都要增加呢?

记住之前所说的额定性能无非: 日志、事务、性能
其实事务就是一个很好的例子,在原始办法执行之前 transaction.begin(),
而后在原始办法执行之后进行 transaction.commit()
还有性能也是能够的,比方在原始办法执行之前记录一个工夫戳,而后在原始办法执行之后
记录一个工夫戳,两者一减,就能看到调用这个办法用了多长时间

6.2.2 那日志用在什么中央呢?

其实看你怎么用啦,如果是想记录这个原始办法抛出异样后记录一个日志的话,那就能够在原始办法抛出异样的时候进行捕捉并记录日志,如上面的代码
public class Around implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Object ret = null;
        try {ret = methodInvocation.proceed();
        } catch (Exception ex) {System.out.println("AOP 捕捉原始办法的异样,记录日志中.....");
        }
        return ret;
    }
}

6.2.3 MethodInterceptor 影响原始办法的返回值

咱们晓得 invoke 办法的返回值就是这个代理办法的返回值,proceed 办法的返回值为原始
办法的返回值,那么咱们当咱们通过 AOP 实现后,如果想影响原始办法的返回值,能够间接
操作 invoke 办法的返回值,而后返回一个本人想要的值,然而个别不倡议这么用,因为原本
代理性能的理论就是为了实现额定性能和外围性能的解耦的,这是属于精益求精的货色,不能
因而还扭转外围办法的性能

七、切入点详解

咱们晓得动静代理有四个步骤是重要的: 原始办法、额定性能、切入点、组装,那么这个切入点到底是什么呢?
这个切入点能够了解为是一个函数表达式,这个表达式申明的是代理办法应该放在哪里。上面为一个 demo

<aop:pointcut id="pc" expression="execution(* *(..))"/>
execution(* *(..)) ===> 示意匹配所有的办法
1.execution() 切入点函数
2.* *(..) 切入点表达式

7.1 办法切入点表达式

咱们先定义一个办法
public void     add(int i ,int j)
     *           *(..)   
* *(..) ---> 所有办法
第一个 *  ---> 修饰符 返回值
第二个 *  ---> 办法名
() ---> 参数表
.. ---> 对于参数的数量、类型没有要求

7.2 一些办法的定义 demo

1. 定义 login 办法作为切入点
* login(..)
2. 定义 register 作为切入点
* register(..)

7.3 略微简单一点的切入点表达式

1. 定义一个 login 办法,且办法由两个字符串类型的参数作为切入点
* login(String,String)
留神: 
1.1 对于非 java.lang 包中的类型,必须写全限定名,如下
* checkPerson(cn.paul.spring.demo.hellospring.entity.User)
=====
1.2 还有一种类型可变数组的那样的写法,如下
* login(String,..)
下面这个表达式象征不关注参数的个数和类型,上面的这三个状况都符合要求
* login(String),login(String,String),login(String,cn.paul.spring.demo.hellospring.entity.User)

7.4 更加精准地限定办法名称的切入点表达式

首先咱们思考一下,如何精准地定位到一个办法?
其实咱们定位一个办法能够通过 包 —> 类 —> 办法名 的模式来找到那个办法,所以上面的图就阐明了之前得通配符切入点表达式的个别表征与指向

备注: 第二个 * 代表指向任意的包或类或办法
实战一下

问:如果我心愿指向特定类的 login 办法(可能有多个类都含有 login 办法),怎么写切入点表达式呢?============
修饰符 返回值       包. 类. 办法(参数)
      *             cn.paul.spring.demo.hellospring.service.impl.UserServiceImpl.login(..)) 
或
      *             cn.paul.spring.demo.hellospring.service.impl.UserServiceImpl.login(String,String))   
问: 如果我心愿特定的类的全副办法都蕴含呢?*             cn.paul.spring.demo.hellospring.service.impl.UserServiceImpl.*(..))   

7.5 类切入点

什么是类切入点?

指定特定类作为切入点(额定性能退出的地位),天然这个类中的所有办法都会被代理,被加上
对应的额定性能

7.5.1 语法

1. 类中的所有办法都退出特定的额定性能
* cn.paul.spring.demo.hellospring.service.impl.UserServiceImpl.*(..))  

问题: 有没有想过一个场景,在不同的包下都有叫 UserServiceImpl 的类,并且我心愿他们都能够被代理?

如问题所述,咱们能够将包的地位设为通配符,然而有些留神点要如下留神
1. Spring 不会一个个包下帮你递归地去找是否有合乎的类,包的通配符应用只限定了
一级包下的类,表达式如下
* *.UserServiceImpl.*(..) =》只对图 1 无效,对图 2 有效
------------
如果我心愿对图 2 无效,那表达式该怎么写呢?如下
* *..UserServiceImpl.*(..) =》对类存在多级包失效 (慎用!)
咱们发现在包的通配符地位前面多了两个点,示意两头含有多层的包,让 Spring 递归地去找!

上面为表达式对应地位的阐明

7.6 包切入点 (最具实战性)

指定包下的全副类及其办法退出额定性能

7.6.1 语法 1

# 切入点包中的所有类,必须在 impl 包中,不能在 impl 包的子包中
* cn.paul.spring.demo.hellospring.service.impl.*.*(..))

7.6.2 语法 2

# 切入点以后包及其子包都失效
* cn.paul.spring.demo.hellospring.service.impl..*.*(..))

7.7 切入点函数

7.7.1 什么是切入点函数?

你了解函数吗?我的了解的函数就是它就像一个独特的运算流程,咱们给它一个定量,它给
咱们返回另一个值,而不同的函数可依据其名称做辨别
所以切入点函数就是咱们给定一个值 (切入点表达式) 给它,它运算到对应的流程,去到指定的地位去做代理,从程序员的角度上来看就是用于执行切入点表达式的

7.7.2 有哪些切入点函数?

  1. execution
这个函数最为重要,能笼罩的状况是最多的
它能够执行:1. 办法切入点表达式 2. 类切入点表达式 3. 包切入点表达式
弊病: 书写会绝对麻烦,不过因为全,麻烦一点也是能够承受的
留神: 其余切入点函数,只是简化版的 execution 表达式,在性能上是完全一致的
  1. args
这个函数次要用于函数 (办法) 参数的匹配,意思就是这个执行函数更关注的是【参数】比方:我心愿切入的是办法参数必须是 2 个字符串类型的参数

1. 通过 execution 函数是这样写的
execution(* *(String,String))

2. 通过 args 函数是这样写的
args(String,String)
  1. within
次要用于进行类、包切入点表达式的匹配
比方切入点:UserServiceImpl 这个类
1. 通过 execution 函数是这样写的
execution(* *..UserServiceImpl.*(..))
1.1 通过 within 函数是这样写的
within(*..UserServiceImpl)
-------------------------------------------
比方切入点为 impl 这个包下的所有类的所有办法
2.1 通过 execution 函数是这样写的
execution(* cn.paul.spring.demo.hellospring.service.impl..*.*(..))
2.2 通过 within 函数是这样写的
within(cn.paul.spring.demo.hellospring.service.impl..*)
  1. annotation
作用: 为具备非凡注解的办法退出额定性能, 即哪个办法贴了这个注解就对该办法退出额定性能
1. 创立一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
2. 通过配置文件进行申明
<aop:pointcut id="pc" expression="@annotation(cn.paul.spring.demo.hellospring.annotation.Log)"/>
  1. 切入点函数的逻辑运算
    指的是 整合多个切入点函数一起配合工作,进而实现更为简单的需要
    5.1 and 与操作(代表一种须要同时成立的运算)
案例: 对参数为 2 个字符串的 login 办法进行切入
1. 应用 execution 函数表达式
execution(* *login(String,String))
2. 应用 and 与操作
思路: execution 函数善于的是从办法的角度去表白,而 args 是善于从办法的参数的角度,所以能够将两者联合起来
execution(* login(..)) and args(String,String)
留神: and 无奈操作同种类型的切入点函数,如下
execution(* login(..)) and execution(* register(..))
为什么? 因为 and 代表着一种 "同时成立", 试问有哪个办法既叫 login 办法,同时也叫 register 办法呢?没有吧....
那么如何实现就只针对 login 办法和 register 办法的切入呢?=》应用 or 逻辑符号
execution(* login(..)) or execution(* register(..))

5.2 or 或操作 (代表一种须要其一成立即可的运算)
具体见 and 操作符的 "留神"

八、动静代理小结

正文完
 0