Java | Spring框架学习笔记--(2)AOP

系列文章:
Java | Spring框架学习笔记–(1)工厂
Java | Spring框架学习笔记–(2)AOP
Java | Spring框架学习笔记–(3)持久层整合
Java | Spring框架学习笔记–(4)MVC框架整合
Java | Spring框架学习笔记–(5)注解编程

Spring笔记(2) AOP

静态代理设计模式

概念:通过代理类,为原始类增加额外功能
好处:利于原始类的维护

名词解释:

1. ⽬标类 原始类
	指的是 业务类 (核⼼功能 --> 业务运算 DAO调⽤)
2. ⽬标⽅法,原始⽅法
	⽬标类(原始类)中的⽅法 就是⽬标⽅法(原始⽅法)
3. 额外功能 (附加功能)
	⽇志,事务,性能

代理类的核心要素:

  • 原始类
  • 额外功能
  • 和原始类实现同一接口

静态代理:为每⼀个原始类,⼿⼯编写⼀个代理类

代码

  1. 接口:

    public interface UserService {
    
        void register();
    
        void login();
    
    }
    
    
  2. 原始类

    public class UserServiceImpl implements UserService{
        @Override
        public void register() {
            System.out.println("UserServiceImpl.register");
        }
    
        @Override
        public void login() {
            System.out.println("UserServiceImpl.login");
        }
    }
    
  3. 代理类(和原始类实现同一个接口)

    public class UserServiceProxy  implements UserService{
        UserService userService = new UserServiceImpl();
    
        @Override
        public void register() {
            System.out.println("代理");
            userService.register();
        }
    
        @Override
        public void login() {
            System.out.println("代理");
            userService.login();
        }
    }
    
  4. 测试

public class TestProxy {
    @Test
    public void test(){
        UserService userService = new UserServiceProxy();
        userService.login();
        userService.register();
    }
}
  1. 输出

    代理
    UserServiceImpl.login
    代理
    UserServiceImpl.register
    
    Process finished with exit code 0
    

    (可以看到,既实现了额外功能,也实现了原始功能)

存在的问题

  1. 静态类文件数量过多,不利于项目管理
    有一个UserServiceImpl就有一个UserServiceProxy
  2. 额外功能维护性差

Spring的动态代理

环境搭建

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.3.1</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.9</version>
</dependency>

开发步骤

  1. 创建原始对象(目标对象)

    public class UserServiceImpl implements UserService{
        @Override
        public void register() {
            System.out.println("UserServiceImpl.register");
        }
    
        @Override
        public void login() {
            System.out.println("UserServiceImpl.login");
        }
    }
    
    
    <bean id="userService" class="com.prince.proxy.UserServiceImpl" />
    
  2. 创建一个类来实现MethodBeforeAdvice接口,类里面的方法里实现额外功能

    public class Before implements MethodBeforeAdvice {
        /**
         * 需要把运⾏在原始⽅法执⾏之前运⾏的额外功能,书写在before⽅法中
         * @param method
         * @param objects
         * @param o
         * @throws Throwable
         */
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("Before.before");
        }
    }
    
    <bean id="before" class="com.prince.proxy.Before" />
    
  3. 定义切入点,并组装

    	<aop:config>
            <!--
                这里的id随便起,
                expression:execution()括号里面是切入点表达式
            -->
            <aop:pointcut id="pc" expression="execution(* *(..))"/>
    
            <!--
                advice-ref:指的是那个代理对象的id
                pointcut-ref:对应<aop:pointcut>的id
                当<aop:pointcut>里配置的expression满足条件的时候,执行before方法。
            -->
            <aop:advisor advice-ref="before" pointcut-ref="pc"/>
        </aop:config>
    
  4. 测试
    通过getBean(),按照原来的方式传入id值,获取的就是代理对象。

    @Test
    public void test(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.register();
        userService.login();
    }
    

细节分析

  1. Spring创建的动态代理类在哪里?

    Spring框架在运⾏时,通过动态字节码技术,在JVM创建的,运⾏在JVM内部,等程序结束后,会和JVM⼀起消失
    什么叫动态字节码技术:通过第三个动态字节码框架(ASM Javassist Cglib),在JVM中创建对应类的字节码,进⽽创建对象,当虚拟机结束,动态字节码跟着消失。
    结论:动态代理不需要定义类⽂件,都是JVM运⾏过程中动态创建的,所以不会造成静态代理,类⽂件数量过多,影响项⽬管理的问题。
    
  2. 动态代理编程简化代理的开发
    在额外功能不改变的前提下,创建其他⽬标类(原始类)的代理对象时,只需要指定原始(⽬标)对象即可。
    (定义切入点的时候并不指定哪个类,只要切入点表达式和方法匹配就可以了)

  3. 维护性大大增强

MethodBeforeAdvice

public class Before implements MethodBeforeAdvice {
    /**
     * 需要把运⾏在原始⽅法执⾏之前运⾏的额外功能,书写在before⽅法中
     * @param method	指的是额外功能增加给的那个原始方法
     * @param objects	方法的参数
     * @param o			原始对象(比如UserService)
     * @throws Throwable
     */
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("Before.before");
    }
}

MethodInterceptor

MethodBeforeAdvice有一个局限性:只能运行在方法执行之前

MethodInterceptor是一个更高级的接口,可以将额外功能加在方法执行之前,也可以加在方法执行之后,也可以同时加在两个地方

注意导入包的时候导入的是org.aopalliance.intercept.MethodInterceptor

public class Around implements MethodInterceptor {
    /**
     * MethodInterceptor方法拦截器,将所需要实现的额外功能加在invoke方法里
     * @param methodInvocation 相当于`MethodBeforeAdvice`的Method参数,这个更高级一点,在方法里面我们要手动调用methodInvocation.proceed()来执行原始方法
     * @return 返回值,直接返回原始方法的返回值就行了。
     * @throws Throwable
     */
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("-----------方法执行之前的额外功能写在这-----------");
        Object retVal = null;  
        try {
            retVal = methodInvocation.proceed();//执行方法
        } catch (Throwable throwable) {
            System.out.println("-----------方法执行且抛出异常后的额外功能写在这-----------");
        } finally {
            System.out.println("-----------方法执行finally后的额外功能写在这-----------");
        }
        System.out.println("-----------方法执行之后的额外功能写在这-----------");
        return retVal;
    }
}

配置也是和之前一样的那4个步骤

    <bean id="userService" class="com.prince.proxy.UserServiceImpl" />

<!--    <bean id="before" class="com.prince.proxy.Before" />-->

    <bean id="around" class="com.prince.proxy.Around"/>

    <aop:config>
        <!--
            这里的id随便起,
            expression:execution()括号里面是切入点表达式
        -->
        <aop:pointcut id="pc" expression="execution(* *(..))"/>

        <!--
            advice-ref:指的是那个代理对象的id
            pointcut-ref:对应<aop:pointcut>的id
            当<aop:pointcut>里配置的expression满足条件的时候,执行before方法。
        -->
<!--        <aop:advisor advice-ref="before" pointcut-ref="pc"/>-->
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>

输出(可以看到2个方法都有被代理)

-----------方法执行之前的额外功能写在这-----------
UserServiceImpl.register
-----------方法执行finally后的额外功能写在这-----------
-----------方法执行之后的额外功能写在这-----------
-----------方法执行之前的额外功能写在这-----------
UserServiceImpl.login
-----------方法执行finally后的额外功能写在这-----------
-----------方法执行之后的额外功能写在这-----------

切入点表达式

定义切入点的时候,表达式* *(..) 就是切入点表达式。
* *(..) 表示匹配所有的方法。

<aop:pointcut id="pc" expression="execution(* *(..))"/>
第一个* -- 修饰符,返回值
第二个* -- 方法名
() -- 参数表
.. -- 对参数没有要求
  1. 方法切入点表达式

    *  *(..)  --> 所有方法
    
    * ---> 修饰符 返回值
    * ---> 方法名
    ()---> 参数表
    ..---> 对于参数没有要求 (参数有没有,参数有几个都行,参数是什么类型的都行)
    
    • 定义login方法作为切入点

      * login(..)
      
      # 定义register作为切入点
      * register(..)
      
    • 定义login方法且login方法有两个字符串类型的参数 作为切入点

      * login(String,String)
      
      #注意:非java.lang包中的类型,必须要写全限定名
      * register(com.baizhiedu.proxy.User)
      
      # ..可以和具体的参数类型连用
      * login(String,..)  --> login(String),login(String,String),login(String,com.baizhiedu.proxy.User)
      
    • 精准方法切入点限定

      修饰符 返回值         包.类.方法(参数)
      
          *               com.baizhiedu.proxy.UserServiceImpl.login(..)
          *               com.baizhiedu.proxy.UserServiceImpl.login(String,String)
      
  2. 类切入点

    指定特定类作为切入点(额外功能加入的位置),自然这个类中的所有方法,都会加上对应的额外功能
    
    • 语法1

      #类中的所有方法加入了额外功能 
      * com.baizhiedu.proxy.UserServiceImpl.*(..)  
      
    • 语法2

      #忽略包
      1. 类只存在一级包  com.UserServiceImpl
      * *.UserServiceImpl.*(..)
      
      2. 类存在多级包    com.baizhiedu.proxy.UserServiceImpl
      * *..UserServiceImpl.*(..)
      
  3. 包切入点表达式 实战

    指定包作为额外功能加入的位置,自然包中的所有类及其方法都会加入额外的功能
    
    • 语法1

      #切入点包中的所有类,必须在proxy中,不能在proxy包的子包中
      * com.baizhiedu.proxy.*.*(..)
      
    • 语法2

      #切入点当前包及其子包都生效 
      * com.baizhiedu.proxy..*.*(..) 
      

切入点函数

作用:用于执行切入点表达式

  1. execution
    最为重要、功能最全。
    如:execution(* com.baizhiedu.proxy..*.*(..))
    弊端:书写麻烦
  2. args
    主要用于方法参数的匹配
    execution(* *(String,String))args(String,String)是一样的。
  3. within
    主要用于进行类、包切入点表达式的匹配
    execution(* *..UserServuceImpl.*(..))可以替换成within(*..UserServiceImpl)(匹配UserServuceImpl类的所有方法)
  4. @annotation
    作用:为加入指定注解的方法加入额外功能
@annotation

作用:为加入指定注解的方法加入额外功能

注解如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

UserServiceImpl中给login方法加入Log注解

@Log
@Override
public void login() {
    System.out.println("UserServiceImpl.login");
}

修改切入点表达式(annotation里面写的是注解的全类名)

<aop:pointcut id="pc" expression="@annotation(com.prince.proxy.Log)"/>

测试,可以发现只有login方法有被代理

UserServiceImpl.register
-----------方法执行之前的额外功能写在这-----------
UserServiceImpl.login
-----------方法执行finally后的额外功能写在这-----------
-----------方法执行之后的额外功能写在这-----------
切入点函数的逻辑运算

逻辑运算:指的是 整合多个切⼊点函数⼀起配合⼯作,进而完成更为复杂的需求

  1. and 与操作
    用法:将2个切入点函数用and运算符连接在一起

    案例:login方法,并且参数为2个String类型的字符串,可以这样写

    execution(* login(String,String))
    execution(* login(..)) and args(String,String)
    
  2. or 或操作

    案例:login和register方法都能被代理

    execution(* login(..)) or execution(* register(..))
    

AOP编程

AOP(Aspect Oriented Programing) 面向切面编程

切面 = 切入点 + 额外功能
面向切面编程 = Spring的动态代理开发

AOP的本质就是Spring的动态代理开发,通过代理类为原始类增加额外功能。
好处:利于原始类的维护。

AOP编程底层实现原理

核心问题

  1. AOP如何创建代理类(动态字节码技术)
  2. Spring工厂如何加工创建代理对象
    – 通过原始对象的ID值,获得的是代理对象

动态代理类的创建

JDK的动态代理

使用的是JDK里内置的Proxy类

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 
          返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。 

方法参数:

  1. ClassLoader loader 类加载器
    类加载器的作用:

    • 通过类加载器,把对应的字节码文件加载到JVM
    • 通过类加载器创建类的Class对象,进而创建这个类的对象

    如何获得类加载器:每一个类的.class文件,都会自动分配与之对应的ClassLoader
    为什么要传入ClassLoader:Proxy通过newProxyInstance创建动态代理的过程中,需要ClassLoader创建类的Class对象,可是因为动态代理类没有对应的.class文件,JVM也就不会为其分配ClassLoader,但是又需要只能当做参数传进去。

  2. Class<?>[] interfaces 原始对象实现的所有接口

  3. InvocationHandler h

    new InvocationHandler() {
        /**
         * 作用:用于实现额外功能,额外功能可以放的位置和Spring动态代理的MethodInterceptor一样(运行原始方法前、后、前后、抛出异常时)
         * @param proxy  忽略掉,代表的是代理对象(newProxyInstance方法的返回值,也作为方法的参数放在这里)
         * @param method 额外功能,所增加给的那个原始方法
         * @param args   原始方法的参数
         * @return		返回值
         * @throws Throwable
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return null;
        }
    }
    

编码实现:

public class JDK_Proxy {
    public static void main(String[] args) {
        UserService us = new UserServiceImpl();
        UserService proxyInstance = (UserService) Proxy.newProxyInstance(us.getClass().getClassLoader(), us.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("---------------log-----------------");
                Object obj = method.invoke(us, args);  //原始方法执行
                return obj;
            }
        });  //创建代理对象
        proxyInstance.login();
        proxyInstance.register();  //通过代理对象来执行方法
    }
}

细节:

  1. newProxyInstance的第一个参数,可以是UserService的类加载器,也可以是JDK_Proxy 的类加载器。

  2. JDK8.0之前,内部类访问外部的对象应该加入final

    final UserService us = new UserServiceImpl();
    
CGlib的动态代理

原理:

在这里插入图片描述

CGlib和JDK的实现方式的区别是:JDK的动态代理是原始对象和代理对象都实现相同的接口,而CGlib的动态代理是父子继承的关系,原始类作为⽗类,代理类作为⼦类,这样既可以保证2者⽅法⼀致,同时在代理类中提供新的实现(额外功能+原始⽅法) 。

编码:

UserService.java(不实现任何接口)

public class UserService {
    public void login(){
        System.out.println("UserService.login");
    }

    public void register(){
        System.out.println("UserService.register");
    }

}

TestCglib.java

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class TestCglib {
    public static void main(String[] args) {
        // 1. 创建原始对象
        UserService us = new UserService();
        // 2. 通过CGlib来创建代理对象(Spring已集成,不需要另外导包)
        Enhancer enhancer = new Enhancer();
        // 2.1 和Proxy一样,需要设置3样东西--类加载器,实现的接口(CGlib的是继承的父类),回调函数
        enhancer.setClassLoader(TestCglib.class.getClassLoader()); //类加载器
        enhancer.setSuperclass(UserService.class);  //父类(原始对象的类)
        //回调函数 -- 注意和之前Spring动态代理的那个MethodInterceptor所在的包不一样,这个的是org.springframework.cglib.proxy.MethodInterceptor
        MethodInterceptor mi = new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("-----------TestCglib.intercept----------");
                Object obj = method.invoke(us,args);
                return obj;
            }
        };
        // 设置回调函数
        enhancer.setCallback(mi);
        // 获取代理对象
        UserService userService = (UserService) enhancer.create();
        userService.login();
        userService.register();
    }
}

输出:

-----------TestCglib.intercept----------
UserService.login
-----------TestCglib.intercept----------
UserService.register

Process finished with exit code 0

Spring对动态代理的加工

思路分析:(用BeanPostProcessor就可了

在这里插入图片描述

编码:

  1. 通过BeanPostProcessor对原始对象进行加工

    public class ProxyBeanPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }
    
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            //通过动态代理对对象进行加工
            return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(), bean.getClass().getInterfaces(), new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("================ProxyBeanPostProcessor.invoke=====================");
                    return method.invoke(bean,args);
                }
            });
        }
    }
    
    
  2. 把这个ProxyBeanPostProcessor加入到Spring的容器中

        <bean id="userService" class="com.prince.proxy.UserServiceImpl" />
    
        <bean id="beanPostProcessor" class="com.prince.factory.ProxyBeanPostProcessor" />
    
  3. 根据id获取UserServiceImpl时,实际上获取到的就是代理对象

    @Test
    public void test() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext1.xml");
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.register();
        userService.login();
    }
    

基于注解的AOP编程

开发步骤:

  1. 原始对象
  2. 额外功能
  3. 切入点
  4. 组装切面

@Aspect注解

定义额外功能的类,需要加入@Aspect注解来代表这是一个切面类

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect //加入Aspect注解,代表这是一个切面类
public class MyAround {
    /*
    和MethodInterceptor相比,这个不用实现指定的接口
    可以自己随便定义一个方法,方法名可以随便起,只要加上@Around注解
    注解里面是切入点表达式
    方法参数ProceedingJoinPoint和MethodInterceptor接口中invoke方法的MethodInvocation是一样的
     */
    @Around("execution(* *(..))")   //加上Around注解,代表里面的是额外功能
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("====================log=====================");
        Object ret = joinPoint.proceed();
        return ret;
    }
}

xml配置:(注意要加入<aop:aspectj-autoproxy />的标签来开启注解配置)

<bean id="userService" class="com.prince.aspectj.UserServiceImpl" />

<bean class="com.prince.aspectj.MyAround" id="around"></bean>

<!--开启注解配置-->
<aop:aspectj-autoproxy />

在测试类中,通过userService这个id值就可以获取到对应的代理对象。

切入点复用@Pointcut注解

如果切面类中里面有2个切入点方法,2个切入点都是同一个表达式,那么我们就可以把2个切入点表达式抽取出来。

@Aspect
public class MyAround1 {

    @Around("execution(* login(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("====================log=====================");
        Object ret = joinPoint.proceed();
        return ret;
    }

    @Around("execution(* login(..))")
    public Object around1(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("====================hhh=====================");
        Object ret = joinPoint.proceed();
        return ret;
    }
}

使用@Pointcut注解就可以将切入点表达式抽取出来

步骤:

  1. 定义一个方法,方法名随便起,但是该方法必须是public修饰,返回值是void
  2. 给这个方法加上@Pointcut注解,参数写上切入点表达式
  3. @Around("方法名()")就可以使用到那个切入点表达式
@Aspect
public class MyAround1 {

    @Pointcut("execution(* login(..))")
    public void myPointcut(){}

    @Around("myPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("====================log=====================");
        Object ret = joinPoint.proceed();
        return ret;
    }

    @Around("myPointcut()")
    public Object around1(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("====================hhh=====================");
        Object ret = joinPoint.proceed();
        return ret;
    }
}

Spring对JDK和CGlib动态代理的切换

运行测试类,打开断点,我们发现Spring底层默认用的是JDK的动态代理,而不是CGlib

在这里插入图片描述

如果想切换到CGlib,可以使用以下方法:

  1. 基于注解:给开启注解配置的标签<aop:aspectj-autoproxy />加上一个属性
    值默认为false,如果为true则代表采用CGlib的方式

    <aop:aspectj-autoproxy proxy-target-class="true" />
    
  2. 传统的AOP开发,给<aop:config>标签加上一样的属性

    <aop:config proxy-target-class="true">
    

在这里插入图片描述

AOP开发中的一个坑

坑:在同一个业务类中,进行业务方法间的调用时,只有最外层的方法,才是加入了额外功能的方法,而内部的方法还是原始对象的方法。

验证:

@Around注解的属性来看,我们的切入点表达式是匹配所有方法的

@Aspect
public class MyAround {
    @Around("execution(* *(..))") 
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("====================log=====================");
        Object ret = joinPoint.proceed();
        return ret;
    }
}

如果我们在UserServiceImpl中的register方法里面调用login方法

public class UserServiceImpl implements UserService {
    @Override
    public void register() {
        System.out.println("UserServiceImpl.register");
        this.login();
    }

    @Log
    @Override
    public void login() {
        System.out.println("UserServiceImpl.login");
    }
}

在测试类中只调用register方法

@Test
public void test(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
    UserService userService = applicationContext.getBean("userService", UserService.class);
    userService.register();
}

输出,可以发现,只有register方法加入了额外功能,而login方法没有加入额外功能

====================log=====================
UserServiceImpl.register
UserServiceImpl.login

分析login方法没有加入额外功能的原因:在UserServiceImpl中的register方法里面调用login方法时候,this.login()的那个this还是原始对象,而不是代理对象

解决方法:在Spring的工厂中重新获取代理对象,然后调用

public class UserServiceImpl implements UserService {
    @Override
    public void register() {
        System.out.println("UserServiceImpl.register");
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.login();
    }

    @Log
    @Override
    public void login() {
        System.out.println("UserServiceImpl.login");
    }
}

但是!ApplicationContext是一个重量级资源,不可以多创建,只创建一个就好。
我们可以拿测试类中的那个ApplicationContext过来用就可以了

ApplicationContextAware接口的作用:当在Spring工厂中创建这个类的对象时,会自动把这个ApplicationContext通过setApplicationContext方法传进来。

public class UserServiceImpl implements UserService, ApplicationContextAware {
    ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void register() {
        System.out.println("UserServiceImpl.register");
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.login();
    }

    @Log
    @Override
    public void login() {
        System.out.println("UserServiceImpl.login");
    }
}

运行测试类,输出,可以发现两个方法都加上了额外功能

====================log=====================
UserServiceImpl.register
====================log=====================
UserServiceImpl.login

Process finished with exit code 0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值