目录
1. 静态代理设计模式
1.1 为什么需要代理设计模式
- 在 JavaEE 分层开发开发中,哪个层次对于我们来讲最重要?
Service 层
- Service 层中包含了哪些代码?
- 核心功能(代码量较多):业务运算,DAO 调用
- 额外功n能(附加功能,不属于业务,可有可无,代码量小):事务、日志、性能 …
- 额外功能书写在 Service 层好不好?
- Service 层的调用者的角度(Controller):需要在 Service 层书写额外功能。
- 软件设计者:Service 层不需要额外功能。
- 拿现实生活中的例子来做对比,解决方案是 引入一个代理.
img1
1.2 代理设计模式
概念:通过代理类,为原始类(⽬标类)增加额外的功能 好处:利于原始类(目标类)的维护
- 名词解释
- 目标类 / 原始类:指的是 业务类 (核心功能 –> 业务运算、DAO调用)
- 目标方法 / 原始方法:目标类(原始类)中的方法就是目标方法(原始方法)
- 额外功能 / 附加功能:日志、事务、性能 …
代理开发的核心要素
- 代理类 = 目标类(原始类) + 额外功能 + 原始类(目标类)实现相同的接口
//房东 --- 目标类 public interface UserService { m1 m2 } public UserServiceImpl implements UserServiceImpl { m1 ---> 业务运算、调用DAO m2 } //---------------------------------------------------- // 中介 --- 代理类:要实现目标类相同的接口 public UserServiceProxy implements UserService { m1 m2 }
1.3 静态代理编码
静态代理:为每⼀个原始类,手工编写⼀个代理类(.java .class)
public class User {}
public interface UserService { void register(User user); boolean login(String name, String password); }
public class UserServiceImpl implements UserService { @Override public void register(User user) { System.out.println("UserServiceImpl.register 业务运算 + DAO"); } @Override public boolean login(String name, String password) { System.out.println("UserServiceImpl.login 业务运算 + DAO"); return true; } }
/** * 静态代理类编码实现 */ public class UserServiceProxy implements UserService { // 实现原始类相同的接口 private UserService 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); } }
1.4 静态代理存在的问题
- 静态类文件数量过多,不利于项目管理
- UserServiceImpl、UserServiceProxy
- OrderServiceImpl、OrderServiceProxy…
- 额外功能维护性差:在代理类中修改额外功能较为麻烦
2. Spring 动态代理开发
概念:通过代理类为原始类(目标类)增加额外功能 // 好处:利于原始类(目标类)的维护
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.9</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency>
2.2 Spring 动态代理的开发步骤(5步)
创建原始对象(目标对象)
public interface UserService { void register(User user); boolean login(String name, String password); }
public class UserServiceImpl implements UserService { @Override public void register(User user) { System.out.println("UserServiceImpl.register 业务运算 + DAO"); } @Override public boolean login(String name, String password) { System.out.println("UserServiceImpl.login 业务运算 + DAO"); return true; } }
额外功能 MethodBeforeAdvice 接口
public class Before implements MethodBeforeAdvice { /** * 作用: 把需要运行在原始方法执行之前运行的额外功能, 书写在 before 方法中 */ @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("---method before advice log---"); } }
<!-- 额外功能 --> <bean id="before" class="com.yusael.aop.Before"/>
定义 切入点:额外功能的加入 // ⽬的: 由程序员根据⾃⼰的需要,决定额外功能加入给哪个原始方法(register、login)
<!--切入点:额外功能的加入--> <!--⽬的: 由程序员根据⾃⼰的需要,决定额外功能加入给哪个原始方法(register、login)--> <!-- 简单的测试:所有方法都做为切入点,都加入额外的功能--> <aop:config> <aop:pointcut id="pc" expression="execution(* * (..))"/> </aop:config>
组装(2、3 整合)
<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"> <bean id="userService" class="com.yusael.aop.UserServiceImpl"/> <!-- 额外功能 --> <bean id="before" class="com.yusael.aop.Before"/> <!--切入点:额外功能的加入--> <!--⽬的:由程序员根据⾃⼰的需要,决定额外功能加入给哪个原始方法(register、login)--> <!-- 简单的测试:所有方法都做为切入点,都加入额外的功能--> <aop:config> <aop:pointcut id="pc" expression="execution(* * (..))"/> <!--表达的含义: 所有的方法 都加入before的额外功能--> <aop:advisor advice-ref="before" pointcut-ref="pc"/> </aop:config> </beans>
- 调用
- 目的:获得 Spring 工厂创建的动态代理对象,并进行调用
注意 : Spring 的工厂通过原始对象的 id 值获得的是代理对象 // 获得代理对象后,可以通过声明接口类型,进行对象的存储
/** * 用于测试动态代理 */ @Test public void test1() { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml"); UserService userService = (UserService) ctx.getBean("userService"); userService.login("admin", "1234"); userService.register(new User()); }
2.3 动态代理细节分析
- Spring 创建的动态代理类在哪里?
Spring 框架在运行时,通过动态字节码技术,在 JVM 创建的,运行在 JVM 内部,等程序结束后,会和 JVM 一起消失。 - 什么是 动态字节码技术?
通过第三方动态字节码框架,在 JVM 中创建对应类的字节码,进而创建对象,当虚拟机结束,动态字节码跟着消失。
结论:
- 动态代理不需要定义类文件,都是 JVM 运行过程中动态创建的;
- 所以不会造成静态代理的缺点:类⽂件数量过多,影响项目管理的问题。
img2
动态代理编程简化代理的开发
在额外功能不改变的前提下,创建其他目标类(原始类)的代理对象时,只需要指定原始(目标)对象即可。
动态代理使得额外功能的维护性大大增强。
2.4 动态代理开发详解
- 额外功能的详解
MethodBeforeAdvice 分析
MethodBeforeAdvice 接口作用:额外功能运行在原始方法执行之前,进行额外功能操作。public class Before implements MethodBeforeAdvice { /** * 作用: 把需要运行在原始方法执行之前运行的额外功能, 书写在 before 方法中 * * Method: 额外功能所增加给的那个原始方法 * login * register * -------- * showOrder * * Object[]: 额外功能所增加给的那个原始方法的参数 * String name,String password * User * -------- * * Object: 额外功能所增加给的那个原始对象 * UserServiceImpl * --------------- * OrderServiceImpl */ @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("---new method before advice log---"); } }
- before 方法的 3 个参数在实战中,该如何使用?
- before 方法的参数,在实战中,会根据需要进行使用,不⼀定都会用到,也有可能都不用。
2.5 MethodInterceptor(方法拦截器)
methodinterceptor 接口:额外功能可以根据需要运行在原始方法执行 前、后、前后。
- 参数:MethodInvocation:额外功能所增加给的那个原始方法 (login, register)
- 返回值:Object:原始方法的返回值 (没有就返回 null)
- invocation.proceed():原始方法运行
额外功能运行在原始方法 之前:
public class Around implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { System.out.println("---额外功能运行在原始方法执行之前---"); Object ret = methodInvocation.proceed(); // 原始方法运行, 获取原始方法的返回值 return ret; } }
额外功能运行在原始方法 之后:
public class Around implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { Object ret = methodInvocation.proceed(); // 原始方法运行, 获取原始方法的返回值 System.out.println("---额外功能运行在原始方法执行之后---"); return ret; } }
额外功能运行在原始方法 之前、之后:
public class Around implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { System.out.println("---额外功能运行在原始方法执行之前---"); Object ret = methodInvocation.proceed(); // 原始方法运行, 获取原始方法的返回值 System.out.println("---额外功能运行在原始方法执行之后---"); return ret; } }
额外功能运行在原始方法抛出异常的时候:
public class Around implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { Object ret = null; try { ret = methodInvocation.proceed(); // 原始方法运行, 获取原始方法的返回值 } catch (Throwable throwable) { System.out.println("---额外功能运行在原始方法抛异常的时候---"); } return ret; } }
MethodInterceptor 影响原始方法的返回值:
public class Around implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { System.out.println("---log---"); Object ret = methodInvocation.proceed(); return false; } }
2.6 切入点详解
切入点决定额外功能加入位置(方法)
<!--execution(* * (..)) 匹配了所有方法--> <aop:pointcut id="pc" expression="execution(* * (..))"/>
- execution():切入点函数
- * *(..):切入点表达式
2.7 切入点表达式
2.7.1 方法切入点
定义一个方法 public void add(int i, int j) * * ( .. )
* * (..) --> 所有方法 * ---> 修饰符 返回值 * ---> 方法名 () ---> 参数表 .. ---> 对于参数没有要求 (参数有没有,参数有⼏个都行,参数是什么类型的都行)
定义 login 、register 方法作为切入点:
<!-- 定义login作为切入点 --> <aop:pointcut id="pc" expression="execution(* login (..))"/> <!-- 定义register作为切入点 --> <aop:pointcut id="pc" expression="execution(* register (..))"/>
定义方法名为 login 且 有两个字符串类型的参数 作为切入点;
<aop:pointcut id="pc" expression="execution(* login (String,String))"/> <!-- ⾮ java.lang java.lang 包中的类型, 必须要写全限定名 --> <aop:pointcut id="pc" expression="execution(* register (com.yusael.proxy.User))"/> <!-- ..可以和具体的参数类型连用 --> <aop:pointcut id="pc" expression="execution(* login(String, ..))"/> <!-- === login(String), login(String,String), login(String,com.baizhi.edu.proxy.User) -->
精准方法切入点限定
<!-- 修饰符 返回值 包 类.方法(参数) --> <aop:pointcut id="pc" expression="execution(* com.yusael.proxy.UserServiceImpl.login(..))"/> <aop:pointcut id="pc" expression="execution(* com.yusael.proxy.UserServiceImpl.login(String, String))"/>
2.7.2 类切入点
指定 特定类作为切入点(额外功能加入的位置) ,这个类中的所有方法,都会加上对应的额外功能。
语法1
<!-- 类中所有的方法加入了额外功能 --> <aop:pointcut id="pc" expression="execution(* com.yusael.proxy.UserServiceImpl.*(..))"/>
语法2
<!-- # 忽略包 --> <!-- 1. 类只存在一级包 --> <aop:pointcut id="pc" expression="execution(* *.UserServiceImpl.*(..))"/> <!-- 2. 类存在多级包 --> <aop:pointcut id="pc" expression="execution(* *..UserServiceImpl.*(..))"/>
2.7.3 包切入点(实战中用的多)
指定 包作为额外功能加入的位置 ,自然包中的所有类及其方法都会加入额外的功能。
语法1
<!-- # 切入点包中的所有类,必须在proxy中,不能在proxy包的⼦包中 --> <aop:pointcut id="pc" expression="execution(* com.yusael.proxy.*.*(..))"/>
语法2
<!-- # 切入点当前包及其⼦包都生效 --> <aop:pointcut id="pc" expression="execution(* com.yusael.proxy..*.*(..))"/>
2.7.4 切入点函数(execution、args、within)
切入点函数:用于执行切入点表达式
exectuion
execution 是最为重要的切入点函数,功能最全;可以执行执行 方法切入点表达式、类切入点表达式、包切入点表达式; 弊端:execution 执⾏切入点表达式 ,书写麻烦execution(* com.yusael.proxy..*.*(..))
注意: 其他的 切入点函数 简化的是 execution 的书写复杂度,功能上完全⼀致。
args
args 作用:主要用于 函数(方法) 参数的匹配;<!-- 切入点:方法参数必须得是 2 个字符串类型的参数 --> <!-- # 使用 execution --> <aop:pointcut id="pc" expression="execution(* *(String, String))"/> <!-- # 使用 args --> <aop:pointcut id="pc" expression="args(String, String)"/>
within
within 作用:主要用于进行 类、包切入点表达式 的匹配。<!-- 切入点: UserServiceImpl 这个类 --> <!-- # 使用 execution --> <aop:pointcut id="pc" expression="expression(* *..UserServiceImpl.*(..))"/> <!-- # 使用 within --> <aop:pointcut id="pc" expression="within(*..UserServiceImpl)"/> <!-- 切入点: com.yusael.proxy 这个包 --> <!-- # 使用 execution --> <aop:pointcut id="pc" expression="execution(* com.yusael.proxy..*.**(..)"/> <!-- # 使用 within --> <aop:pointcut id="pc" expression="within(com.yusael.proxy..*)"/>
@annotation
作用:为具有特殊注解的 方法 加入额外功能。
例如我们自定义了一个注解:Log@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Log { }
然后我们要为使用了 Log 注解的方法加入额外功能。
<aop:pointcut id="pc" expression="@annotation(com.yusael.Log)"/>
- 切入点函数的逻辑运算(and、or)
切入点函数的逻辑运算 指的是:整合多个切入点函数⼀起配合工作,进⽽完成更为复杂的需求。and 与操作:
<!-- 案例: 方法名叫 login 同时 参数是 2个字符串 --> <!-- # execution --> <aop:pointcut id="pc" expression="execution(* login(String, String))"/> <!-- # execution and args --> <aop:pointcut id="pc" expression="execution(* login(..)) and args(String, String))"/> <!-- 注意:与操作不同⽤于同种类型的切⼊点函数 --> <!-- 以下这个是错误的, 因为不存在同时叫 login 和 register 的方法 --> <aop:pointcut id="pc" expression="execution(* login(..)) and execution(* register(..))"/>
or 或操作:
<!-- 案例: 方法名叫 register 或 login 的⽅法作为切⼊点 --> <aop:pointcut id="pc" expression="execution(* login(..)) or execution(* register(..))"/>