面试类 - Spring基础(二)
AOP
19.说说什么是 AOP?
AOP,也就是 Aspect-oriented Programming,译为面向切面编程。
简单点说,就是把一些业务逻辑中的相同代码抽取到一个独立的模块中,让业务逻辑更加清爽。
横向抽取
举个例子,假如我们现在需要在业务代码开始前进行参数校验,在结束后打印日志,该怎么办呢?
我们可以把日志记录
和数据校验
这两个功能抽取出来,形成一个切面,然后在业务代码中引入这个切面,这样就可以实现业务逻辑和通用逻辑的分离。
AOP应用示例
业务代码不再关心这些通用逻辑,只需要关心自己的业务实现,这样就实现了业务逻辑和通用逻辑的分离。
我们来回顾一下 Java 语言的执行过程:
Java 执行过程
AOP 的核心是动态代理,可以使用 JDK 动态代理来实现,也可以使用 CGLIB 来实现。
AOP 有哪些核心概念?
- 切面(Aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象
- 连接点(Join Point):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中,连接点指的是被拦截到的方法,实际上连接点还可以是字段或者构造方法
- 切点(Pointcut):对连接点进行拦截的定位
- 通知(Advice):指拦截到连接点之后要执行的代码,也可以称作增强
- 目标对象 (Target):代理的目标对象
- 引介(introduction):一种特殊的增强,可以动态地为类添加一些属性和方法
- 织入(Weabing):织入是将增强添加到目标类的具体连接点上的过程。可以分为 3 种类型的织入:
①、编译期织入:切面在目标类编译时被织入。
②、类加载期织入:切面在目标类加载到 JVM 时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。
③、运行期织入:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面。
Spring 采用运行期织入,而 AspectJ 采用编译期织入和类加载器织入。
AOP 有哪些环绕方式?
AOP 一般有 5 种环绕方式:
- 前置通知 (@Before)
- 返回通知 (@AfterReturning)
- 异常通知 (@AfterThrowing)
- 后置通知 (@After)
- 环绕通知 (@Around)
环绕方式
多个切面的情况下,可以通过 @Order
指定先后顺序,数字越小,优先级越高。代码示例如下:
@Aspect
@Component
public class WebLogAspect {
private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
@Pointcut("@annotation(cn.fighter3.spring.aop_demo.WebLog)")
public void webLog() {}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 打印请求相关参数
logger.info("========================================== Start ==========================================");
// 打印请求 url
logger.info("URL : {}", request.getRequestURL().toString());
// 打印 Http method
logger.info("HTTP Method : {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印请求的 IP
logger.info("IP : {}", request.getRemoteAddr());
// 打印请求入参
logger.info("Request Args : {}",new ObjectMapper().writeValueAsString(joinPoint.getArgs()));
}
@After("webLog()")
public void doAfter() throws Throwable {
// 结束后打个分隔线,方便查看
logger.info("=========================================== End ===========================================");
}
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//开始时间
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 打印出参
logger.info("Response Args : {}", new ObjectMapper().writeValueAsString(result));
// 执行耗时
logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
return result;
}
}
总结一下:
AOP,也就是面向切面编程,是一种编程范式,旨在提高代码的模块化。比如说可以将日志记录、事务管理等分离出来,来提高代码的可重用性。
AOP 的核心概念包括切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)和织入(Weaving)等。
① 像日志打印、事务管理等都可以抽离为切面,可以声明在类的方法上。
② 在 Spring AOP 中,连接点总是表示方法的执行。
③ Spring AOP 支持五种类型的通知:前置通知、后置通知、环绕通知、异常通知、最终通知等。
④ 在 AOP 中,切点用于指定我们想要在哪些连接点上执行通知的规则。
⑤ 织入是指将切面应用到目标对象并创建新的代理对象的过程。Spring AOP 默认在运行时通过动态代理方式实现织入。
像 @Transactional
注解,就是一个典型的 AOP 应用,它就是通过 AOP 来实现事务管理的。我们只需要在方法上添加 @Transactional
注解,Spring 就会在方法执行前后添加事务管理的逻辑。
- Java 面试指南(付费)收录的腾讯 Java 后端实习一面原题:说说 AOP 的原理。
- Java 面试指南(付费)收录的小米 25 届日常实习一面原题:说说你对 AOP 和 IoC 的理解。
- Java 面试指南(付费)收录的快手面经同学 7 Java 后端技术一面面试原题:说一下 Spring AOP 的实现原理
20.你平时有用到 AOP 吗?
有,我在技术派实战项目中就有使用,我利用 AOP 打印了接口的入参和出参日志,以及执行时间。
技术派教程
我是这样使用的。
第一步,自定义一个注解作为切点
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MdcDot {
String bizCode() default "";
}
第二步,配置 AOP 切面:
@Aspect
:标识切面@Pointcut
:设置切点,这里以自定义注解为切点@Around
:环绕切点,打印方法签名和执行时间
技术派项目:配置 AOP 切面
第三步,在使用的地方加上自定义注解
技术派项目:使用注解
第四步,当接口被调用时,就可以看到对应的执行日志。
2023-06-16 11:06:13,008 [http-nio-8080-exec-3] INFO |00000000.1686884772947.468581113|101|c.g.p.forum.core.mdc.MdcAspect.handle(MdcAspect.java:47) - 方法执行耗时: com.github.paicoding.forum.web.front.article.rest.ArticleRestControllerrecommend = 47
21.说说 JDK 动态代理和 CGLIB 代理?
Spring 的 AOP 是通过动态代理来实现的,动态代理主要有两种方式:JDK 动态代理和 CGLIB 代理。
①、JDK 动态代理是基于接口的代理方式,它使用 Java 原生的 java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口来创建和管理代理对象。
- 基于 Interface:JDK 动态代理要求目标对象必须实现一个或多个接口。代理对象不是直接继承自目标对象,而是实现了与目标对象相同的接口。
- 使用 InvocationHandler:在调用代理对象的任何方法时,调用都会被转发到一个 InvocationHandler 实例的 invoke 方法。可以在这个 invoke 方法中定义拦截逻辑,比如方法调用前后执行的操作。
- 基于 Proxy:Proxy 利用 InvocationHandler 动态创建一个符合目标类实现的接口实例,生成目标类的代理对象。
②、CGLIB(Code Generation Library)是一个第三方代码生成库,它通过继承方式实现代理,不需要接口,被广泛应用于 Spring AOP 中,用于提供方法拦截操作。
- 基于继承,CGLIB 通过在运行时生成目标对象的子类来创建代理对象,并在子类中覆盖非 final 的方法。因此,它不要求目标对象必须实现接口。
- 基于 ASM,ASM 是一个 Java 字节码操作和分析框架,CGLIB 可以通过 ASM 读取目标类的字节码,然后修改字节码生成新的类。它在运行时动态生成一个被代理类的子类,并在子类中覆盖父类的方法,通过方法拦截技术插入增强代码。
选择 CGLIB 还是 JDK 动态代理?
- 如果目标对象没有实现任何接口,则只能使用 CGLIB 代理。如果目标对象实现了接口,通常首选 JDK 动态代理。
- 虽然 CGLIB 在代理类的生成过程中可能消耗更多资源,但在运行时具有较高的性能。对于性能敏感且代理对象创建频率不高的场景,可以考虑使用 CGLIB。
- JDK 动态代理是 Java 原生支持的,不需要额外引入库。而 CGLIB 需要将 CGLIB 库作为依赖加入项目中。
你会用 JDK 动态代理和 CGLIB 吗?
假设我们有这样一个小场景,客服中转,解决用户问题:
用户向客服提问题
①、JDK 动态代理实现:
JDK动态代理类图
第一步,创建接口
public interface ISolver {
void solve();
}
第二步,实现对应接口
public class Solver implements ISolver {
@Override
public void solve() {
System.out.println("疯狂掉头发解决问题……");
}
}
第三步,动态代理工厂:ProxyFactory,直接用反射方式生成一个目标对象的代理,这里用了一个匿名内部类方式重写 InvocationHandler 方法。
public class ProxyFactory {
// 维护一个目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
// 为目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("请问有什么可以帮到您?");
// 调用目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("问题已经解决啦!");
return null;
}
});
}
}
第五步,客户端:Client,生成一个代理对象实例,通过代理对象调用目标对象方法
public class Client {
public static void main(String[] args) {
//目标对象:程序员
ISolver developer = new Solver();
//代理:客服小姐姐
ISolver csProxy = (ISolver) new ProxyFactory(developer).getProxyInstance();
//目标方法:解决问题
csProxy.solve();
}
}
②、CGLIB 动态代理实现:
CGLIB动态代理类图
第一步:定义目标类(Solver),目标类 Solver 定义了一个 solve 方法,模拟了解决问题的行为。目标类不需要实现任何接口,这与 JDK 动态代理的要求不同。
public class Solver {
public void solve() {
System.out.println("疯狂掉头发解决问题……");
}
}
第二步:动态代理工厂(ProxyFactory),ProxyFactory 类实现了 MethodInterceptor 接口,这是 CGLIB 提供的一个方法拦截接口,用于定义方法的拦截逻辑。
public class ProxyFactory implements MethodInterceptor {
//维护一个目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
//为目标对象生成代理对象
public Object getProxyInstance() {
//工具类
Enhancer en = new Enhancer();
//设置父类
en.setSuperclass(target.getClass());
//设置回调函数
en.setCallback(this);
//创建子类对象代理
return en.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("请问有什么可以帮到您?");
// 执行目标对象的方法
Object returnValue = method.invoke(target, args);
System.out.println("问题已经解决啦!");
return null;
}
}
- ProxyFactory 接收一个 Object 类型的 target,即目标对象的实例。
- 使用 CGLIB 的 Enhancer 类来生成目标类的子类(代理对象)。通过 setSuperclass 设置代理对象的父类为目标对象的类,setCallback 设置方法拦截器为当前对象(this),最后调用 create 方法生成并返回代理对象。
- 重写 MethodInterceptor 接口的 intercept 方法以提供方法拦截逻辑。在目标方法执行前后添加自定义逻辑,然后通过 method.invoke 调用目标对象的方法。
第三步:客户端使用代理,首先创建目标对象(Solver 的实例),然后使用 ProxyFactory 创建该目标对象的代理。通过代理对象调用 solve 方法时,会先执行 intercept 方法中定义的逻辑,然后执行目标方法,最后再执行 intercept 方法中的后续逻辑。
public class Client {
public static void main(String[] args) {
//目标对象:程序员
Solver developer = new Solver();
//代理:客服小姐姐
Solver csProxy = (Solver) new ProxyFactory(developer).getProxyInstance();
//目标方法:解决问题
csProxy.solve();
}
}
- Java 面试指南(付费)收录的帆软同学 3 Java 后端一面的原题:cglib 的原理
- Java 面试指南(付费)收录的腾讯面经同学 22 暑期实习一面面试原题:Spring AOP 实现原理
22.说说 Spring AOP 和 AspectJ AOP 区别?
Spring AOP
Spring AOP 属于运行时增强
,主要具有如下特点:
-
基于动态代理来实现,默认如果使用接口的,用 JDK 提供的动态代理实现,如果是方法则使用 CGLIB 实现
-
Spring AOP 需要依赖 IoC 容器来管理,并且只能作用于 Spring 容器,使用纯 Java 代码实现
-
在性能上,由于 Spring AOP 是基于动态代理来实现的,在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,使得 Spring AOP 的性能不如 AspectJ 的那么好。
-
Spring AOP 致力于解决企业级开发中最普遍的 AOP(方法织入)。
AspectJ
AspectJ 是一个易用的功能强大的 AOP 框架,属于编译时增强
, 可以单独使用,也可以整合到其它框架中,是 AOP 编程的完全解决方案。AspectJ 需要用到单独的编译器 ajc。
AspectJ 属于静态织入,通过修改代码来实现,在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,一般有如下几个织入的时机:
-
编译期织入(Compile-time weaving):如类 A 使用 AspectJ 添加了一个属性,类 B 引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。
-
编译后织入(Post-compile weaving):也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。
-
类加载后织入(Load-time weaving):指的是在加载类的时候进行织入,要实现这个时期的织入,有几种常见的方法
整体对比如下:
Spring AOP和AspectJ对比
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
· Manus的开源复刻OpenManus初探