主题
《面渣逆袭》Spring 篇 · 第 3/7 章。原版 PDF(下载 / 打印)
19.说说什么是 AOP?
AOP :面向切面编程。简单说,就是把一些业务逻辑中的相同的代码抽取到一个独立的模块中,让业务逻辑更加清爽。

具体来说,假如我现在要 crud 写一堆业务,可是如何业务代码前后前后进行打印日志和参数的校验呢?
我们可以把日志记录和数据校验可重用的功能模块分离出来,然后在程序的执行的合适的地方动态地植入这些代码并执行。这样就简化了代码的书写。

业务逻辑代码中没有参和通用逻辑的代码,业务模块更简洁,只包含核心业务代码。实现了业务逻辑和通用逻辑的代码分离,便于维护和升级,降低了业务逻辑和通用逻辑的耦合性。
AOP 可以将遍布应用各处的功能分离出来形成可重用的组件。在编译期间、装载期间或运行期间实现在不修改源代码的情况下给程序动态添加功能。从而实现对业务逻辑的隔离,提高代码的模块化能力。

AOP 的核心其实就是动态代理,如果是实现了交叉的话就会使用 JDK 动态代理,否则使用 CGLIB代理,主要应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。
AOP 有哪些核心概念?
切面(Aspect ):类是对物体特征的抽象,切面就是对横切关注点的抽象
连接点(Joinpoint ):被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
切点(Pointcut ):对连接点进行拦截的定位
通知(Advice ):所谓通知指的就是指拦截到连接点之后要执行的代码,也可以称作增强
目标对象(Ta rget ):代理的目标对象
织入(We abing ):织入是将增强添加到目标类的具体连接点上的过程。
编译期织入:切面在目标类编译时被织入类加载期织入:切面在目标类加载到 JVM 时被织入。需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。
运行期织入:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。SpringAOP 就是以这种方式织入切面。
Spring 采用运行期织入,而 AspectJ 采用编译期织入和类加载器织入。
引介(introduction ):引介是一种特殊的增强,可以动态地为类添加一些属性和方法
AOP 有哪些环绕方式?
AOP 一般有5 种环绕方式:
前置通知 (@Before)
返回通知 (@AfterReturning)
异常通知 (@AfterThrowing)
后置通知 (@After)
环绕通知 (@Around)
多个切面的情况下,可以通过 @ Order 指定先后顺序,数字越小,优先级越高。
20.说说你平时有用到 AOP 吗?
PS :这道题老三的同事面试候选人的时候问到了,候选人说了一堆 AOP 原理,同事就势来一句,你能现场写一下 AOP 的应用吗?结果— — 场面一度很尴尬。虽然我对面试写这种百度就能出来的东西持保留意见,但是还是加上了这一问,毕竟招人最后都是要撸代码的。
这里给出一个小例子,SpringBoot 项目中,利用 AOP 打印交叉的入参和出参日志,以及执行时间,还是比较快捷的。
引入依赖:引入 AOP 依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>自定义注解:自定义一个注解作为切点
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})@ Documented
java
public @interface WebLog {
}配置 AOP 切面:
@ Aspect :标识切面@ Pointcut :设置切点,这里以自定义注解为切点,定义切点有很多其它种方式,自定义注解是比较常用的一种。
@ Before :在切点之前织入,打印了一些入参信息@ Around :环绕切点,打印返回参数和交叉执行时间
@ Aspect
@ Component
java
public class WebLogAspect {
private final static Logger logger =
LoggerFactory.getLogger(WebLogAspect.class);/
- 以自定义 @ WebLog 注解为切点
/
java
@Pointcut("@annotation(cn.fighter3.spring.aop_demo.WebLog)")
public void webLog() {}/
- 在切点之前织入*/
java
@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("ClassMethod :{}.{}",
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印请求的 IP
logger.info("IP : {}", request.getRemoteAddr());
// 打印请求入参logger.info("RequestArgs :{}",new
ObjectMapper().writeValueAsString(joinPoint.getArgs()));
}/
- 在切点之后织入
* @throws Throwable*/
java
@After("webLog()")
public void doAfter() throws Throwable {
// 结束后打个分隔线,方便查看logger.info("===========================================End
===========================================");
}/
- 环绕*/
java
@Around("webLog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable
{
//开始时间
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 打印出参logger.info("ResponseArgs :{}",new
ObjectMapper().writeValueAsString(result));
// 执行耗时
logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
return result;
}
}使用:只需要在交叉上加上自定义注解
java
@GetMapping("/hello")
@WebLog(desc = "这是一个欢迎接又")
public String hello(String name){
return "Hello "+name;
}执行结果:可以看到日志打印了入参、出参和执行时间

21.说说 JDK 动态代理和 CGLIB 代理 ?
Spring 的 AOP 是通过动态代理来实现的,动态代理主要有两种方式 JDK 动态代理和 Cglib 动态代理,这两种动态代理的使用和原理有些不同。
JDK 动态代理
Interface:对于 JDK 动态代理,目标类需要实现一个 Interface 。
InvocationHandler:InvocationHandler 是一个交叉,可以通过实现这个交叉,定义横切逻
辑,再通过反射机制(invoke )调用目标类的代码,在次过程,可能包装逻辑,对目标方法进行前置后置处理。
- Proxy:Proxy 利用 InvocationHandler 动态创建一个符合目标类实现的交叉的实例,生成目标
类的代理对象。
CgLib 动态代理
- 使用 JDK 创建代理有一大限制,它只能为交叉创建代理实例,而 CgLib 动态代理就没有这个限
制。
- CgLib 动态代理是使用字节码处理框架ASM,其原理是通过字节码技术为一个类创建子类,并
在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。
- CgLib创建的动态代理对象性能比 JDK 创建的动态代理对象的性能高不少,但是 CGLib 在创
建代理对象时所花费的时间却比 JDK 多得多,所以对于单例的对象,因为无需频繁创建对象,用 CGLib 合适,反之,使用 JDK 方式要更为合适一些。同时,由于 CGLib 由于是采用动态创建子类的方法,对于 final 方法,无法进行代理。
我们来看一个常见的小场景,客服中转,解决用户问题:

JDK 动态代理实现:

交叉
java
public interface ISolver {
void solve();
}目标类: 需要实现对应交叉
java
public class Solver implements ISolver {@ Override
java
public void solve() {
System.out.println("疯狂掉头发解决问题……");
}
}态代理工厂: ProxyFactory ,直接用反射方式生成一个目标对象的代理对象,这里用了一个匿名内部类方式重写 InvocationHandler 方法,实现交叉重写也差不多
java
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
java
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 ,生成一个代理对象实例,通过代理对象调用目标对象方法
java
public class Client {
public static void main(String[] args) {
//目标对象:程序员
ISolver developer = new Solver();
//代理:客服小姐姐
ISolver csProxy = (ISolver) new ProxyFactory(developer).getProxyInstance();
//目标方法:解决问题
csProxy.solve();
}
}Cglib 动态代理实现:

目标类:Solver ,这里目标类不用再实现交叉。
java
public class Solver {
public void solve() {
System.out.println("疯狂掉头发解决问题……");
}
}动态代理工厂:
java
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
java
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;
}
}客户端:Client
java
public class Client {
public static void main(String[] args) {
//目标对象:程序员
Solver developer = new Solver();
//代理:客服小姐姐
Solver csProxy = (Solver) new ProxyFactory(developer).getProxyInstance();
//目标方法:解决问题
csProxy.solve();
}
}22.说说 Spring AOP 和 AspectJ AOP 区别?
SpringAOP
SpringAOP 属于运行时增强,主要具有如下特点:
- 基于动态代理来实现,默认如果使用交叉的,用 JDK 提供的动态代理实现,如果是方法则使用
CGLIB 实现
SpringAOP 需要依赖 IOC 容器来管理,并且只能作用于 Spring 容器,使用纯 Java 代码实现
在性能上,由于 SpringAOP 是基于动态代理来实现的,在容器启动时需要生成代理实例,在方
法调用上也会增加栈的深度,使得 SpringAOP 的性能不如 AspectJ 的那么好。
- SpringAOP 致力于解决企业级开发中最普遍的 AOP( 方法织入) 。
AspectJ
AspectJ 是一个易用的功能强大的 AOP 框架,属于编译时增强, 可以单独使用,也可以整合到其它框架中,是 AOP 编程的完全解决方案。AspectJ 需要用到单独的编译器 ajc 。
AspectJ 属于静态织入,通过修改代码来实现,在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,一般有如下几个织入的时机:
- 编译期织入(Compile-timeweaving ):如类 A 使用 AspectJ 添加了一个属性,类 B 引用了
它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B 。
2.编译后织入(Post-compile weaving):也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。
- 类加载后织入(Load-timeweaving ):指的是在加载类的时候进行织入,要实现这个时期的织
入,有几种常见的方法整体对比如下:
