1、为什么需要代理
AOP使用的设计模式就是 代理设计模式
在日常中的开发大家都是这样的:
- controller->service->dao
在这三层中最重要的就是service层,这是因为我们的具体业务逻辑都是写在service层中的。但是随着我们的业务逻辑越来越多,越来越复杂,我们的service层中的代码也变得越来越臃肿。
那么有没有办法改进,让service层只关心我们的核心业务,其他的附加操作抽取出来呢?答案是有得,那就是我们的 代理设计模式。我们的service类只用写具体的核心业务,其他的功能交由我们的代理类来进行完成。
- 那么我们生活中有没有使用代理模式的例子呢?
当然有,假设我们现在需要买房子,至于去发布卖房子的信息,已经寻找要买房子的人,这些都不是我们需要关心的事情,我们只需要做好签合同(核心业务)的工作即可。其他交由中介来帮助我们处理,这就是代理。
2、代理设计模式
2.1、代理的概念
- 概念:通过代理类,为原始类增加指定功能
- 好处:利于原始类的维护
2.2、专业名词
- 原始类:原始的业务类
- 原始方法:原始业务类中具体的核心代码
- 额外功能:附加功能,比如,日志、事务等
2.3、代理的核心要素
代理类 = 原始类 + 额外功能 + 实现相同的借口
bean对象
public class User { private String loginName; private String password; }
原始类:
public interface UserService { public void register(User user); public boolean login(String loginName, String password); }
原始类的实现类:
public class UserServiceImpl implements UserService { @Override public void register(User user) { System.out.println("UserServiceImpl.register"); } @Override public boolean login(String loginName, String password) { System.out.println("UserServiceImpl.login"); return false; } }
代理实现(后期我们可以用spring来借口这里的new对象操作):
public class UserServiceProxy implements UserService { private UserServiceImpl userService = new UserServiceImpl(); @Override public void register(User user) { System.out.println("---------register.log------------"); userService.register(user); } @Override public boolean login(String loginName, String password) { System.out.println("-----------login.log-------------"); return userService.login(loginName, password); } }
我们发现,这样我们可以轻松的实现对原始类的增强,但是如果我们有十几个原始类,是不是这种增强的类也要重复十几次呢?
2.4、静态代理存在的问题
- 静态类的文件过多,如果我们有100个service原始类,我们现在要对这100个原始类进行增强,那么我们是不是也要写100个代理类呢?这显然违背了我们的初心
- 如果说我们现在代理类中的方法需要进行修改,我们的100个代理类是不是都要修改呢?这显然也是不好的
3、动态代理
由于上面静态代理出现的问题,Spring为我们提供了一个动态代理
3.1、动态代理概念
- 概念:增强原始类
- 好处:利于原始类的维护
3.2、环境搭建
使用spring的动态代理,我们需要加入这些maven坐标(大家不要忘记了导入之前spring的spirng-context的坐标哦)
<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>
创建原始对象
public class UserServiceImpl implements UserService { @Override public void register(User user) { System.out.println("UserServiceImpl.register"); } @Override public boolean login(String loginName, String password) { System.out.println("UserServiceImpl.login"); return false; } }
<bean id="userService" class="com.wx.service.impl.UserServiceImpl"></bean>
创建代理类
public class LogBefore implements MethodBeforeAdvice { @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("----------LogBefore.before---------------"); } }
<bean id="logBefore" class="com.wx.proxy.LogBefore"></bean>
将我们的原始类和代理类都交由Spring来进行管理,下面我们就需要给他们建立一个连接
<aop:config > <aop:pointcut id="pc" expression="execution(* *(..))"/> <aop:advisor advice-ref="logBefore" pointcut-ref="pc"></aop:advisor> </aop:config>
定义我们的切入点(这个切入点书写的规则,会面会具体介绍),针对我们的哪些类进行代理增强
4、动态代理分析
4.1、Spring创建的动态代理类
动态代理技术是通过第三方框架,在JVM中创建对应类的字节码,进而创建创建,当虚拟机结束,动态字节码跟着消失。
以前我们运行java类,首先需要有java代码,编译长class文件,然后在JVM启动的时候加载进来,开始运行它的一些方法
那么动态代理,我们现在没有java类,没有class文件,那我们需要如何创建一个字节码,丢给JVM编译呢?这就需要第三方动态字节码框架来帮助我们实现,这些可以直接在JVM中创建动态对象
总结:动态代理不需要定义类文件,都是JVM动态创建的,所以不会造成静态代理、类文件过多,和影响项目管理的问题。
4.2、动态代理开发步骤分析
我们使用Spring的动态代理一共做了以下几个步骤
- 编写原始对象
- 编写代理类
- 编写配置文件,写切入点,我们的代理对象增强哪些方法
- 将我们的代理类和切入点进行关联
4.3、动态代理的可维护性
之前的LogBefore代理类,现在无法满足我们的需求了。这个时候咋办呢?我们需要重新创建一个新的代理类
public class LogBeforeNew implements MethodBeforeAdvice { @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("----------LogBeforeNew.before---------------"); } }
然后需要将我们的配置文件进行修改下即可
<bean id="logBeforeNew" class="com.wx.proxy.LogBeforeNew"></bean> <aop:config > <aop:pointcut id="pc" expression="execution(* *(..))"/> <aop:advisor advice-ref="logBeforeNew" pointcut-ref="pc"></aop:advisor> </aop:config>
将我们的的advice-ref换成我们新的代理类
5、动态代理详解
5.1、MethodBeforeAdvice接口
上面我们已经写完了代理类,现在我们需要来具体看看代理类接口中before()方法中各个参数的含义
public void before(Method method, Object[] objects, Object o)
- method:调用的方法
- objects:调用方法的参数,数组接收
- o:原始对象
Spring在方法中将对象传递给我们了,至于我们如何使用,就需要看具体的业务逻辑了
5.2、MethodInterceptor接口
上面的MethodBeforeAdvice接口只能在方法调用钱进行增强,如果我们想要在院士对象前后做处理怎么办呢?这个时候,我们就需要使用MethodInterceptor接口了
public class Around implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { System.out.println("Around.invoke.before"); Object proceed = methodInvocation.proceed(); System.out.println("Around.invoke.after"); return proceed; } }
将Around配置到配置文件中
<bean id="around" class="com.wx.proxy.Around"></bean> <aop:config > <aop:pointcut id="pc" expression="execution(* *(..))"/> <aop:advisor advice-ref="around" pointcut-ref="pc"></aop:advisor> </aop:config>
大家看,是不是在我们调用原始对象方法的前后都做了增强。典型的使用场景:事务
针对异常原始方法中抛出异常,我们的代理类是否可以感知到呢?
public class UserServiceImpl implements UserService { @Override public boolean login(String loginName, String password) throws RuntimeException{ System.out.println("UserServiceImpl.login"); throw new RuntimeException(); } }
public class Around implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { System.out.println("Around.invoke.before"); Object proceed = null; try { proceed = methodInvocation.proceed(); }catch (RuntimeException e){ System.out.println("Around.invoke.throws Exception"); e.printStackTrace(); } System.out.println("Around.invoke.after"); return proceed; } }
可以发现,我们的代理类是可以捕获原始对象抛出的异常,这样,我们就可以对原始对象抛出的异常,进行处理
invoke方法的返回值,可以使用我们调用原始对象方法的返回值,也可以针对不同的情况,对这个返回值进行修改。
6、切入点详解
切入点的意思是,我们的代理类需要增强哪些方法?那么我们之前是如何配置的呢
<aop:pointcut id="pc" expression="execution(* *(..))"/>
我们只在配置文件中加入了这个切入点,我们的代理类就可以增强我们所有的方法。
- execution():代表切入点函数
- * *(..):代表表达式
6.1、切入点表达式
我们普通的一个类的结构是这样的:public boolean login(String loginName, String password)
拆解一下,第一部分public boolean,第二部分login,第三部分(String loginName, String password)
刚好和我们的切入点表达式对应:第一部分 * ,第二部分 * ,第三部分(..)。 * 号代表通配符,所有的意思
所以我们的切入点表达式 * *(..) 就是代表所有的方法
那如果现在只想针对login方法增强,那么我们应该如何来操作呢??
<aop:config > <aop:pointcut id="pc" expression="execution(* login(..))"/> <aop:advisor advice-ref="around" pointcut-ref="pc"></aop:advisor> </aop:config>
那如果我们想要更加精确怎么办呢?我们还可以使用这种表达式形式
<aop:config > <aop:pointcut id="pc" expression="execution(* com.wx.service.impl.UserServiceImpl.register(..))"/> <aop:advisor advice-ref="around" pointcut-ref="pc"></aop:advisor> </aop:config>
第二部分的方法加上全路径限定,这样可以更加精准的指定需要代理增强的方法
7、切入点函数
用户执行切入点表达式
7.1、execution
需要我们手动写切入点表达式,书写起来有点麻烦。具体可以参照上面的代码回顾下
7.2、args
针对于方法的参数进行匹配
需求:我现在只想针对所有方法中参数是两个String的方法进行增强
如果使用切入点表达式: * *(String,String)
但是使用args就比较简单:args(String,String)
<aop:config > <aop:pointcut id="pc" expression="args(String,String)"/> <aop:advisor advice-ref="around" pointcut-ref="pc"></aop:advisor> </aop:config>
如果我们使用的不是基本类型,那么我们就需要指定类型的全路径包名,来确保Spring可以找到这个类
7.3、within
主要针对于类、包来进行匹配
<aop:config > <aop:pointcut id="pc" expression="within(com.wx.service.impl.UserServiceImpl)"/> <aop:advisor advice-ref="around" pointcut-ref="pc"></aop:advisor> </aop:config>
7.4、annotation
为加上了特殊注解的方法进行匹配
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyLog { }
<aop:config > <aop:pointcut id="pc" expression="@annotation(com.wx.annotation.MyLog)"/> <aop:advisor advice-ref="around" pointcut-ref="pc"></aop:advisor> </aop:config>
public class UserServiceImpl implements UserService { @Override public void register(User user) { System.out.println("UserServiceImpl.register"); } @MyLog @Override public boolean login(String loginName, String password) throws RuntimeException{ System.out.println("UserServiceImpl.login"); throw new RuntimeException(); } }
7.5、切入点函数运算逻辑
<aop:config > <aop:pointcut id="pc" expression="execution(* login(..)) or args(String,String)"/> <aop:advisor advice-ref="around" pointcut-ref="pc"></aop:advisor> </aop:config>
注意这里 and or 的关系,还有execution() 和execution()中间不允许使用and来连接
8、总结
我们只需要记住aop的四个步骤:
- 目标对象:被代理对象的核心类、方法
- 额外功能:你要增强的功能是什么
- 切入点:作用在什么方法上,比如编写切入点函数
- 组装:让spring帮助我们创建代理对象