Spring框架学习

文章40 |   阅读 20030 |   点赞0

来源:https://blog.csdn.net/yerenyuan_pku/category_6457009.html

Spring入门第六讲——Spring AOP的XML开发

x33g5p2x  于2021-12-18 转载在 其他  
字(14.8k)|赞(0)|评价(0)|浏览(561)

什么是AOP?

Spring是用来解决实际开发中的一些问题的,AOP解决了OOP中遇到的一些问题,是OOP的延续和扩展。我们可从以下三个方面来理解AOP。

  • 第一个方面:扩展功能不是通过修改源代码实现的,这可通过Struts2框架中的拦截器来理解;
  • 第二个方面:AOP采用横向抽取机制实现。要理解横向抽取机制,就必须先认识纵向抽取机制。例如有如下的一个类:
public class UserDao {
	public void save() {
		添加的逻辑...
	}
}

现在我们想在save()方法中要扩展一个功能,即日志添加功能,添加完该功能之后,就可记录在什么时候添加了哪个用户,我们想到的最原始的方法就是直接修改源代码。

public class UserDao {
	public void save() {
		添加的逻辑...
		直接写添加日志记录的代码以实现...
	}
}

很显然这是一种愚蠢的做法,并且这儿还有一个原则:修改功能一般不是直接修改源代码来实现的。顺其自然地,咱现在就要来讲纵向抽取机制了,这时我们可编写一个BaseDao类。

public class BaseDao {
	public void wirtelog() {
		记录日志的逻辑...
	}
}

接下来让UserDao类继承BaseDao类,如下:

public class UserDao extends BaseDao {
	public void save() {
		添加的逻辑...
		// 记录日志
		super.wirtelog();
	}
}

这样是不是就万事大吉了呢?你懂的!因为当父类的方法名称变化时,子类调用的方法也必然要进行修改。最后,终于要讲横向抽取机制了,横向抽取机制分为两种情况,下面分别加以简单阐述。

  • 有接口情况的横向抽取机制
    例如有一个如下接口:
public interface UserDao {
	public void add();
}

接口的一个实现类如下:

public class UserDaoImpl implements UserDao {
	public void add() {
		...
	}
}

我们现在就可以使用动态代理技术来增强类里面的方法,即创建接口的实现类代理对象,并增强UserDaoImpl类里面的add()方法。

  • 无接口情况的横向抽取机制
    例如有一个如下User类:
public class User {
	public void add() {
		...
	}
}

我们现在也可以使用动态代理技术来增强类里面的方法,即创建被增强方法所在类的子类代理对象,并增强User类里面的add()方法。

  • 第三个方面:AOP底层使用动态代理技术实现。它同样也要分为两种情况:

  • 有接口情况:创建接口实现类代理对象;

  • 没有接口情况:创建增强方法所在类的子类代理对象。

为什么学习AOP?

因为在不修改源代码的情况下,即可对程序进行增强,所以使用AOP咱就可以进行权限校验、日志记录、性能监控以及事务控制了。

Spring AOP的由来

AOP最早是由AOP联盟的组织提出的,他们制定了一套规范,Spring将AOP思想引入到了它的框架中,所以它也必须遵守AOP联盟的规范。

Spring AOP的底层实现原理

Spring AOP的底层用到了两种代理机制,它们分别是:

  • JDK的动态代理:针对实现了接口的类产生代理,所以这种代理机制只能对实现了接口的类产生代理;
  • Cglib的动态代理:针对没有实现接口的类产生代理,采用的是底层的字节码增强的技术,生成当前类的子类对象(对没有实现接口的类产生代理对象,其实就是生成那个类的子类对象)。其实它有点类似于javassist第三方代理技术。

JDK的动态代理

首先创建一个动态web项目,例如spring_demo02_aop,然后导入Spring框架相关依赖jar包,要导入哪些jar包呢?老子不想说了,自己想去。然后,在src目录下创建一个com.meimeixia.spring.demo01包,并在该包下创建一个名为UserDao的接口。

package com.meimeixia.spring.demo01;

public interface UserDao {
	
	public void save();
	public void update();
	public void find();
	public void delete();

}

接着,在com.meimeixia.spring.demo01包下创建以上接口的一个实现类——UserDaoImpl.java。

package com.meimeixia.spring.demo01;

public class UserDaoImpl implements UserDao {

	@Override
	public void save() {
		System.out.println("保存用户......");
	}

	@Override
	public void update() {
		System.out.println("修改用户......");
	}

	@Override
	public void find() {
		System.out.println("查询用户......");
	}

	@Override
	public void delete() {
		System.out.println("删除用户......");
	}

}

现在来了这样一个需求:我们想在UserDaoImpl实现类的save方法执行之前,进行一个权限校验。那该咋怎?可以使用JDK的动态代理来增强实现类里面的save方法,即创建该实现类的代理对象,并增强实现类里面的save方法。说干就干,在com.meimeixia.spring.demo01包下编写一个名为JDKProxy的类,在该类中使用JDK的动态代理机制来对UserDaoImpl实现类产生代理对象。

package com.meimeixia.spring.demo01;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/** * 使用JDK的动态代理机制来对UserDao产生代理 * @author liayun * */
public class JDKProxy implements InvocationHandler {
	
	//得将被增强的对象传递到我们的代理当中
	private UserDao userDao;//被增强的对象

	public JDKProxy(UserDao userDao) {
		this.userDao = userDao;
	}
	
	/* * 产生UserDao代理的方法 */
	public UserDao createProxy() {
		UserDao userDaoProxy = (UserDao) Proxy.newProxyInstance(userDao.getClass().getClassLoader(), 
				userDao.getClass().getInterfaces(), this);
		return userDaoProxy;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		//判断方法名是不是save?
		if ("save".equals(method.getName())) {
			//增强save方法,进行一个权限校验
			System.out.println("权限校验~~~~~~~~~~~~~~~");
			return method.invoke(userDao, args);
		}
		return method.invoke(userDao, args);
	}
	
}

紧接着,在com.meimeixia.spring.demo01包下创建一个SpringDemo01的单元测试类,其内容如下:

package com.meimeixia.spring.demo01;

import org.junit.Test;

public class SpringDemo01 {

	/* * 假设:我想让你在save方法执行之前,进行一个权限校验 * * 如何自己手写代码:JDK的动态代理。 */
	@Test
	public void demo01() {
		/* UserDao userDao = new UserDaoImpl(); userDao.save(); userDao.update(); userDao.find(); userDao.delete(); */
		
		UserDao userDao = new UserDaoImpl();
		//创建代理
		UserDao proxy = new JDKProxy(userDao).createProxy();
		proxy.save();
		proxy.update();
		proxy.find();
		proxy.delete();
	}
	
}

最后,运行以上demo01单元测试方法,Eclipse控制台就会打印出如下内容。

Cglib的动态代理

首先,在src目录下创建一个com.meimeixia.spring.demo02包,并在该包下创建一个名为CustomerDao的类,我们将其作为目标类。

package com.meimeixia.spring.demo02;

/** * 目标类 * @author liayun * */
public class CustomerDao {

	public void save() {
		System.out.println("保存客户......");
	}
	
	public void update() {
		System.out.println("修改客户......");
	}
	
	public void find() {
		System.out.println("查询客户......");
	}

	public void delete() {
		System.out.println("删除客户......");
	}
}

现在来了这样一个需求:我们想在CustomerDao类中的save方法执行之前,进行一个权限校验。那该咋怎?这时就只能使用Cglib的动态代理来增强类里面的save方法了,即创建被增强方法所在类的子类代理对象,并增强类里面的save方法。说干就干,在com.meimeixia.spring.demo02包下编写一个名为CglibProxy的类,在该类中使用Cglib的动态代理机制来对CustomerDao类产生代理对象。

package com.meimeixia.spring.demo02;

import java.lang.reflect.Method;

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

/** * Cglib动态代理 * @author liayun * */

//一旦CglibProxy这个类实现了MethodInterceptor接口,那么这个类就相当于InvocationHandler
public class CglibProxy implements MethodInterceptor {

	private CustomerDao customerDao;

	public CglibProxy(CustomerDao customerDao) {
		this.customerDao = customerDao;
	}
	
	/* * 使用Cglib产生代理的方法 */
	public CustomerDao createProxy() {
		//1.创建Cglib的核心类的对象
		Enhancer enhancer = new Enhancer();
		//2.给它设置父类
		enhancer.setSuperclass(customerDao.getClass());
		//3.设置回调(回调类似于InvocationHandler对象)
		enhancer.setCallback(this);
		//4.创建代理对象
		CustomerDao proxy = (CustomerDao) enhancer.create();
		return proxy;
	}

	/* * proxy:即CustomerDao的代理对象,也即它的子类。 */
	@Override
	public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
		//判断方法名是不是save?
		if ("save".equals(method.getName())) {
			//增强save方法,进行一个权限校验
			System.out.println("权限校验~~~~~~~~~~~~~~~~");
			return methodProxy.invokeSuper(proxy, args);
		}
		
		//如果不是save方法,那么执行子类的父类里面的方法(父类里面的方法是没有增强的)
		return methodProxy.invokeSuper(proxy, args);
	}
	
}

然后,在com.meimeixia.spring.demo02包下创建一个SpringDemo02的单元测试类,其内容如下:

package com.meimeixia.spring.demo02;

import org.junit.Test;

public class SpringDemo02 {
	
	/* * Cglib的测试 */
	@Test
	public void demo01() {
		CustomerDao customerDao = new CustomerDao();
		
		//产生一个代理对象
		CustomerDao proxy = new CglibProxy(customerDao).createProxy();
		
		proxy.save();
		proxy.update();
		proxy.find();
		proxy.delete();
	}

}

最后,运行以上demo01单元测试方法,Eclipse控制台就会打印出如下内容。

Spring的基于AspectJ的AOP开发

AOP开发中的相关术语

咱们要进行Spring AOP的XML开发,那么得知道AOP开发中一些比较专业的术语。大家初次见到这些东西,可能不太懂,但代码写的多了,自然而然就会理解了。

为了让大家更好地理解这些AOP开发中比较专业的术语,下面我会通过一个代码示例来说明它们。

AspectJ的简介

AOP思想最早是由AOP联盟组织提出的,而Spring是使用这种思想最好的一个框架。Spring的AOP有自己实现的方式,但这种方式非常繁琐。正好,AspectJ是一个AOP的框架,所以Spring2.X引入了AspectJ来作为自身AOP的开发。这样看来,Spring其实有两套AOP的开发方式,它们分别是:

  • Spring传统方式,不过已经被弃用了;
  • Spring基于AspectJ的AOP的开发,这种方式使用的非常广泛,这儿讲的就是它。

说了这么多,那么啥是AspectJ呢?AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件,也就是说AspectJ是一个基于Java语言的AOP框架。Spring2.0以后新增了对AspectJ切点表达式的支持。@AspectJ是AspectJ1.5新增的功能,通过JDK5注解技术,允许直接在Bean类中定义切面。新版本Spring框架,都建议使用AspectJ方式来开发AOP,使用AspectJ需要导入Spring AOP和AspectJ相关的jar包。
从上面的阐述中,我们应认识到AspectJ并不是Spring框架的一部分,而是一个单独的面向切面的框架,只不过它经常和Spring框架一起使用进行AOP的操作而已。使用AspectJ方式来开发AOP共有下面两种方式。

  • 第一种方式:基于AspectJ的XML配置文件的方式
  • 第二种方式:基于AspectJ的注解的方式

只不过本文讲解的是基于AspectJ的XML配置文件的方式,下一讲再讲第二种方式。

使用基于AspectJ的XML配置文件的方式进行AOP开发

引入相应的jar包

上面我说过,Spring其实有两套AOP的开发方式,它们分别是:

  • Spring传统的AOP开发:不过它已经被弃用了,如果真想使用这种方式,那么得导入下面两个jar包。

  • Spring基于AspectJ的AOP的开发:使用这种方式,除了要导入最基本的jar包外,还需要导入Spring AOP和AspectJ相关的jar包。

编写目标类

首先,在src目录下创建一个com.meimeixia.spring.demo03包,并在该包下创建一个名为ProductDao的接口。

package com.meimeixia.spring.demo03;

public interface ProductDao {

	public void save();
	public void update();
	public void find();
	public void delete();
	
}

接着,在com.meimeixia.spring.demo03包下创建以上接口的一个实现类——ProductDaoImpl.java。

package com.meimeixia.spring.demo03;

public class ProductDaoImpl implements ProductDao {

	@Override
	public void save() {
		System.out.println("保存商品......");
	}

	@Override
	public void update() {
		System.out.println("修改商品......");
	}

	@Override
	public void find() {
		System.out.println("查询商品......");
	}

	@Override
	public void delete() {
		System.out.println("删除商品......");
	}

}

创建增强的类以及增强的方法

现在来了这样一个需求:我们想在ProductDaoImpl实现类的save方法执行之前,进行一个权限校验,那该咋怎?这时我们可以编写一个切面类,并在切面类中编写一个进行权限校验的方法。待会,我们就在Spring配置文件中对其进行配置,让切面类中的权限校验方法在save方法执行之前执行。

package com.meimeixia.spring.demo03;

/** * 切面类 * @author liayun * */
public class MyAspectXML {

	/* * 前置通知 * * 权限校验的方法 */
	public void checkPri() {
		System.out.println("权限校验~~~~~~~~~~~~~~");
	}
	
}

在Spring配置文件中进行配置

首先,在Spring配置文件中引入aop约束,那么问题来了,这个约束又该怎么写呢?可参考docs\spring-framework-reference\html目录下的xsd-configuration.html文件,在其内容中找到如下内容。

然后,在Spring配置文件配置好目标对象(即被增强的对象)和切面类。

<?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 http://www.springframework.org/schema/aop/spring-aop.xsd">

	<!-- 配置目标对象,即被增强的对象 -->
	<bean id="productDao" class="com.meimeixia.spring.demo03.ProductDaoImpl" />
	
	<!-- 将编写好的切面类交给Spring去管理 -->
	<bean id="myAspect" class="com.meimeixia.spring.demo03.MyAspectXML" />
	
</beans>

接着,通过AOP的配置来完成对目标对象去产生代理。

编写一个单元测试类并进行测试

首先,在com.meimeixia.spring.demo03包下创建一个SpringDemo03的单元测试类,其内容如下:

package com.meimeixia.spring.demo03;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/** * AOP的入门 * @author liayun * */
public class SpringDemo03 {

	@Test
	public void demo01() {
		ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		ProductDao productDao = (ProductDao) context.getBean("productDao");
		productDao.save();
		productDao.update();
		productDao.find();
		productDao.delete();
	}
	
}

然后,运行以上demo01单元测试方法,Eclipse控制台就会打印出如下内容。

Spring整合JUnit单元测试

其实,Spring也可以整合JUnit单元测试,Spring对JUnit4进行了支持,可以通过注解方便的测试Spring程序,所以就不必写那么麻烦的单元测试类了。如果Spring真要整合JUnit单元测试,那么首先得导入如下jar包:

然后,将SpringDemo03的单元测试类修改成下面这个样子。

package com.meimeixia.spring.demo03;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/** * AOP的入门 * @author liayun * */
//下面是固定写法:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")//加载类路径下的配置文件
public class SpringDemo03 {
	
	//想用谁,就注入谁(但是注意这个类得交给Spring管)
	@Resource(name="productDao")
	private ProductDao productDao;

	@Test
	public void demo01() {
		productDao.save();//作断点调试,可以看到使用到的是JDK的动态代理:JdkDynamicAopProxy
		productDao.update();
		productDao.find();
		productDao.delete();
	}
	
}

这里,大家可能会有一个疑惑,为什么在以上单元测试类中能直接使用@Resource注解完成属性的注入呢?这是因为咱引入了spring-test-4.2.4.RELEASE.jar这个jar包。引入该包之后,只能在单元测试类中去使用@Resource注解完成属性的注入,否则的话,你想在一些普通的类里面完成属性注入,那你肯定得配置组件扫描了。
此时,运行以上demo01单元测试方法,Eclipse控制台就会打印出如下内容。

演示其他通知类型

前置通知

前置通知是指在目标方法执行之前进行操作。上面我演示的就是前置通知,只不过,还有一点我还没有说到,那就是前置通知还可以获得切入点的信息。为了验证这一点,我们可以将以上MyAspectXML切面类修改成下面这个样子。

package com.meimeixia.spring.demo03;

import org.aspectj.lang.JoinPoint;

/** * 切面类 * @author liayun * */
public class MyAspectXML {

	/* * 前置通知 * * 权限校验的方法 */
	public void checkPri(JoinPoint joinPoint) {
		System.out.println("权限校验~~~~~~~~~~~~~~" + joinPoint);
	}
		
}

此时,Spring的配置文件不用做修改,试着运行一下SpringDemo03单元测试类中的demo01方法,你就会看到Eclipse控制台打印出了如下内容。

后置通知

后置通知是指在目标方法执行之后进行操作。它除了可以获得切入点的信息以外,还可以获得方法的返回值。为了验证这一点,首先,将ProductDao接口修改成下面这个样子(主要是修改了一下delete方法的声明,让其返回一个字符串)。

package com.meimeixia.spring.demo03;

public interface ProductDao {

	public void save();
	public void update();
	public void find();
// public void delete();
	public String delete();
	
}

然后,将以上接口的实现类(ProductDaoImpl.java)修改成下面这个样子。

package com.meimeixia.spring.demo03;

public class ProductDaoImpl implements ProductDao {

	@Override
	public void save() {
		System.out.println("保存商品......");
	}

	@Override
	public void update() {
		System.out.println("修改商品......");
	}

	@Override
	public void find() {
		System.out.println("查询商品......");
	}
	
	@Override
	public String delete() {
		System.out.println("删除商品......");
		return "删除商品成功";
	}

}

接着,在MyAspectXML切面类中添加一个日志记录的方法,一定要注意方法中参数的写法哟!

紧接着,咱还需要配置切入点和切面。

最后,运行一下SpringDemo03单元测试类中的demo01方法,你就会看到Eclipse控制台打印出了如下内容。

环绕通知

环绕通知是功能最强的一个通知,它是指在目标方法执行之前和之后进行操作。很重要的一点就是它可以阻止目标方法的执行。为了验证这一点,我们可以在以上MyAspectXML切面类中添加一个性能监控的方法。

package com.meimeixia.spring.demo03;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

/** * 切面类 * @author liayun * */
public class MyAspectXML {
	
	/* * 前置通知 * * 权限校验的方法 */
	public void checkPri(JoinPoint joinPoint) {
		System.out.println("权限校验~~~~~~~~~~~~~~" + joinPoint);
	}
	
	/* * 后置通知 * * 日志记录的方法 */
	public void writeLog(Object result) {//你配置里面这个地方写的是啥,这个地方传参数就得传啥。
		System.out.println("日志记录~~~~~~~~~~~~~~" + result);
	}
	
	/* * 性能的监控 */
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		System.out.println("环绕前增强~~~~~~~~~~~~~~");
		Object obj = joinPoint.proceed();//相当于执行目标程序,有可能会有返回值
		System.out.println("环绕后增强~~~~~~~~~~~~~~");
		return obj;
	}
	
}

接着,配置切入点和切面。这里,咱主要是在ProductDaoImpl实现类中的update方法(目标方法)执行之前和之后做一些事情。

最后,运行SpringDemo03单元测试类中的demo01方法,你就会看到Eclipse控制台打印出了如下内容。

异常抛出通知

异常抛出通知是指在程序出现异常的时候而进行的操作,而且它还可以获得异常的信息。异常抛出通知能想到的一个应用场景就是在事务管理的时候会用到。为了验证这一点,首先,修改一下ProductDaoImpl实现类中的find方法,使其抛出一个除零异常。

package com.meimeixia.spring.demo03;

public class ProductDaoImpl implements ProductDao {

	@Override
	public void save() {
		System.out.println("保存商品......");
	}

	@Override
	public void update() {
		System.out.println("修改商品......");
	}

	@Override
	public void find() {
		System.out.println("查询商品......");
		int i = 10 / 0;
	}

// @Override
// public void delete() {
// System.out.println("删除商品......");
// }
	
	@Override
	public String delete() {
		System.out.println("删除商品......");
		return "删除商品成功";
	}

}

然后,在MyAspectXML切面类中添加一个异常抛出的方法,一定要注意方法中参数的写法哟!

紧接着,咱还需要配置切入点和切面。

最后,运行SpringDemo03单元测试类中的demo01方法,你就会看到Eclipse控制台打印出了如下内容。

最终通知

无论目标方法是否出现异常,最终通知都会执行。此时,我们已经知道了ProductDaoImpl实现类中的find方法(也即目标方法)抛出了一个除零异常。现在,咱就是要看看find方法(也即目标方法)出现了异常,最终通知会不会执行。为了验证这一点,我们在以上MyAspectXML切面类中添加如下的一个after方法。

package com.meimeixia.spring.demo03;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

/** * 切面类 * @author liayun * */
public class MyAspectXML {
	
	/* * 前置通知 * * 权限校验的方法 */
	public void checkPri(JoinPoint joinPoint) {
		System.out.println("权限校验~~~~~~~~~~~~~~" + joinPoint);
	}
	
	/* * 后置通知 * * 日志记录的方法 */
	public void writeLog(Object result) {//你配置里面这个地方写的是啥,这个地方传参数就得传啥。
		System.out.println("日志记录~~~~~~~~~~~~~~" + result);
	}
	
	/* * 性能的监控 */
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		System.out.println("环绕前增强~~~~~~~~~~~~~~");
		Object obj = joinPoint.proceed();//相当于执行目标程序,有可能会有返回值
		System.out.println("环绕后增强~~~~~~~~~~~~~~");
		return obj;
	}
	
	/* * 异常抛出 */
	//得到异常的信息
	public void afterThrowing(Throwable ex) {
		System.out.println("异常抛出通知~~~~~~~~~~~~~~" + ex.getMessage());
	}
	
	/* * 最终通知:相当于finally代码块中的内容 */
	public void after() {
		System.out.println("最终通知~~~~~~~~~~~~~~");
	}
	
}

接着,配置切入点和切面。

最后,运行SpringDemo03单元测试类中的demo01方法,你就会看到Eclipse控制台打印出了如下内容。

从上面输出的结果中,我们就证明了即使目标方法出现了异常,最终通知也会执行。

Spring中切入点表达式的写法

Spring通过execution函数,可以定义切入点切入的方法。其语法为:[访问修饰符] 方法的返回值 包名.类名.方法名(参数)。下面,我就举几个例子演示一下Spring中切入点表达式的写法。

  • 匹配所有类的public方法

  • 匹配指定包(但不包含其子包)下所有类的所有方法

  • 匹配指定包及其子孙包下所有类的所有方法

  • 匹配指定类的所有方法

  • 匹配实现特定接口的所有类的方法

  • 匹配所有以save开头的方法

  • 匹配所有类里面的所有方法,这个太彪悍了!

了解完上面的知识点以后,你可以试着解决一下这个需求:在ProductDaoImpl实现类中的所有方法执行之前,应用前置增强。此时,你的Spring配置文件可能要改成下面这个样子。

相关文章