7038 字
20 分钟
Spring AOP原理与自定义注解详解

spring-aop原理#

简介

Aspect Oriented Programming 面向切面编程。解耦是程序员编码开发过程中一直追求的。AOP也是为了解耦所诞生。

具体思想是:定义一个切面,在切面的纵向定义处理方法,处理完成之后,回到横向业务流。

AOP 在Spring框架中被作为核心组成部分之一,的确Spring将AOP发挥到很强大的功能。最常见的就是事务控制。工作之余,对于使用的工具,不免需要了解其所以然。学习了一下,写了些程序帮助理解。

AOP 主要是利用代理模式的技术来实现的。

1、静态代理:#

就是设计模式中的proxy模式

a.业务接口

b.业务实现

c.代理对象

d.测试类

从这段代码可以看出来,代理模式其实就是AOP的雏形。 上端代码中talk(String msg, String singname)是一个切面。在代理类中的sing(singname)方法是个后置处理方法。

这样就实现了,其他的辅助方法和业务方法的解耦。业务不需要专门去调用,而是走到talk方法,顺理成章的调用sing方法

再从这段代码看:1、要实现代理方式,必须要定义接口。2、每个业务类,需要一个代理类。

2、动态代理:#

jdk1.5中提供,利用反射。实现InvocationHandler接口。

a.代理类

b.测试类

输出结果会是:

切面之前执行 people talk业务说法 切面之后执行

说明只要在业务调用方法切面之前,是可以动态的加入需要处理的方法。

从代码来看,如果再建立一个业务模块,也只需要一个代理类。ITalk iTalk = (ITalk) new DynamicProxy().bind(new PeopleTalk()); 将业务接口和业务类绑定到动态代理类。

但是这种方式:还是需要定义接口。

3、利用cglib#

a.业务类

b.代理类

c.测试类

最后输出结果:

事物开始 people talk业务方法 事物结束 事物开始 spreak chinese业务方法 事物结束

spring-boot-starter-aop依赖#

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

引入了AOP的场景启动器,我们点击去看看

还是引入了spring-aop和aspectj的依赖,和我们Spring集成AOP是引入了相同的包,接着我们直接就可以创建Advisor了,如上AspectJTest这个类,但是我们并没有通过@EnableAspectJAutoProxy开启AOP呢?那是因为AOP的自动配置类帮我们开启了

Java AOP#

apo专业术语图解:

通知方法为:要写入的方法

连接点图中一共16个

开发模式@Aspect注解说明

2 注解说明#

2.1 @Aspect

作用是把当前类标识为一个切面供容器读取

2.2 @Before 标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有

2.3 @AfterReturning

后置增强,相当于AfterReturningAdvice,方法正常退出时执行

2.4 @AfterThrowing

异常抛出增强,相当于ThrowsAdvice

2.5 @After

final增强,不管是抛出异常或者正常退出都会执行

2.6 @Around

环绕增强,相当于MethodInterceptor,前面四种的合体:

try{
@before 前置通知
method.invoke();
@afterreturning 返回通知
}catch(e){
@afterthrowing 异常通知
}finally{
@after 后置通知
}

3 execution切点函数#

execution函数用于匹配方法执行的连接点,语法为:

execution(方法修饰符(可选)  返回类型  方法名  参数  异常模式(可选)) 

 

参数部分允许使用通配符:

  •  匹配任意字符,但只能匹配一个元素

.. 匹配任意字符,可以匹配任意多个元素,表示类时,必须和*联合使用

  •  必须跟在类名后面,如Horseman+,表示类本身和继承或扩展指定类的所有类

示例中的* chop(..)解读为:

方法修饰符  无

返回类型      *匹配任意数量字符,表示返回类型不限

方法名          chop表示匹配名称为chop的方法

参数               (..)表示匹配任意数量和类型的输入参数

异常模式       不限

 

更多示例:

void chop(String,int)

匹配目标类任意修饰符方法、返回void、方法名chop、带有一个String和一个int型参数的方法

public void chop(*)

匹配目标类public修饰、返回void、方法名chop、带有一个任意类型参数的方法

public String o(..)

 匹配目标类public修饰、返回String类型、方法名中带有一个o字符、带有任意数量任意类型参数的方法

public void o(String,..)

 匹配目标类public修饰、返回void、方法名中带有一个o字符、带有任意数量任意类型参数,但第一个参数必须有且为String型的方法

也可以指定类:

public void examples.chap03.Horseman.*(..)

匹配Horseman的public修饰、返回void、不限方法名、带有任意数量任意类型参数的方法

public void examples.chap03.man.(..)

匹配以man结尾的类中public修饰、返回void、不限方法名、带有任意数量任意类型参数的方法

指定包:

public void examples.chap03.*.chop(..)

匹配examples.chap03包下所有类中public修饰、返回void、方法名chop、带有任意数量任意类型参数的方法

public void examples..*.chop(..)

匹配examples.包下和所有子包中的类中public修饰、返回void、方法名chop、带有任意数量任意类型参数的方法 可以用这些表达式替换StorageAdvisor中的代码并观察效果

4 更多切点函数#

除了execution(),Spring中还支持其他多个函数,这里列出名称和简单介绍,以方便根据需要进行更详细的查询

4.1 @annotation()

表示标注了指定注解的目标类方法

例如 @annotation(org.springframework.transaction.annotation.Transactional) 表示标注了@Transactional的方法

4.2 args()

通过目标类方法的参数类型指定切点

例如 args(String) 表示有且仅有一个String型参数的方法

4.3 @args()

通过目标类参数的对象类型是否标注了指定注解指定切点

如 @args(org.springframework.stereotype.Service) 表示有且仅有一个标注了@Service的类参数的方法

4.4 within()

通过类名指定切点

如 within(examples.chap03.Horseman) 表示Horseman的所有方法

4.5 target()

通过类名指定,同时包含所有子类

如 target(examples.chap03.Horseman)  且Elephantman extends Horseman,则两个类的所有方法都匹配

4.6 @within()

匹配标注了指定注解的类及其所有子类

如 @within(org.springframework.stereotype.Service) 给Horseman加上@Service标注,则Horseman和Elephantman 的所有方法都匹配

4.7 @target()

所有标注了指定注解的类

如 @target(org.springframework.stereotype.Service) 表示所有标注了@Service的类的所有方法

4.8 this()

大部分时候和target()相同,区别是this是在运行时生成代理类后,才判断代理类与指定的对象类型是否匹配

5 逻辑运算符#

表达式可由多个切点函数通过逻辑运算组成

5.1 &&

与操作,求交集,也可以写成and

例如 execution(* chop(..)) && target(Horseman)  表示Horseman及其子类的chop方法

5.2 ||

或操作,求并集,也可以写成or

例如 execution(* chop(..)) || args(String)  表示名称为chop的方法或者有一个String型参数的方法

5.3 !

非操作,求反集,也可以写成not

6 实际使用:#

package com.example.aop.aop.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* DESC: *
* Created by zhengyong on 2019/6/23.
*/
@Aspect
@Component
//多切面时,使用order注解来排序,执行规则是,先进后出。(面包反弹)
@Order(1)
public class MyAspect {
/**
* 抽取重复切入点
* 要求:public void 方法 且未实现的方法
*/
@Pointcut("execution(public Object com..service.MethodBusiness.*(int,int))")
public void testPoint(){}
/**
* 抽取重复切入点 抓取注解
* 要求:public void 方法 且未实现的方法
*/
@Pointcut(value = "@annotation(com.zy.ossspringbootstarter.annotation.TestAnnotation)")
public void pointCut(){};
/**
* 告诉spring何时执行
* try{
* @before 前置通知
* method.invoke();
* @afterreturning 返回通知
* }catch(e){
* @afterthrowing 异常通知
* }finally{
* @after 后置通知
* }
*
* @Before 目标方法执行前
* @AfterReturning 目标方法正常返回
* @AfterThrowing 目标方法抛出异常后
* @After 最后执行
* @Around 环绕
*/
//before表示何时执行,切入点表达式表示何时执行
@Before(value = "testPoint()")
public static void testBefore() {
System.out.println("切面,前置通知");
}
/**
* 切入点表达式:
* 固定格式: execution(访问权限符 返回值类型 方法类全名(参数列表))
* 通配符:
* * :
* 1)匹配一个或者多个字符 execution(public Object com.example.aop.aop.service.Method*.*(int,int))
* 2)匹配任意一个参数(可能会重载无法切入) execution(public Object com.example.aop.aop.service.Method*.*(int,*))
* 3)只能匹配一层路径
* 4)*不能代替访问权限符,可以省略
* ..:
* 1)匹配任意个数任意参数(表达式开头不能已双点开始)
* execution(public Object com.example.aop.aop.service.MethodBusiness.*(..))
* 2)匹配任意多层路径
* execution(public Object com..service.MethodBusiness.*(int,int))
* 最精确的
* execution(public Object com.example.aop.aop.service.MethodBusiness.add(int,int))
* 最模糊的
* execution(* *.*(..)) 千万别写
*
* aferReturning可以拿到结果信息
* Object result 方法参数列表中写入接受结果的类型
* 告诉spring 返回结果用什么参数来接收:returning = "result"
*/
@AfterReturning(value = "execution(public Object com.example.aop.aop.service.Method*.*(*,*))",returning = "result")
public static void testAfterReturning(JoinPoint joinPoint,Object result) {
System.out.println("切面,返回通知,方法名:"+joinPoint.getSignature().getName()+",结果是:"+result);
}
/**
* 通知执行顺序
* 正常:@before @after(后置通知) @aferReturnin(返回通知)
* 异常:@before @after(后置通知) @aferThrowing(异常通知)
*
* @AfterThrowing 可以获取异常信息
*/
@AfterThrowing(value = "execution(public Object com.example.aop.aop.service.MethodBusiness.*(int,int))",throwing ="e")
public static void testAfterThrowing(JoinPoint joinPoint,Exception e){
System.out.println("切面,异常通知,异常是:"+e);
}
/**
* joinPoint获取 方法名和 方法参数
*/
@After(value = "execution(public Object com.example.aop.aop.service.MethodBusiness.*(..))")
public static void testAfter(JoinPoint joinPoint){
//获取签名---获取方法名
String name = joinPoint.getSignature().getName();
System.out.println("方法名:"+name);
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println("方法参数列表:"+arg);
}
System.out.println("切面,后置通知");
}
/**
* 环绕方法就是动态代理
* 和普通方法的却别就是,会直接影响目标方法
环绕通知,只影响当前切面中的通知执行顺序(比较深入了解执行顺序)
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around(value = "execution(public Object com.example.aop.aop.service.MethodBusiness.*(..))")
public static Object testAround(ProceedingJoinPoint joinPoint) throws Throwable{
Object[] args = joinPoint.getArgs();
Object proceed = null;
try{
//before
//利用反射,执行目标方法,需要参数,就是mothod.invoke()
proceed= joinPoint.proceed(args);
//afterReturning
}catch(Exception e){
//afterThrowing
//如果有异常,要抛出去(看需求)
}finally {
//after
}
//反射调用后,返回值一定也要返回出去
return proceed;
}
}

多切面执行顺序图解:#

面包反弹原理

aop获取注解上的值#

@Before(value = "@annotation(com.example.aop.aop.annotation.DoSomething)")
public void before(JoinPoint joinPoint){
//获取签名
Signature signature = joinPoint.getSignature();
//获取强转为方法签名
MethodSignature methodSignature = (MethodSignature) signature;
//获取方法上的注解
DoSomething annotation = methodSignature.getMethod().getAnnotation(DoSomething.class);
//获取注解上的参数的值
if(annotation.isDoSomething()){
System.out.println("【老铁你来了】");
}else{
System.out.println("【给老子滚蛋】");
}
//获取参数
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println("参数是:"+arg);
}
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
for (String paramName : paramNames) {
System.out.println("参数名是"+paramName);
}
System.out.println("注解前置方法");
}
/**
* (joinPoint.getArgs()[0]) 获取某个参数
* (joinPoint.getArgs()[0]).getClass() 获取参数类型(反射)
* (joinPoint.getArgs()[0]).getClass().getDeclaredField(amountName) 获取属性
* 不能访问类的成员。带有修饰词“private”的属性 eg aClass.getDeclaredField(bizOrderNoName).get(object)
* 报错。解决方法,通过get方法获取。
* String bizOrderNo = (String) object.getClass().getDeclaredMethod("getBizOrderNo").invoke(object);
*/

在SpringAOP中如何获取请求的参数和参数名#

private static Map getFieldsName(ProceedingJoinPoint joinPoint) throws ClassNotFoundException, NoSuchMethodException {
String classType = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
// 参数值
Object[] args = joinPoint.getArgs();
Class<?>[] classes = new Class[args.length];
for (int k = 0; k < args.length; k++) {
if (!args[k].getClass().isPrimitive()) {
// 获取的是封装类型而不是基础类型
String result = args[k].getClass().getName();
Class s = map.get(result);
if(args[k].getClass().getName().equals(RequestFacade.class.getName())){
//处理HttpServletRequest请求参数,导致反射找不到方法
classes[k] = s == null ? HttpServletRequest.class: s;
}else if(args[k].getClass().getName().equals(ResponseFacade.class.getName())){
classes[k] = s == null ? HttpServletResponse.class: s;
}else {
classes[k] = s == null ? args[k].getClass() : s;
}
}
}
ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
// 获取指定的方法,第二个参数可以不传,但是为了防止有重载的现象,还是需要传入参数的类型
Method method = Class.forName(classType).getMethod(methodName, classes);
// 参数名
String[] parameterNames = pnd.getParameterNames(method);
// 通过map封装参数和参数值
HashMap<String, Object> paramMap = new HashMap();
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
return paramMap;
}
private static HashMap<String, Class> map = new HashMap<String, Class>() {
{
put("java.lang.Integer", int.class);
put("java.lang.Double", double.class);
put("java.lang.Float", float.class);
put("java.lang.Long", long.class);
put("java.lang.Short", short.class);
put("java.lang.Boolean", boolean.class);
put("java.lang.Char", char.class);
}
};

Spring的AOP顺序#

Spring的循环依赖

AOP常用注解:

@Before 前置通知:目标方法之前执行

@After 后置通知:目标方法之后执行(始终执行)

@AfterReturning 返回后通知:执行方法结束前执行(异常不执行)

@AfterThrowing 异常通知:出现异常时候执行

@Around 环绕通知:环绕目标方法执行

面试题#

你肯定知道spring,那说说aop的全部通知顺序springboot或springboot2对aop的执行顺序影响?

说说你使用AOP中碰到的坑?

28_spring4下的aop测试案例#

新建Maven工程,pom.xml如下:

pom#
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lun</groupId>
<artifactId>HelloSpringBoot</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>HelloSpringBoot</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- <version>1.5.9.RELEASE</version〉 ch/qos/Logback/core/joran/spi/JoranException解决方案-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.3</version>
</dependency>
<!-- web+actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- SpringBoot与Redis整合依赖 -->
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<!-- Spring Boot AOP技术-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
<!-- 一般通用基础配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
启动类#
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
接口CalcService#
public interface CalcService {
public int div(int x, int y);
}
接口实现类CalcServiceImpl新加@Service#
import org.springframework.stereotype.Service;
@Service
public class CalcServiceImpl implements CalcService {
@Override
public int div(int x, int y) {
int result = x / y;
System.out.println("===>CalcServiceImpl被调用,计算结果为:" + result);
return result;
}
}

新建一个切面类MyAspect并为切面类新增两个注解:

  • @Aspect 指定一个类为切面类
  • @Component 纳入Spring容器管理
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
@Before("execution(public int com.lun.interview.service.CalcServiceImpl.*(..))")
public void beforeNotify() {
System.out.println("********@Before我是前置通知");
}
@After("execution(public int com.lun.interview.service.CalcServiceImpl.*(..))")
public void afterNotify() {
System.out.println("********@After我是后置通知");
}
@AfterReturning("execution(public int com.lun.interview.service.CalcServiceImpl.*(..))")
public void afterReturningNotify() {
System.out.println("********@AfterReturning我是返回后通知");
}
@AfterThrowing(" execution(public int com.lun.interview.service.CalcServiceImpl.*(..))")
public void afterThrowingNotify() {
System.out.println("********@AfterThrowing我是异常通知");
}
@Around(" execution(public int com.lun.interview.service.CalcServiceImpl.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object retvalue = null;
System.out.println("我是环绕通知之前AAA");
retvalue = proceedingJoinPoint.proceed();
System.out.println("我是环绕通知之后BBB");
return retvalue ;
}
}
测试类#
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.SpringVersion;
import org.springframework.test.context.junit4.SpringRunner;
import com.lun.interview.service.CalcService;
@SpringBootTest
@RunWith(SpringRunner.class)
public class AopTest {
@Resource
private CalcService calcService;
@Test
public void testAop4() {
System.out.println(String.format("Spring Verision : %s, Sring Boot Version : %s.", //
SpringVersion.getVersion(), SpringBootVersion.getVersion()));
calcService.div(10, 2);
}
}
spring4下的aop测试结果#
Spring Verision : 4.3.13.RELEASE, Sring Boot Version : 1.5.9.RELEASE.
我是环绕通知之前AAA
********@Before我是前置通知
===>CalcServiceImpl被调用,计算结果为:5
我是环绕通知之后BBB
********@After我是后置通知
********@AfterReturning我是返回后通知

修改测试类,让其抛出算术异常类:

@SpringBootTest
@RunWith(SpringRunner.class)
public class AopTest {
@Resource
private CalcService calcService;
@Test
public void testAop4() {
System.out.println(String.format("Spring Verision : %s, Sring Boot Version : %s.", //
SpringVersion.getVersion(), SpringBootVersion.getVersion()));
//calcService.div(10, 2);
calcService.div(10, 0);//将会抛异常
}
}

结果

Spring Verision : 4.3.13.RELEASE, Sring Boot Version : 1.5.9.RELEASE.
我是环绕通知之前AAA
********@Before我是前置通知
********@After我是后置通知
********@AfterThrowing我是异常通知
java.lang.ArithmeticException: / by zero
at com.lun.interview.service.CalcServiceImpl.div(CalcServiceImpl.java:10)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
...
小结#

AOP执行顺序:

  • 正常情况下:@Before前置通知----->@After后置通知----->@AfterRunning正常返回
  • 异常情况下:@Before前置通知----->@After后置通知----->@AfterThrowing方法异常

spring5下的aop测试#

修改POM#
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<!-- 1.5.9.RELEASE -->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lun</groupId>
<artifactId>HelloSpringBoot</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>HelloSpringBoot</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- web+actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- SpringBoot与Redis整合依赖 -->
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<!-- Spring Boot AOP技术-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
<!-- 一般通用基础配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
修改测试类#
import javax.annotation.Resource;
import org.junit.jupiter.api.Test;
//import org.junit.Test;
//import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.SpringVersion;
//import org.springframework.test.context.junit4.SpringRunner;
import com.lun.interview.service.CalcService;
@SpringBootTest
//@RunWith(SpringRunner.class)
public class AopTest {
@Resource
private CalcService calcService;
@Test
public void testAop5() {
System.out.println(String.format("Spring Verision : %s, Sring Boot Version : %s.", //
SpringVersion.getVersion(), SpringBootVersion.getVersion()));
System.out.println();
calcService.div(10, 2);
//calcService.div(10, 0);
}
}

输出结果

Spring Verision : 5.2.8.RELEASE, Sring Boot Version : 2.3.3.RELEASE.
我是环绕通知之前AAA
********@Before我是前置通知
===>CalcServiceImpl被调用,计算结果为:5
********@AfterReturning我是返回后通知
********@After我是后置通知
我是环绕通知之后BBB

修改测试类,让其抛出算术异常类:

@SpringBootTest
public class AopTest {
@Resource
private CalcService calcService;
...
@Test
public void testAop5() {
System.out.println(String.format("Spring Verision : %s, Sring Boot Version : %s.", //
SpringVersion.getVersion(), SpringBootVersion.getVersion()));
System.out.println();
calcService.div(10, 2);
//calcService.div(10, 0);
}
}

输出结果

Spring Verision : 5.2.8.RELEASE, Sring Boot Version : 2.3.3.RELEASE.
我是环绕通知之前AAA
********@Before我是前置通知
********@AfterThrowing我是异常通知
********@After我是后置通知
java.lang.ArithmeticException: / by zero
at com.lun.interview.service.CalcServiceImpl.div(CalcServiceImpl.java:10)
at com.lun.interview.service.CalcServiceImpl$$FastClassBySpringCGLIB$$355acbc4.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)

总节#

spring4 aop执行顺序:

  • 正常情况下:@Before前置通知----->@After后置通知----->**@AfterRunning正常返回**
  • 异常情况下:@Before前置通知----->@After后置通知----->**@AfterThrowing方法异常**

spring5 aop执行顺序(将After想象成finally):

  • 正常情况下:@Before前置通知----->**@AfterRunning正常返回**----->@After后置通知
  • 异常情况下:@Before前置通知----->**@AfterThrowing方法异常**----->@After后置通知

image-20240319103609456

Spring的循环依赖#

恶心的大厂面试题。

  • 你解释下spring中的三级缓存?
  • 三级缓存分别是什么?三个Map有什么异同?
  • 什么是循环依赖?请你谈谈?看过spring源码吗?
  • 如何检测是否存在循环依赖?实际开发中见过循环依赖的异常吗?
  • 多例的情况下,循环依赖问题为什么无法解决?

什么是循环依赖?#

多个bean之间相互依赖,形成了一个闭环。比如:A依赖于B、B依赖于C、C依赖于A。

通常来说,如果问Spring容器内部如何解决循环依赖,一定是指默认的单例Bean中,属性互相引用的场景。

image-20240319104418423

两种注入方式对循环依赖的影响

循环依赖官网说明

Circular dependencies

If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.

For example: Class A requires an instance of class B through constructor injection, and class B requires an instance of class A through constructor injection. If you configure beans for classes A and B to be injected into each other, the Spring IoC container detects this circular reference at runtime, and throws a BeanCurrentlyInCreationException.

One possible solution is to edit the source code of some classes to be configured by setters rather than constructors. Alternatively, avoid constructor injection and use setter injection only. In other words, although it is not recommended, you can configure circular dependencies with setter injection.

Unlike the typical case (with no circular dependencies), a circular dependency between bean A and bean B forces one of the beans to be injected into the other prior to being fully initialized itself (a classic chicken-and-egg scenario).

结论#

我们AB循环依赖问题只要A的注入方式是setter且singleton ,就不会有循环依赖问题。

spring循环依赖纯java代码验证案例#

Spring容器循环依赖报错演示BeanCurrentlylnCreationException

循环依赖现象在spring容器中注入依赖的对象,有2种情况

  • 构造器方式注入依赖(不可行)
  • 以set方式注入依赖(可行)

构造器方式注入依赖(不可行)

@Component
public class ServiceB{
private ServiceA serviceA;
public ServiceB(ServiceA serviceA){
this.serviceA = serviceA;
}
}
@Component
public class ServiceA{
private ServiceB serviceB;
public ServiceA(ServiceB serviceB){
this.serviceB = serviceB;
}
}
public class ClientConstructor{
public static void main(String[] args){
new ServiceA(new ServiceB(new ServiceA()));//这会抛出编译异常
}
}

以set方式注入依赖(可行)

@Component
public class ServiceBB{
private ServiceAA serviceAA;
public void setServiceAA(ServiceAA serviceAA){
this.serviceAA = serviceAA;
System.out.println("B里面设置了A");
}
}
@Component
public class ServiceAA{
private ServiceBB serviceBB;
public void setServiceBB(ServiceBB serviceBB){
this.serviceBB = serviceBB;
System.out.println("A里面设置了B");
}
}
public class ClientSet{
public static void main(String[] args){
//创建serviceAA
ServiceAA a = new ServiceAA();
//创建serviceBB
ServiceBB b = new ServiceBB();
//将serviceA入到serviceB中
b.setServiceAA(a);
//将serviceB法入到serviceA中
a.setServiceBB(b);
}
}

输出结果:

B里面设置了A
A里面设置了B

spring循环依赖bug演示#

beans:A,B

public class A {
private B b;
public B getB() {
return b;
}
public void setB(B b) {
this.b = b;
System.out.println("A call setB.");
}
}
public class B {
private A a;
public A getA() {
return a;
}
public void setA(A a) {
this.a = a;
System.out.println("B call setA.");
}
}

运行类

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ClientSpringContainer {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
A a = context.getBean("a", A.class);
B b = context.getBean("b", B.class);
}
}

单例#

默认的单例(Singleton)的场景是支持循环依赖的,不报错

beans.xml

<?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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<bean id="a" class="com.lun.interview.circular.A">
<property name="b" ref="b"></property>
</bean>
<bean id="b" class="com.lun.interview.circular.B">
<property name="a" ref="a"></property>
</bean>
</beans>

输出结果

00:00:25.649 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6d86b085
00:00:25.828 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [beans.xml]
00:00:25.859 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'a'
00:00:25.875 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'b'
B call setA.
A call setB.

多例#

原型(Prototype)的场景是不支持循环依赖的,会报错

beans.xml

<?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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<bean id="a" class="com.lun.interview.circular.A" scope="prototype">
<property name="b" ref="b"></property>
</bean>
<bean id="b" class="com.lun.interview.circular.B" scope="prototype">
<property name="a" ref="a"></property>
</bean>
</beans>

输出结果

00:01:39.904 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6d86b085
00:01:40.062 [main] DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 2 bean definitions from class path resource [beans.xml]
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'a' defined in class path resource [beans.xml]: Cannot resolve reference to bean 'b' while setting bean property 'b'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b' defined in class path resource [beans.xml]: Cannot resolve reference to bean 'a' while setting bean property 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:342)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1697)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1442)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:342)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1115)
at com.lun.interview.circular.ClientSpringContainer.main(ClientSpringContainer.java:10)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'b' defined in class path resource [beans.xml]: Cannot resolve reference to bean 'a' while setting bean property 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:342)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1697)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1442)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:593)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:342)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
... 9 more
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:268)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330)
... 17 more

重要结论#

(spring内部通过3级缓存来解决循环依赖) - DefaultSingletonBeanRegistry

只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。

第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象。

第二级缓存:earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完。)

第三级缓存:Map<String, ObjectFactory<?>> singletonFactories,存放可以生成Bean的工厂。

package org.springframework.beans.factory.support;
...
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
...
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
...
}

spring循环依赖debug前置知识#

三层map#

实例化 - 内存中申请一块内存空间,如同租赁好房子,自己的家当还未搬来。

初始化属性填充 - 完成属性的各种赋值,如同装修,家具,家电进场。

3个Map和四大方法,总体相关对象

image-20240319105038901

第一层singletonObjects存放的是已经初始化好了的Bean。(成品)

第二层earlySingletonObjects存放的是实例化了,但是未初始化的Bean。(半成品)

第三层singletonFactories存放的是FactoryBean。当一个bean正在创建过程中,会被放入该缓存中。(准备生产)假如A类实现了FactoryBean,那么依赖注入的时候不是A类,而是A类产生的Bean。

package org.springframework.beans.factory.support;
...
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
...
/**
单例对象的缓存:bean名称—bean实例,即:所谓的单例池。
表示已经经历了完整生命周期的Bean对象
第一级缓存
*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/**
早期的单例对象的高速缓存: bean名称—bean实例。
表示 Bean的生命周期还没走完(Bean的属性还未填充)就把这个 Bean存入该缓存中也就是实例化但未初始化的 bean放入该缓存里
第二级缓存
*/
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/**
单例工厂的高速缓存:bean名称—ObjectFactory
表示存放生成 bean的工厂
第三级缓存
*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
...
}

A / B两对象在三级缓存中的迁移说明#

  1. A创建过程中需要B,于是A将自己放到三级缓里面,去实例化B。
  2. B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A。
  3. B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态),然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
@FunctionalInterface
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}

spring循环依赖小总结#

Spring创建 bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化

每次创建 bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个

当我们创建 beanA的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了beanB,接着就又去创建beanB,同样的流程,创建完beanB填充属性时又发现它依赖了beanA又是同样的流程, 不同的是:这时候可以在三级缓存中查到刚放进去的原始对象beanA.所以不需要继续创建,用它注入 beanB,完成 beanB的创建

既然 beanB创建好了,所以 beanA就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成。

Spring解决循环依赖依靠的是Bean的”中间态”这个概念,而这个中间态指的是已经实例化但还没初始化的状态—>半成债。实例化的过程又是通过构造器创建的,如果A还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决。”对

Spring为了解决单例的循坏依赖问题,使用了三级缓存:

其中一级缓存为单例池(singletonObjects)。

二级缓存为提前曝光对象(earlySingletonObjects)。

三级级存为提前曝光对象工厂(singletonFactories) 。

假设A、B循环引用,实例化A的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了B,同样的流程也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖A,这时候从缓存中查找到早期暴露的A,没有AOP代理的话,直接将A的原始对象注入B,完成B的初始化后,进行属性填充和初始化,这时候B完成后,就去完成剩下的A的步骤,如果有AOP代理,就进行AOP处理获取代理后的对象A,注入B,走剩下的流程。

Spring解决循环依赖过程:#

  1. 调用doGetBean()方法,想要获取beanA,于是调用getSingleton()方法从缓存中查找beanA
  2. 在getSingleton()方法中,从一级缓存中查找,没有,返回null
  3. doGetBean()方法中获取到的beanA为null,于是走对应的处理逻辑,调用getSingleton()的重载方法(参数为ObjectFactory的)
  4. 在getSingleton()方法中,先将beanA_name添加到一个集合中,用于标记该bean正在创建中。然后回调匿名内部类的creatBean方法
  5. 进入AbstractAutowireCapableBeanFactory#ndoCreateBean,先反射调用构造器创建出beanA的实例,然后判断<是否为单例>、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中(即是否在第四步的集合中)。判断为true则将beanA添加到【三级缓存】中
  6. 对beanA进行属性填充,此时检测到beanA依赖于beanB,于是开始查找beanB
  7. 调用doGetBean()方法,和上面beanA的过程一样,到缓存中查找beanB,没有则创建,然后给beanB填充属性
  8. 此时 beanB依赖于beanA,调用getSingleton()获取beanA,依次从一级、二级、三级缓存中找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject,此时这个singletonObject指向的就是上面在doCreateBean()方法中实例化的beanA
  9. 这样beanB就获取到了beanA的依赖,于是beanB顺利完成实例化,并将beanA从三级缓存移动到二级缓存中
  10. 随后beanA继续他的属性填充工作,此时也获取到了beanB,beanA也随之完成了创建,回到getsingleton()方法中继续向下执行,将beanA从二级缓存移动到一级缓存中

衍生问题#

问题 1<为什么构造器注入属性无法解决循环依赖问题>?#

由于 spring 中的 bean 的创建过程为先实例化,再初始化(在进行对象实例化的过程中不必赋值)将实例化好的对象暴露出去,供其他对象调用,然而使用构造器注入,必须要使用构造器完成对象的初始化的操作,就会陷入死循环的状态

问题 2<一级缓存能不能解决循环依赖问题>? 不能#

在三个级别的缓存中存储的对象是有区别的,一级缓存为完全实例化且初始化的对象,二级缓存实例化但未初始化对象,如果只有一级缓存,如果是并发操作下,就有可能取到实例化但未初始化的对象存入一级缓存,就会出现问题。

问题 3<二级缓存能不能解决循环依赖问题>?#

理论上二级缓存可以解决循环依赖问题,但是需要注意,为什么需要在三级缓存中存储匿名内部类(ObjectFactory),原因在于需要创建代理对象

eg<现有> A 类,需要生成代理对象A, 是否需要进行实例化(需要) 。

在三级缓存中存放的是生成具体对象的一个匿名内部类,该类可能是代理类也可能是普通的对象,而使用三级缓存可以保证无论是否需要

是代理对象,都可以保证使用的是同一个对象,而不会出现一会儿使用普通bean一会儿使用代理类。

另一种回答:

尽管二级缓存已经解决了大部分循环依赖的问题,但在某些特殊情况下,仍然需要使用三级缓存。

  • 当存在多个构造函数时,二级缓存无法正确解决循环依赖的问题。这是因为在创建bean时,Spring无法确定应该使用哪个构造函数进行实例化。而三级缓存可以解决这个问题,通过在三级缓存中存储正在创建的bean对象,Spring可以在实例化时选择正确的构造函数。
  • 当存在循环依赖的链路过长时,二级缓存可能无法正确解决循环依赖。在链路过长的情况下,二级缓存可能无法正确地维护依赖关系,导致循环依赖无法解决。而三级缓存通过在创建过程中存储bean对象的引用,可以更好地处理链路过长的情况。

问题4:为什么多例不能解决循环依赖问题?#

只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,而非单例的bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。


自定义注解#

一:什么是注解?#

对于很多初次接触的开发者来说应该都有这个疑问?*Annontation*是Java5开始引入的新特征,中文名称叫注解。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。   Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。包含在 java.lang.annotation 包中。

二:注解的用处:#

1、生成文档。这是最常见的,也是java 最早提供的注解。常用的有@param @return 等 ​ 2、跟踪代码依赖性,实现替代配置文件功能。比如Dagger 2 依赖注入,未来java 开发,将大量注解配置,具有很大用处; ​ 3、在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。

三:注解的原理:#

注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。

四:元注解:#

java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解): @Documented – 注解是否将包含在JavaDoc中 @Retention – 什么时候使用该注解 @Target – 注解用于什么地方 @Inherited – 是否允许子类继承该注解

1.)@Retention – 定义该注解的生命周期 ● RetentionPolicy.SOURCE : 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。 ● RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式 ● RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。

2.)Target – 表示该注解用于什么地方。默认值为任何元素,表示该注解用于什么地方。可用的ElementType 参数包括 ● ElementType.CONSTRUCTOR: 用于描述构造器 ● ElementType.FIELD: 成员变量、对象、属性(包括enum实例) ● ElementType.LOCAL_VARIABLE: 用于描述局部变量 ● ElementType.METHOD: 用于描述方法 ● ElementType.PACKAGE: 用于描述包 ● ElementType.PARAMETER: 用于描述参数 ● ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明

3.)@Documented – 一个简单的Annotations 标记注解,表示是否将注解信息添加在java 文档中。

4.)@Inherited – 定义该注释和子类的关系 ​ @Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的annotation 类型被用于一个class,则这个annotation 将被用于该class 的子类。

五:常见标准的Annotation:#

1.)Override ​ java.lang.Override 是一个标记类型注解,它被用作标注方法。它说明了被标注的方法重载了父类的方法,起到了断言的作用。如果我们使用了这种注解在一个没有覆盖父类方法的方法时,java 编译器将以一个编译错误来警示。 2.)Deprecated ​ Deprecated 也是一种标记类型注解。当一个类型或者类型成员使用@Deprecated 修饰的话,编译器将不鼓励使用这个被标注的程序元素。所以使用这种修饰具有一定的“延续性”:如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员,虽然继承或者覆盖后的类型或者成员并不是被声明为@Deprecated,但编译器仍然要报警。 3.)SuppressWarnings ​ SuppressWarning 不是一个标记类型注解。它有一个类型为String[] 的成员,这个成员的值为被禁止的警告名。对于javac 编译器来讲,被-Xlint 选项有效的警告名也同样对@SuppressWarings 有效,同时编译器忽略掉无法识别的警告名。   @SuppressWarnings(“unchecked”)

六:自定义注解#

自定义注解类编写的一些规则:

1. Annotation 型定义为@interface, 所有的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口.
2. 参数成员只能用public 或默认(default) 这两个访问权修饰
3. 参数成员只能用基本类型byte、short、char、int、long、float、double、boolean八种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组.
4. 要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 Annotation 对象,因为你除此之外没有别的获取注解对象的方法
5. 注解也可以没有定义成员,,不过这样注解就没啥用了

PS<自定义注解需要使用到元注解>

七:我的第一个注解实例#

1、写一个自定义注解:#

/**
* 我的第一个注解
*/
//注解的使用范围
@Target({ElementType.METHOD,ElementType.TYPE})
//定义注解的生命周期
@Retention(RetentionPolicy.RUNTIME)
public @interface MyFirst {
int code();
String message();
boolean isNo() default true;
}

枚举和类注解示范:

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 水果颜色注解
*/
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitColor {
/**
* 颜色枚举
*/
public enum Color{ BLUE,RED,GREEN};
/**
* 颜色属性
*/
Color fruitColor() default Color.GREEN;
}

2、测试自定义注解的使用(简陋)#

package com.leyou.dos;
import com.leyou.annotation.MyFirst;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* <p>DESC: 测试注解开发</p>
* <p>DATE: 2019/5/23</p>
* <p>VERSION:1.0.0</p>
* <p>@AUTHOR: ZhengYong</p>
*/
@Component
@Data
@MyFirst(code = 90,message = "haah")
public class TestAnnotation {
private String name;
private Integer age;
@MyFirst(code = 0,message = "你好呀")
public static String getStr(){
return "abd";
}
public static void main(String[] args) {
//获取加了注解的类对象
Class<TestAnnotation> aClass = TestAnnotation.class;
//获取自定义注解的类对象
Class<MyFirst> myFirstClass = MyFirst.class;
//获取方法列表
Method[] methods = aClass.getMethods();
for (Method f :methods) {
System.out.println(f.getName());
//判断是否存在自定义注解
if(f.isAnnotationPresent(myFirstClass)){
System.out.println("我逮到你了");
//如果存在,获取方法上的 自定义注解对象
MyFirst myFirst = (MyFirst)f.getAnnotation(myFirstClass);
System.out.println(""+myFirst.code()+myFirst.isNo()+myFirst.message());
}
}
}
}

3、从spring上下文中测试自定义注解#

package com.leyou.annotation;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
/**
* <p>DESC: </p>
* <p>DATE: 2019/5/24</p>
* <p>VERSION:1.0.0</p>
* <p>@AUTHOR: ZhengYong</p>
*/
@Component
public class AnnotationContext implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>{
private ApplicationContext applicationContext;
/**
* 获取spring 上下文对象
* ApplicationContextAware 接口实现的方法
* @param applicationContext
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 监听刷新事件
* ApplicationListener 接口实现的方法
* @param contextRefreshedEvent
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//自定义注解的类对象
Class<MyFirst> myFirstClass = MyFirst.class;
//从容器中获取 加了自定义注解的bean对象
Map<String, Object> annotation = applicationContext.getBeansWithAnnotation(myFirstClass);
//遍历集合
Set<Map.Entry<String, Object>> entrySet = annotation.entrySet();
for(Map.Entry<String, Object> m :entrySet){
//获取bean的名字
m.getKey();
//获取bean的类对象
Class<? extends Object> aClass = m.getValue().getClass();
System.out.println("bean名字:"+m.getKey());
System.out.println("类名:"+aClass.getName());
//获取注解实例
MyFirst first = AnnotationUtils.findAnnotation(aClass, myFirstClass);
//循环当前类的方法
Method[] methods = aClass.getMethods();
for(Method method :methods){
boolean present = method.isAnnotationPresent(myFirstClass);
if(present){
MyFirst annotation1 = method.getAnnotation(myFirstClass);
System.out.println("方式一"+annotation1.code()+annotation1.message());
}
//方式二
MyFirst annotation2 = AnnotationUtils.findAnnotation(method, myFirstClass);
if(annotation2 != null){
System.out.println("方式二"+annotation2.code()+annotation2.message());
}
}
}
}
}

4、使用aop自定义注解#

1、记录日志

2、拦截器记录参数

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Spring AOP原理与自定义注解详解
https://onecodemaker.cn/posts/java核心-07-aop与注解/
作者
糖糖IT
发布于
2026-01-24
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录