0.6、Spring源码学习 ——Spring AOP的使用规律和测试代码

本文围绕Spring AOP展开,介绍其与IOC(DI)的依赖机制,探讨如何开启Spring AOP,分析JDK和cglib动态代理的区别、效率及使用场景,还涉及Spring bean不同代理对象的名称获取、测试代码,以及AOP使用中的一些问题,如内部调用增强、切点与切面设置等。

前言

体能状态先于精神状态,习惯先于决心,聚焦先于喜好。

Spring AOP 和 IOC(DI)

·Spring IOC 依靠的是 Java 的反射机制
·Spring AOP 依赖的是动态代理机制
关于Spring AOP 的几个问题

1、如何开启 Spring AOP?

在配置文件声明 aop:aspectj-autoproxy/或者<aop:aspectj-autoproxy proxy-target-class=“true”/> ;设置切点
还有一个不常使用的配置 expose-proxy=“true”,这个可以解决目标对象内部调用时无法实施切面增强的问题,具体看本文11

2、Spring AOP 有什么影响,所有Spring bean都会使用代理模式吗?

最主要的影响是,正常来说 Spring 容器在启动时会将所有的Bean注册为 Spring Bean,如果开启代理模式,相关的Bean存在形式是一个代理对象
需要注意的是,AOP需要配合切点使用,只有切点相关的Bean才会被注册为代理对象(Spring 默认是单例的),否则依旧只是正常对象.

3、< aop:aspectj-autoproxy/> 和 < aop:aspectj-autoproxy proxy-target-class=“true”/> 的区别

aop:aspectj-autoproxy/会使用 JDK动态代理
<aop:aspectj-autoproxy proxy-target-class=“true”/> 会使用cglib动态代理
但这不是绝对的,如果切点本身不是接口,那么Spring无论如何都会使用cglib动态代理,为相关的bean创建代理对象,并注册到Spring上下文中

4、JDK动态代理和cglib动态代理的区别?

JDK动态代理需要借助接口完成对接口子类的代理工作
cglib可以直接代理类

5、JDK动态代理和cglib动态代理那个效率高?

看了一片文章,历史上是cglib动态代理效率高,现在是JDK动态代理高 https://www.cnblogs.com/haiq/p/4304615.html

6、什么时候一定要用cglib动态代理

如果你的切点是和接口相关,但是你又需要在代码中直接向子类注入,那么就必须使用cglib 动态代理了,因为JDK动态代理不支持类的直接代理.
与这一条相关的一个经典问题就是
java.lang.ClassCastException: com.sun.proxy.$ Proxy10 cannot be cast to class *

7、如果切点相关的类没有父接口可以使用 aop:aspectj-autoproxy/ 吗?

可以.Spring 在初始化上下文的时候会进行判断,如果切点相关的类只是类,没有实现接口,那么这个切点就是定义在类上的,此时Spring会直接为这个类相关的Bean设置一个
cglib动态代理对象并注册到Spring 上下文中,此时配置文件中可以使用 aop:aspectj-autoproxy/
但是如果这个类实现了接口,并且这个类被标注为一个Spring bean,那么不管切点是设置在接口上的还是这个类上的,aop:aspectj-autoproxy/ 情况下,这个Spring bean
最终会以代理对象的形式被注册到 Spring 上下文.

一旦这个Spring Bean 是以JDK 动态代理对象初始化到Spring 上下文,其就无法被注入到一个类中了,只能被注入到一个接口中,这个在开发中务必注意。

8、对于同一个Spring bean,其正常对象、JDK动态代理对象、cglib 动态代理对象的名称是怎样的?

Spring 初始化会将 Spring bean 存储到一个 “map”里,key 是名字,value 是对象

正常对象:
key “temServiceImpl”
value TemServiceImpl

JDK动态代理:
key “temServiceImpl”
value $Proxy10

cglib:
key “temServiceImpl”
value TemServiceImpl E n h a n c e r B y S p r i n g C G L I B EnhancerBySpringCGLIB EnhancerBySpringCGLIB445220ba

9、第8条中的信息是从哪里获得第的?

第8条的信息是通过一个断点,然后debug加载Spring 配置文件后,通过IDE(eclipse或IDEA)查看断点信息得到的.
断点的位置为:org.springframework.context.support.AbstractApplicationContext.getBean(String name)
的 return getBeanFactory().getBean(name); 这一行
你可以通过快捷键获取 getBeanFactory() 的信息,
或者在IDE中也提供了这样的视窗,如eclipse中 Variables-this-beanfactory-singletonObjects 然后一个一个查看就行了

10、关键测试代码
相关类UML图

在这里插入图片描述

配置文件

这里放置在 src/main/resources 下,名称配置下面测试方法取名为 applicationContext2.xml,读者可以自定义

<context:component-scan base-package="包路径"/>
<aop:aspectj-autoproxy/>
实体类

@NotNull 标签单独添加 maven 依赖 https://mvnrepository.com/artifact/javax.validation/validation-api/2.0.1.Final

    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
public class Person {
	/**姓名*/
	@NotNull(message = "name不可为空")
	private String name;
	/**年龄*/
	@Size(min = 1,max=200,message = "age应在1到200之间")
	private int age;
	/**备注*/
	private String comments;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	
	public String getComments() {
		return comments;
	}
	public void setComments(String comments) {
		this.comments = comments;
	}
	@Override
	public String toString() {
		/*
		 * 查看对方包的 maven依赖:https://mvnrepository.com/artifact/org.apache.commons/commons-lang3/3.9
		 **/
		return ToStringBuilder.reflectionToString(this);
	}
	
}
结果返回类
public class ResponseBody {
	
	private String code;
	private String msg;
	public String getCode() {
		return code;
	}
	public void setCode(String code) {
		this.code = code;
	}
	public String getMsg() {
		return msg;
	}
	public void setMsg(String msg) {
		this.msg = msg;
	}

	public ResponseBody(String code,String msg) {
		this.code=code;
		this.msg=msg;
	}
	
	public ResponseBody() {
		
	}
	
}
接口类
public interface TemService {
	ResponseBody sayHello(Person p);
	void setBeanName(String name);
}
接口实现类
@Component
//public class TemServiceImpl {
public class TemServiceImpl implements TemService,BeanNameAware{

	@Override
	public ResponseBody sayHello(Person p) {
		System.out.println("进入 sayHello");
		ResponseBody r=new ResponseBody("SUCCESS","调用成功");
		
		setBeanName("sayHello内部直接调用同类方法setBeanName,不会通过代理对象调用无AOP增强,除非单独通过AOP调用setBeanName");
		return r;
	}

	@Override
	public void setBeanName(String name) {
		System.out.println("调用setBeanName输出:"+name);
	}

}
切点配置类

如果需要指定任意包和子包,下的类和方法 使用 … 即可代表任意包和子包,* 代表任意类,*代表任意方法

// tem 包和其子包,任意类,任意方法,任意参数
@Pointcut("execution(* com.bestcxx.stu.tem..*.*(..))")
@Aspect
@Component
public class MyAspect {
	/**
	 * 定义切点
	 * @param point
	 * @return
	 */

	@Pointcut("execution(* com.bestcxx.stu.tem.aspect.service.TemService.sayHello(..))")
	//@Pointcut("execution(* com.bestcxx.stu.tem.aspect.serviceimpl.TemServiceImpl.sayHello(..))")
	public void point() {
	}
	
	/**
	 * 切面的处理方法
	 * @param point
	 * @return
	 */
	@Around(value="point()")
	public Object around(ProceedingJoinPoint point) {
		System.out.println("来自@aspect的@Around注解的通知:这是一个切点");
		try {
			if(point==null) {
				System.out.println("没有参数");
				//正常逻辑不受影响
				Object result = point.proceed();
				return result;
			}
			//获取第一个参数
			//本切面拦截的方法只有一个入参 Person
			Object object0=point.getArgs()[0];
			
			//本例子作为测试方法,限定了对 Person类 的校验,如果需要通用方法可以去除本if方法
			if(object0 instanceof Person) {
				//方法内部对 object0 对应对注解和成员变量值对约定进行匹配
				//checkFieldsByObject(object0);
				System.out.println("方法1");
			}else {
				System.out.println("应该是方法2“"+object0);
			}
			//正常逻辑不受影响
			Object result = point.proceed();
			return result;
		}catch(Throwable t) {
			//如果上面的校验抛出了异常,在这里进行捕获
			ResponseBody r=new ResponseBody();
			r.setCode("ERROR");
			r.setMsg(t.getMessage());
			return r;
		}
	}
}
配置文件
  <aop:aspectj-autoproxy/>
  <!-- <aop:aspectj-autoproxy proxy-target-class="true"/> -->
  <context:component-scan base-package="com.bestcxx.stu.tem.aspect"/>
  • maven 依赖
	<dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>1.9.2</version>
  </dependency>

  <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.2</version>
  </dependency>
	
 <dependency>
   <groupId>cglib</groupId>
   <artifactId>cglib</artifactId>
   <version>2.2.2</version>
  </dependency>
<dependency>
 <groupId>org.apache.commons</groupId>
 <artifactId>commons-lang3</artifactId>
  <version>3.9</version>
</dependency>
测试方法
  • 方法1
static GenericApplicationContext ctx;
	
static {
	ctx = new GenericApplicationContext();
	XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ctx);
	xmlReader.loadBeanDefinitions(new ClassPathResource("test/applicationcontext-tem.xml"));
	ctx.refresh();
}
	
//@Autowired 这里不要用注解的方式注入,否则无法复现
private TemServiceImpl temServiceImpl;
	
@Test
public void testSayHello2() {
	temServiceImpl = (TemServiceImpl) ctx.getBean("temServiceImpl");
}
  • 方法2
public static void main(String[] args) {
		ApplicationContext application=new ClassPathXmlApplicationContext("classpath:applicationContext2.xml");
		
		TemService temService=application.getBean(TemService.class);
		Person p=new Person();
		p.setAge(1);
		p.setComments("描述信息");
		p.setName("jecket");
		//sayHello 内部直接调用 setBeanName
		System.out.println(temService.sayHello(p));
		
		//单独调用 setBeanName
		System.out.println();
		System.out.println();
		temService.setBeanName("单独调用 setBeanName,触发AOP");
	}

测试2结果

调用setBeanName输出:temServiceImpl
来自@aspect的@Around注解的通知:这是一个切点
方法1
进入 sayHello
调用setBeanName输出:sayHello内部直接调用同类方法setBeanName,不会通过代理对象调用无AOP增强,除非单独通过AOP调用setBeanName
com.bestcxx.test.ResponseBody@67b14530


来自@aspect的@Around注解的通知:这是一个切点
应该是方法2“单独调用 setBeanName,触发AOP
调用setBeanName输出:单独调用 setBeanName,触发AOP
11、通过AOP代理对象调用方法一,方法一内部间接调用方法二无AOP增强

本文提供的测试代码可以直接使用方法2测试,
debug可以发现,只有通过代理对象触发切点才会触发切面的通知,否则相当于直接调用了某个普通类.

避免内部直接调用解决该问题

在日常使用中注意这种情况,比如使用AOP实现日志功能,需要直接通过Spring Bean调用,以一个代理对象调用切点方法,而不是方法内部.

通过Spring 配置解决该问题

Spring 提供了一个配置参数 expose-proxy

<aop:aspectj-autoproxy expose-proxy="true"/>

然后需要在代码中将 this.方法的调用修改为((目标Interface)AopContext.currentProxy()).方法()的形式。

如果使用注解则是
@EnableAspectJAutoProxy(exposeProxy = true)
方法层面保持不变

12、@Aspect 修饰的类中方法可以作为切点吗?

测试发现,不可以,自己不可以设置自己,其他的切面类也无法达到这个效果,后面看源码后看看Bean的具体加载机制再补充.
被AOP修饰的是proxy Spring bean,被@Aspect 和 @Component 修饰的是普通 Spring bean.

在跟踪源码的过程中也可以发现,对于基础类的 Bean 和被特别声明不可为 代理的bean 都不会被设置为代理对象,也即不可以被设置为切点。

13、同一个切点可以设置多个切面吗?

可以,而且从debug,来看,当调用到切点方法时,AOP会利用代理机制获取一个chain

在这里插入图片描述

14、切面在目标对象的哪个生命周期应用到目标对象并创建新的代理对象?

·编译期:切面在目标类编译时被织入.这种方式需要特殊的编译器.AspectJ的织入编译器就是以这种方式织入切面的.
·类加载期:切面在目标类加载到JVM时被织入.这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强改目标类的字节码.AspectJ5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面.
·运行期:切面在应用运行的某个时刻被织入.一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象.Spring AOP就是以这种方式织入切面的.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值