Spring基础-AOP代理

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的动态代理一共做了以下几个步骤

  1. 编写原始对象
  2. 编写代理类
  3. 编写配置文件,写切入点,我们的代理对象增强哪些方法
  4. 将我们的代理类和切入点进行关联

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帮助我们创建代理对象
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值