介绍
Aspect Oriented Programming,即面向切面编程
OOP是面向对象编程,封装,多态,继承
AOP是一种新的编程模式,把系统看做多个对象的交互。AOP把系统分解为不同的关注点,称之为切面
实例
一个业务组件BookService,有创建,更新,删除几个操作,还需要安全检查,日志记录和事物处理
public class BookService {
public void createBook(Book book) {
securityCheck();
Transaction tx = startTransaction();
try {
// 核心业务逻辑
tx.commit();
} catch (RuntimeException e) {
tx.rollback();
throw e;
}
log("created book: " + book);
}
}
对于安全检查,日志,事务代码等会重复出目前每个业务方法中,OOP很难将这些四处分散的代码模块化
可以使用代理模式,将某个功能例如权限检查放入proxy中
public class SecurityCheckBookService implements BookService {
private final BookService target;
public SecurityCheckBookService(BookService target) {
this.target = target;
}
public void createBook(Book book) {
securityCheck();
target.createBook(book);
}
public void updateBook(Book book) {
securityCheck();
target.updateBook(book);
}
public void deleteBook(Book book) {
securityCheck();
target.deleteBook(book);
}
private void securityCheck() {
...
}
}
这种方式比较麻烦,要先抽取接口,针对每个方法实现proxy
将权限检查视作一种切面。日志,事物也视为切面,再以某种自动化的形式,将切面植入核心逻辑,实现proxy
- 核心逻辑,BookService
- 切面逻辑
- 权限检查Aspect
- 日志Aspect
- 事物Aspect
AOP原理
如何把切面植入到核心逻辑中,如果客户端获得了BookService引用,当调用bookService.createBook时,如何对方法进行拦截,并对拦截后进行安全检查,日志,事务,相当于完成所有业务功能
三种方式:
- 编译期:编译期间吧切面调用编译进入字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器
- 类加载器:在目标类被装载到JVM时,需要一个特殊的类装载器,对目标字节码进行增强
- 运行期:目标对象和切面都是普通的java类,通过JVM动态代理或第三方库实现运行期动态植入
Sprin的aop就是基于JVM的动态代理,由于JVM的动态代理必须实现接口,如果一个普通得类没有业务接口,就需要通过GCLIB或JAVAASSIST第三方库实现
AOP本质上就是一个动态代理,把一些常用的功能从每个业务方法中抽离出来
AOP对于解决特定问题,例如事务管理很管用,由于分散到各处的事务代码几乎是完全一样的,并且他们需要的参数(JDBC的Connenction)也是固定的,另一些特定问题,例如日志打印,常常需要捕获局部变量,但如果使用AOP实现日志,只能输出特定格式的日志,AOP必须适合特定的场景
概念
- Aspect:切面 一个横跨多个核心逻辑功能,系统关注点
- Joinpoint:连接点 定义在应用程序流程在何处的切面执行
- Pointcut:切入点 一组连接的集合点
- Advice:增强 特定连接点上执行的动作
- Introduction:引介 一个已有的java对象动态的增加新的接口
- weaving: 织如 将切面整合到程序的执行流程中
- Interceptor:拦截器 实现增强方式
- TargetObject:目标对象 真正执行业务的核心逻辑对象
- AOP Proxy:AOP代理 客户端持有的增强后的对象引用
装配
准备给UserService的每个业务执行前添加日志,MailService每个业务执行前后添加日志,spring中,Maven引入
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
定义日志切面
@Aspect
@Component
public class LoggingAspect {
// 在执行UserService的每个方法前执行:
@Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
public void doAccessCheck() {
System.err.println("[Before] do access check...");
}
// 在执行MailService的每个方法前后执行:
@Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")
public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
System.err.println("[Around] start " + pjp.getSignature());
Object retVal = pjp.proceed();
System.err.println("[Around] done " + pjp.getSignature());
return retVal;
}
}
doAccessCheck方法,定义了一个before朱姐,后面的字符串告知Aspectj在何处执行该方法,执行UserService每个public方法前执行doAccessCheck代码
doLogging定义了around注解,决定是否执行目标方法,因此在内部先打印日志,再调用函数,最后打印日之后返回结果
LoggingAspect类的声明处,除了用component表明本身是一个beab以外,再加上aspect注解,表明他的before标注的函数需要注入到UserService每个public方法执行前,around表明需要注入到mailservice每个public执行前后
再给配置类加上EnableAspectJAutoProxy
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {
...
}
spring的IOC容器看到该注解,会自动查找带有aspect的bean,根据每个函数的before,around注解把aop注入到特定的bean中
[Before] do access check...
[Around] start void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User)
Welcome, test!
[Around] done void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User)
[Before] do access check...
[Around] start void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)
Hi, Bob! You are logged in at 2020-02-14T23:13:52.167996+08:00[Asia/Shanghai]
[Around] done void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)
AOP的原理就是编写一个子类,并持有原始实例的引用
public UserServiceAopProxy extends UserService {
private UserService target;
private LoggingAspect aspect;
public UserServiceAopProxy(UserService target, LoggingAspect aspect) {
this.target = target;
this.aspect = aspect;
}
public User login(String email, String password) {
// 先执行Aspect的代码:
aspect.doAccessCheck();
// 再执行UserService的逻辑:
return target.login(email, password);
}
public User register(String email, String password, String name) {
aspect.doAccessCheck();
return target.register(email, password, name);
}
...
}
spring容器启动时为我们家自动创建aspect的子类,取代了原始的userservice,打印从spring容器获取的userservice类型,类似UserService
1f44e01c,是spring使用GCLib动态创建的子类,对调用方来说没有区别
使用步骤
虽然spring容器实现sop较为复杂,我们使用aop却超级简单,分为三步
- 定义执行方法,在方法上通过AspectJ注解告知spring应该在何处调用该方法
- 标记@Component和@Aspect
- 在@Configuration类上标注@EnableAspectAutoProxy
拦截器类型
- @Before:拦截器先执行拦截代码,再执行目标代码,拦截器异常,目标代码不运行
- @After:拦截器先执行目标代码,再执行拦截器代码,无论目标代码是否异常,都将执行拦截器代码
- @AfterReturning: 和@After不同的是,只有当目标代码返回正常时,才执行拦截器代码
- @AfterThrowning: 和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码
- @Around:能完全控制目标代码是否执行
注解装配
一个SecurityAspect
@Aspect
@Component
public class SecurityAspect {
@Before("execution(public * com.itranswarp.learnjava.service.*.*(..))")
public void check() {
if (SecurityContext.getCurrentUser() == null) {
throw new RuntimeException("check failed");
}
}
}
能实现无差别覆盖,某个包下面的所有bean的所有方法都会被这个check函数拦截
@Around("execution(public * update*(..))")
public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
// 对update开头的方法切换数据源:
String old = setCurrentDataSource("master");
Object retVal = pjp.proceed();
restoreCurrentDataSource(old);
return retVal;
}
误伤范围更大,由于从方法前缀区分是否是数据库操作是超级不可取的
在使用AOP时,虽然spring容器可以把指定方法通过AOP规则装配到指定bean的指定函数前后,但是自动装配由于不恰当的范围容易导致以外的结果,许多不需要的AOP代理也被自动代理了,后续新增的bean由于不清楚规则,容易被自动装配
最好被装配的bean能够知道自己被安排了,spring提供的Transactional就是一个超级好的例子,如果我们希望在一个数据库事务中被调用,就标注Transactional
@Component
public class UserService {
// 有事务:
@Transactional
public User createUser(String name) {
...
}
// 无事务:
public boolean isValidName(String name) {
...
}
// 有事务:
@Transactional
public void updateUser(User user) {
...
}
}
或者直接在类上注解,表明所有public都被安排了
@Component
@Transactional
public class UserService {
...
}
定义一个性能监控的注解
@Target(METHOD)
@Retention(RUNTIME)
public @interface MetricTime {
String value();
}
在需要被监控的地方标识该注解
@Component
public class UserService {
// 监控register()方法性能:
@MetricTime("register")
public User register(String email, String password, String name) {
...
}
...
}
定义MetricAspect
@Aspect
@Component
public class MetricAspect {
@Around("@annotation(metricTime)")
public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
String name = metricTime.value();
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long t = System.currentTimeMillis() - start;
// 写入日志或发送至JMX:
System.err.println("[Metrics] " + name + ": " + t + "ms");
}
}
}
metric函数标注了Around表明符合目标方法是带有MetricTime注解的函数,由于metric()方法参数类型是MetricTime(注意参数名是metricTime不是MetricTime),我们通过它获取性能监控的名称。
有了@MetricTime注解,再配合MetricAspect,任何bean只要函数标注了MetricTime注解,就可以实现自动监控
Welcome, Bob!
[Metrics] register: 16ms
避坑
无论是使用Aspect语法,还是Annotation,使用AOP,实际上就是Spring自动为我们创建一个Proxy,使得调用方无感知的调用指定函数,但运行期间动态织入其他逻辑,本质上是一个代理模式
使用GClib来实现运行期间动态创建proxy
定义一个UserService的Bean
@Component
public class UserService {
// 成员变量:
public final ZoneId zoneId = ZoneId.systemDefault();
// 构造方法:
public UserService() {
System.out.println("UserService(): init...");
System.out.println("UserService(): zoneId = " + this.zoneId);
}
// public方法:
public ZoneId getZoneId() {
return zoneId;
}
// public final方法:
public final ZoneId getFinalZoneId() {
return zoneId;
}
}
MailService,注入Userservice
@Component
public class MailService {
@Autowired
UserService userService;
public String sendMail() {
ZoneId zoneId = userService.zoneId;
String dt = ZonedDateTime.now(zoneId).toString();
return "Hello, it is " + dt;
}
}
@Configuration
@ComponentScan
public class AppConfig {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MailService mailService = context.getBean(MailService.class);
System.out.println(mailService.sendMail());
}
}
UserService(): init...
UserService(): zoneId = Asia/Shanghai
Hello, it is 2020-04-12T10:23:22.917721+08:00[Asia/Shanghai]
输出正常
给UserService加上AOP支持,添加一个简单的LoggingAspect
@Aspect
@Component
public class LoggingAspect {
@Before("execution(public * com..*.UserService.*(..))")
public void doAccessCheck() {
System.err.println("[Before] do access check...");
}
}
并在AppConfig加上EnableAspectJAutoProxy,会得到空指针异常
Exception in thread "main" java.lang.NullPointerException: zone
at java.base/java.util.Objects.requireNonNull(Objects.java:246)
at java.base/java.time.Clock.system(Clock.java:203)
at java.base/java.time.ZonedDateTime.now(ZonedDateTime.java:216)
at com.itranswarp.learnjava.service.MailService.sendMail(MailService.java:19)
at com.itranswarp.learnjava.AppConfig.main(AppConfig.java:21)
跟踪到是在 ZoneId zoneId = userService.zoneId;为null,但其是由final定义
缘由定位
- 正常创建一个UserService原始实例,通过反射调用构造函数实现,行为和预期一致
- 通过GCLIB创建UserService子类,引用原始的LoggingAspect
public UserService$$EnhancerBySpringCGLIB extends UserService {
UserService target;
LoggingAspect aspect;
public UserService$$EnhancerBySpringCGLIB() {
}
public ZoneId getZoneId() {
aspect.doAccessCheck();
return target.getZoneId();
}
}
spring创建的AOP代理,类名总是类似UserService
1c76af9d,为了让调用方获得UserService引用,必须继承自UserService,代理类会覆盖所有public和protect函数,并在内部将调用委托给原始的UserService实例
这时出现了两个UserService实例,一个是代码中定义的原始实例
UserService original = new UserService();
一个是UserService$$EnhancerBySpringCGLIB
UserService$$EnhancerBySpringCGLIB proxy = new UserService$$EnhancerBySpringCGLIB();
proxy.target = original;
proxy.aspect = ...
当启用了AOP,从ApplicationContext获取到的UserService实例是proxy,注入到mailservice的userservice也是代理,最终proxy实例的成员变量,也就是继承userservice的zoneid是null
其中,
public class UserService {
public final ZoneId zoneId = ZoneId.systemDefault();
...
}
在UserService$$EnhancerBySpringCGLIB中,并未执行,由于没必要初始化代理的成员变量,由于proxy的目的是代理方法,编译器实际的代码
public class UserService {
public final ZoneId zoneId;
public UserService() {
super(); // 构造方法的第一行代码总是调用super()
zoneId = ZoneId.systemDefault(); // 继续初始化成员变量
}
}
不过,spring通过GCLib创建的UserService$$EnhancerBySpringCGLIB代理类的构造函数中,并未调用super,因此从父类继承的成员变量,包括final成员变量,都没有初始化
疑问:java规定任何类的构造函数,第一行必须调用super,没有编译器会自动加上,但是GCLib构造的proxy类,是直接生成字节码,并没有源码到编译这一步骤,直接构造字节码,在一个类的构造函数中就不必定非要调用super
Spring通过GCLib创建的代理类,不会初始化代理类自身继承的任何成员变量,包括final类型的成员变量
修复
只需要把直接访问的字段,改为通过函数访问
@Component
public class MailService {
@Autowired
UserService userService;
public String sendMail() {
// 不要直接访问UserService的字段:
ZoneId zoneId = userService.getZoneId();
...
}
}
无论注入的UserService是原始实例还是代理实例,getZoneId()都能正常工作,由于代理类会覆写getZoneId()方法,并将其委托给原始实例:
public UserService$$EnhancerBySpringCGLIB extends UserService {
UserService target = ...
...
public ZoneId getZoneId() {
return target.getZoneId();
}
}
注意到我们还给UserService添加了一个public+final的方法:
@Component
public class UserService {
...
public final ZoneId getFinalZoneId() {
return zoneId;
}
}
如果在MailService中,调用的不是getZoneId(),而是getFinalZoneId(),又会出现NullPointerException,这是由于,代理类无法覆写final方法(这一点绕不过JVM的ClassLoader检查),该方法返回的是代理类的zoneId字段,即null。
实际上,如果我们加上日志,Spring在启动时会打印一个警告:
10:43:09.929 [main] DEBUG org.springframework.aop.framework.CglibAopProxy - Final method [public final java.time.ZoneId xxx.UserService.getFinalZoneId()] cannot get proxied via CGLIB: Calls to this method will NOT be routed to the target instance and might lead to NPEs against uninitialized fields in the proxy instance.
上面的日志大意就是,由于被代理的UserService有一个final方法getFinalZoneId(),这会导致其他Bean如果调用此方法,无法将其代理到真正的原始实例,从而可能发生NPE异常。
因此,正确使用AOP
- 访问被注入的bean时,总是调用方法而非直接访问字段
- 编写bean时,如果可能会被代理,就不要编写public final函数

















暂无评论内容