Spring学习——动态代理、AOP

一、AOP前奏

AOP:(Aspect Oriented Programming)面向切面编程

OOP:(Object Oriented Programming)面向对象编程

面向切面编程:基于OOP基础之上的新编程思想;指在程序运行期间,将某段代码动态的切入到指定的方法的指定位置进行运行的这种编程方式,成为面向切面编程

二、动态代理实现日志功能

问题的引入:计算器运行计算方法时进行日志记录

方法一:直接编写在方法内部【不推荐】,修改维护麻烦,代码耦合
方法二:使用动态代理实现日志功能

动态代理的实现过程:

  1. 定义一个Calculator接口,声明加减乘除方法
  2. 定义一个MyCalculator类实现Calculator接口,完成方法体
  3. 定义一个生成代理对象的类CalculatorProxy,获取代理对象
  4. 重写InvocationHandler的invoke方法,在执行目标方法前后,添加相应的日志输出,也可以处理异常信息

Calculator接口:

public interface Calculator {
    
    
    //加减乘除方法
    public int add(int i, int j);
    public int subtract(int i, int j);
    public int multiply(int i, int j);
    public int divide(int i, int j);
}

MyCalculator类:

public class MyCalculator implements Calculator {
    
    
    @Override
    public int add(int i, int j) {
    
    
        return i + j;
    }@Override
    public int subtract(int i, int j) {
    
    
        return i - j;
    }@Override
    public int multiply(int i, int j) {
    
    
        return i * j;
    }@Override
    public int divide(int i, int j) {
    
    
        return i / j;
    }
}

日志工具类LogUtils:

public class LogUtils {
    
    
    //执行前
    public static void before(Method method,Object... args) {
    
    
        System.out.println("【"+method.getName()+"】方法开始执行了,用的参数列表是【"+ Arrays.asList(args)+"】");
    }
    //执行后
    public static void after(Method method,Object result) {
    
    
        System.out.println("【"+method.getName()+"】方法执行完成了,计算结果是【"+ result+"】");
    }
    //出现异常
    public static void exception(Method method,Exception e) {
    
    
        System.out.println("【"+method.getName()+"】方法出现异常了,异常信息是:"+e.getCause());
    }
    //方法结束
    public  static void end(Method method) {
    
    
        System.out.println("【"+method.getName()+"】方法最终结束了");
    }
}

生成代理对象的类CalculatorProxy:

public class CalculatorProxy {
    
    

	/**
	 * 为传入的参数对象创建一个动态代理对象
	 */
	public static Calculator getProxy(final Calculator calculator) {
    
    
		
		//方法执行器。帮我们目标对象执行目标方法
		InvocationHandler h = new InvocationHandler() {
    
    
			/**
			 * Object proxy:代理对象;给jdk使用,任何时候都不要动这个对象
			 * Method method:当前将要执行的目标对象的方法
			 * Object[] args:这个方法调用时外界传入的参数值
			 */
			@Override
			public Object invoke(Object proxy, Method method, Object[] args)
					throws Throwable {
    
    
				// 利用反射执行目标方法
				//目标方法执行后的返回值
				//System.out.println("这是动态代理将要帮你执行方法...");
				Object result = null;
				try {
    
    
					LogUtils.logStart(method, args);
					result = method.invoke(calculator, args);
					LogUtils.logReturn(method, result);
				} catch (Exception e) {
    
    
					LogUtils.logException(method,e);
				}finally{
    
    
					LogUtils.logEnd(method);
				}
				
				//返回值必须返回出去外界才能拿到真正执行后的返回值
				return result;
			}
		};
		Class<?>[] interfaces = calculator.getClass().getInterfaces();
		ClassLoader loader = calculator.getClass().getClassLoader();
		
		//Proxy为目标对象创建代理对象;
		Object proxy = Proxy.newProxyInstance(loader, interfaces, h);
		return (Calculator) proxy;
	}
}

结果:

【add】方法开始执行了,用的参数列表是【[1, 2]】
动态代理要帮你执行方法!
【add】方法执行完成了,计算结果是【3】
【add】方法最终结束了
【divide】方法开始执行了,用的参数列表是【[2, 0]】
动态代理要帮你执行方法!
【divide】方法出现异常了,异常信息是:java.lang.ArithmeticException: / by zero
【divide】方法最终结束了

动态代理的问题:

1、代码复杂
2、jdk默认的动态代理,如果目标对象没有实现任何接口,是无法为其创建代理对象的

所以就引出了Spring的动态代理:Spring动态代理—>Spring实现AOP功能;底层就是动态代理

1、可以利用Spring一句代码都不写的去创建动态代理;实现简单
2、没有强制要求必须实现接口

三、AOP

AOP:(Aspect Oriented Programming) 面向切面编程,将某段代码动态的切入到指定方法的指定位置(方法的开始、结束、异常…)。

使用场景:

1、AOP加日志保存到数据库中
2、AOP做权限验证
3、AOP做安全检查
4、AOP做事务控制

3.1 几个专业术语

在这里插入图片描述

  • 横切关注点:从每个方法中抽取出来的同一类非核心业务。
  • 切面(Aspect):封装横切关注点信息的类,每个关注点体现为一个通知方法。
  • 通知(Advice):切面必须要完成的各个具体工作
  • 目标(Target):被通知的对象
  • 代理(Proxy):向目标对象应用通知之后创建的代理对象
  • 连接点(Joinpoint):横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。
  • 切入点(pointcut):定位连接点的方式。

3.2 注解实现AOP的步骤(重点)

  1. 导包

     spring-aop-4.0.0.RELEASE
     spring-beans-4.0.0.RELEASE
     spring-context-4.0.0.RELEASE
     spring-core-4.0.0.RELEASE
     spring-expression-4.0.0.RELEASE
     spring-aspects-5.2.3.RELEASE.jar基础包
     
     加强版的面向切面编程(即使目标对象没有实现任何接口也能创建动态代理)
     com.springsource.org.aspectj.weaver-1.6.8.RELEASE
     com.springsource.org.aopalliance-1.0.0
     com.springsource.net.sf.cglib-2.2.0
    
  2. 将目标类和切面类(封装了通知方法的类)加入到IOC容器中,注解@Component,配置文件开启context:component-scan包扫描

  3. 告诉Spring哪个是切面类,在类上注解@Aspect

  4. 告诉Spring切面中的方法都是何时何地运行,方法上注解

    通知注解:
     	@Before:在目标方法之前运行;前置通知
     	@After:在目标方法之后运行;后置通知
     	@AfterReturning:在目标方法正常返回之后;返回通知
     	@AfterThrowing:在目标方法抛出异常之后;异常通知
     	@Around:环绕通知
    
  5. 在注解中写切入点表达式:execution(访问权限符 返回值类型 方法全类名(参数表))

  6. 配置文件中开启基于注解的AOP功能

     <!--  开启基于注解的AOP功能;aop名称空间-->
     <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    

代码实现:

目标类:

@Service
public class MyMathCalculator implements Calculator{
    
    
	.......
}

切面类:

@Component
@Aspect
public class LogUtils {
    
    
	
	/**
	 * 告诉Spring每个方法都什么时候运行;
	 * try{
	 * 		@Before
	 * 		method.invoke(obj,args);
	 * 		@AfterReturning
	 * }catch(e){
	 * 		@AfterThrowing
	 * }finally{
	 * 		@After
	 * }
	 * 5个通知注解,告诉Spring,切面类里面的每一个方法,都是何时何地运行
	 * @Before:在目标方法之前运行;  					 前置通知
	 * @After:在目标方法结束之后						后置通知
	 * @AfterReturning:在目标方法正常返回之后			返回通知
	 * @AfterThrowing:在目标方法抛出异常之后运行			异常通知
	 * @Around:环绕	
	 */
	
	//想在执行目标方法之前运行;写切入点表达式
	//execution(访问权限符   返回值类型   方法签名),在注解中写切入点表达式
	@Before("execution(public int com.zb.impl.MyMathCalculator.*(int, int))")
	public static void logStart(){
    
    
        System.out.println("【xxx】方法开始执行,用的参数列表【xxx】");
    }

    //想在目标方法正常执行完成之后执行
	@AfterReturning("execution(public int com.zb.impl.MyMathCalculator.*(int, int))")
    public static void logReturn(){
    
    
        System.out.println("【xxxx】方法正常执行完成,计算结果是:");
    }

    //想在目标方法出现异常的时候执行
	@AfterThrowing("execution(public int com.zb.impl.MyMathCalculator.*(int, int))")
    public static void logException() {
    
    
        System.out.println("【xxxx】方法执行出现异常了,异常信息是:;这个异常已经通知测试小组进行排查");
    }
}

配置文件:

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

	<!-- 包扫描 -->
	<context:component-scan base-package="com.zb"></context:component-scan>
	<!--  开启基于注解的AOP功能;aop名称空间-->
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
	
</beans>

测试:

public class AOPTest {
    
    
	ApplicationContext ioc = new ClassPathXmlApplicationContext("ApplicationContext.xml");
	@Test
	public void test01(){
    
    
		Calculator bean = ioc.getBean(Calculator.class);
		bean.add(2, 3);
	}
}

基于注解的AOP步骤总结

<!--基于注解的AOP步骤:
    1、将目标类、切面类加入到IOC容器中===>@Component
    2、告诉Spring哪个是切面类===>@Aspect
    3、在切面类中使用5个通知注解,来配置切面中的这些通知方法都何时何地运行
    4、在配置文件中开启基于注解的AOP功能 <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
-->

3.3 注解实现的几个细节

3.3.1 IOC容器中保存组件的代理类

  1. IOC容器中保存的是组件的代理对象。所以当组件实现了接口时,ioc.getBean()中使用的接口类型,也可以用id名

    @Test
    public void test01(){
          
          
    	//AOP底层就是动态代理,容器中保存的组件是他的代理对象:$Proxy23,
    	//所有用MyMathCalculator.class获取不到,因为他本身不是这个类型,他是其的代理对象
    	//使用接口类型获取组件的代理对象
    	Calculator bean = ioc.getBean(Calculator.class);
    	System.out.println(bean.toString());//com.zb.impl.MyMathCalculator@dd8ba08
    	System.out.println(bean.getClass().getName());//com.sun.proxy.$Proxy12
    	
    	//使用id名获取组件的代理对象
    	Calculator bean1 = (Calculator)ioc.getBean("myMathCalculator");
    	System.out.println(bean1.toString());//com.zb.impl.MyMathCalculator@dd8ba08
    	System.out.println(bean1.getClass().getName());//com.sun.proxy.$Proxy12
    }
    
  2. 补充:若该对象没有接口,则使用本类类型

    当目标类没有实现接口时,则spring将自动使用cglib帮我们创建代理对象

    @Test
    public void test01(){
          
          
    	
    	MyMathCalculator bean2 = ioc.getBean(MyMathCalculator.class);
    	bean2.add(1, 2);
    	//com.zb.impl.MyMathCalculator@22fcf7ab
    	System.out.println(bean2);
    	//class com.zb.impl.MyMathCalculator$$EnhancerByCGLIB$$f7c49fce
    	//cglib帮我们创建好的代理对象
    	System.out.println(bean2.getClass());
    	
    	//如果实现了接口就转成接口类型,否则就转成类类型
    	MyMathCalculator bean1 = (MyMathCalculator)ioc.getBean("myMathCalculator");
    	System.out.println(bean1.toString());//com.zb.impl.MyMathCalculator@22fcf7ab
    	System.out.println(bean1.getClass().getName());//com.zb.impl.MyMathCalculator$$EnhancerByCGLIB$$f7c49fce
    }
    

3.3.2 切入表达式的写法(通配符)

通配符:

 * :
	1)匹配一个或者多个字符:execution(public int com.atguigu.impl.MyMath*r.*(int, int))
	2)匹配任意一个参数:第一个是int类型,第二个参数任意类型;(匹配两个参数)
		execution(public int com.atguigu.impl.MyMath*.*(int, *))
	3)只能匹配一层路径
		execution(public int com.zb.*.MyMathCalculator.*(int, int))
	4)权限位置*不能;权限位置不写就行;public【可选的】
.. :
	1)匹配任意多个参数,任意类型参数
		execution(public int com.zb.impl.MyMathCalculator.*(..))
	2)匹配任意多层路径:
		execution(public int com.atguigu..MyMath*.*(..));

记住两种:

最精确的 execution(public int com.achang.impl.MyMathCalculator.add(int,int))
最模糊的 execution(* .(…)):千万别写

在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。

execution (* *.add(int,..)) || execution(* *.sub(int,..))

3.3.3 通知方法的执行顺序

try{
	@Before
	method.invoke(obj,args);
	@AfterReturning
}catch(e){
	@AfterThrowing
}finally{
	@After
}
正常执行情况:@Before(前置通知)—>@After(后置通知)—>@AfterReturning(正常返回)
异常执行情况:@Before(前置通知)—>@After(后置通知)—>@AfterThrowing(方法异常)

3.3.4 JoinPoint获取目标方法的详细信息

JoinPoint joinPoint:封装了当前目标方法的详细信息

//JoinPoint joinPoint:封装了当前目标方法的详细信息
@Before("execution(public int com.zb.*.MyMathCalculator.*(int, int))")
public static void logStart(JoinPoint joinPoint){
    
    
	//获取目标方法运行时使用的参数
	Object[] args = joinPoint.getArgs();
	//获取到方法签名
	Signature signature = joinPoint.getSignature();
	String name = signature.getName();
    System.out.println("【"+name+"】方法开始执行,用的参数列表【"+Arrays.asList(args)+"】");
}

3.3.5 通过throwing、returning属性来接收结果返回值和异常信息

//returning="result",告诉Spring这个result用来接收返回值
@AfterReturning(value="execution(public int com.zb.impl.MyMathCalculator.*(int, int))",returning="result")
   public static void logReturn(JoinPoint joinPoint,Object result){
    
    
	String name = joinPoint.getSignature().getName();
       System.out.println("【"+name+"】方法正常执行完成,计算结果是:"+result);
   }

//throwing="exception",告诉Spring哪个参数用来接收异常
@AfterThrowing(value="execution(public int com.zb.impl.MyMathCalculator.*(..))",throwing="exception")
   public static void logException(JoinPoint joinPoint,Exception exception) {
    
    
	String name = joinPoint.getSignature().getName();
       System.out.println("【"+name+"】方法执行出现异常了,异常信息是:"+exception+";这个异常已经通知测试小组进行排查");
   }

3.3.6 Spring对通知方法的约束

  • Spring对通知方法的要求不严格:修改返回值和去掉静态static都照样运行
    在这里插入图片描述

  • 但参数表上的每一个参数,Spring都得知道是什么
    在这里插入图片描述

3.3.7 使用@Pointcut注解抽取可重用的切入点表达式

  • 在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现。
  • 在AspectJ切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。
  • 切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
  • 其他通知可以通过方法名称引入该切入点
    在这里插入图片描述

3.3.8 环绕通知(重点)

  • 环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
  • 对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
  • 在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。相当于动态代理中的method.invoke(obj,args)
  • 注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。

环绕通知和其他通知执行顺序:
切面类:

@Component
@Aspect
public class LogUtils {
    
    
	
	@Pointcut("execution(public int com.zb.*.MyMathCalculator.*(int, int))")
	public void hahaMyPoint(){
    
    
		
	}
	
	//想在执行目标方法之前运行;写切入点表达式
	//execution(访问权限符   返回值类型   方法签名),在注解中写切入点表达式
	@Before("hahaMyPoint()")
	public static void logStart(JoinPoint joinPoint){
    
    
		//获取目标方法运行时使用的参数
		Object[] args = joinPoint.getArgs();
		//获取到方法签名
		Signature signature = joinPoint.getSignature();
		String name = signature.getName();
        System.out.println("【"+name+"】方法开始执行,用的参数列表【"+Arrays.asList(args)+"】");
    }

    //想在目标方法正常执行完成之后执行
	//returning="result",告诉Spring这个result用来接收返回值
	@AfterReturning(value="hahaMyPoint()",returning="result")
    private void logReturn(JoinPoint joinPoint,Object result){
    
    
		String name = joinPoint.getSignature().getName();
        System.out.println("【"+name+"】方法正常执行完成,计算结果是:"+result);
    }

    //想在目标方法出现异常的时候执行
	//throwing="exception",告诉Spring哪个参数用来接收异常
	@AfterThrowing(value="hahaMyPoint()",throwing="exception")
    public static int logException(JoinPoint joinPoint,Exception exception) {
    
    
		String name = joinPoint.getSignature().getName();
        System.out.println("【"+name+"】方法执行出现异常了,异常信息是:"+exception+";这个异常已经通知测试小组进行排查");
		return 0;
    }

    //想在目标方法结束的时候执行
	@After("hahaMyPoint()")
    public static void logEnd(JoinPoint joinPoint) {
    
    
		String name = joinPoint.getSignature().getName();
        System.out.println("【"+name+"】方法最终结束了");
    }
	
	/**
	 *{
	 *	try{
	 *		环绕前置
	 *		环绕执行:目标方法执行
	 *		环绕返回
	 *	}catch(){
	 *		环绕出现异常
	 *	}finally{
	 *		环绕后置
	 *	}
	 *}
	 *新的顺序:
	 *		(环绕前置---普通前置)----目标方法执行----环绕正常返回/出现异常-----环绕后置----普通后置---普通返回或者异常
	 */
	@Around("hahaMyPoint()")
	public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
    
    
		
		String name = pjp.getSignature().getName();
		Object[] args = pjp.getArgs();
		Object proceed = null;
		try {
    
    
			//@Before
			System.out.println("【环绕前置通知】【"+name+"方法开始】");
			//就是利用反射调用目标方法即可,就是method.invoke(obj,args)
			proceed = pjp.proceed(args);
			//@AfterReturing
			System.out.println("【环绕返回通知】【"+name+"方法返回,返回值"+proceed+"】");
		} catch (Exception e) {
    
    
			//@AfterThrowing
			System.out.println("【环绕异常通知】【"+name+"】方法出现异常,异常信息:"+e);
			//为了让外界能知道这个异常,这个异常一定抛出去
			throw new RuntimeException(e);
		} finally{
    
    
			//@After
			System.out.println("【环绕后置通知】【"+name+"】方法结束");
		}
		//反射调用后的返回值也一定返回出去
		return proceed;
	}
}

结果:
在这里插入图片描述
执行顺序
(环绕前置 —> 普通前置)(环绕调用目标方法后普通前置才能感受到,才会调用) —> 目标方法执行 —> 环绕正常返回/出现异常 —> 环绕后置 —> 普通后置 —> 普通返回或者异常

3.3.9 多切面运行顺序

  • 切面的优先级可以通过实现Ordered接口或利用@Order注解指定。

  • 实现Ordered接口,getOrder()方法的返回值越小,优先级越高。

  • 若使用@Order注解,序号出现在注解中

    @Order(2) //使用@Order改变切面顺序;数值越小优先级越高
    public class LogUtils {
          
          }
    @Order(1)
    public class ValidateAspect {
          
          }
    

多切面运行顺序类型于队列(先进后出)
在这里插入图片描述

3.4 以XML方式配置AOP(重点)

  1. 概述

    除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。
    正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。

  2. 配置细节

    在bean配置文件中,所有的Spring AOP配置都必须定义在<aop:config>元素内部。对于每个切面而言,都要创建一个<aop:aspect>元素来为具体的切面实现引用后端bean实例。

  3. 声明切入点

    切入点使用<aop:pointcut>元素声明。
    切入点必须定义在<aop:aspect>元素下,或者直接定义在<aop:config>元素下。
    定义在<aop:aspect>元素下:只对当前切面有效
    定义在<aop:config>元素下:对所有切面都有效
    基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点
    <aop:before method="…" pointcut-ref="…" >:指定怎么切入,切在哪里,相当于@Before等,该标签中也可以指定返回值、异常等信息

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

	<!-- 基于配置的AOP -->
	
	<bean id="MyMathCalculator" class="com.zb.impl.MyMathCalculator"></bean>
	<bean id="LogUtils" class="com.zb.utils.LogUtils"></bean>
	<bean id="ValidateAspect" class="com.zb.utils.ValidateAspect"></bean>
	
	<aop:config>
		<!-- 切入点 -->
		<aop:pointcut expression="execution(* com.zb.impl.*.*(..))" id="globalPoint"/>
		<!--自定义切面aspect,ref:要引用的类-->
		<aop:aspect ref="LogUtils">
			<!-- 切入点 -->	
			<aop:pointcut expression="execution(* com.zb.impl.*.*(..))" id="mypoint"/>
			<!-- 前置 -->
			<aop:before method="logStart" pointcut="execution(* com.zb.impl.*.*(..))"/>
			<!-- 返回 -->
			<aop:after-returning method="logReturn" pointcut-ref="mypoint" returning="result"/>
			<!-- 异常 -->
			<aop:after-throwing method="logException" pointcut-ref="mypoint" throwing="exception"/>
			<!-- 后置 -->
			<aop:after method="logEnd" pointcut-ref="mypoint"/>
			<aop:around method="myAround" pointcut="execution(* com.zb.impl.*.*(..))"/>
		</aop:aspect>
		
		<aop:aspect ref="ValidateAspect">
			<aop:before method="logStart" pointcut="execution(* com.zb.impl.*.*(..))"/>
			<aop:after-returning method="logReturn" pointcut-ref="globalPoint" returning="result"/>
			<aop:after-throwing method="logException" pointcut-ref="globalPoint" throwing="exception"/>
			<aop:after method="logEnd" pointcut-ref="globalPoint"/>
		</aop:aspect>
	</aop:config>
</beans>

注解配置:快速方便
XML配置:功能完善
总结:重要的内容用XML配置,不重要的使用注解

猜你喜欢

转载自blog.csdn.net/weixin_44630656/article/details/115008741