1. AOP
面向切面编程,Spring的两大核心概念之一,面试必问。
1.1 如何理解AOP
理解AOP,就要先理解面向切面编程的思想
看图:
说明:
- 圆代表应用程序的完整功能
- 如果想要对原有的功能做增强,也就新增一些逻辑,那么势必会改变原有代码,表现在图上,就代表圆就要被改变,也就是圆就不是圆了,破坏了整体的结构
- 这时候 引入了一条线,这条线就是添加的新功能
- 和圆相交的面,就是原有的功能,也就是在不改变圆结构的情况下,对原有的功能做了增强
- 相交的面 叫做切面
- 这就是面向切面编程思想的来源
在不改变原有功能的基础上,对功能进行增强
1.2 对比OOP思想和AOP思想
OOP:面向对象编程
比如一个业务组件BookService
,它有几个业务方法:
- createBook:添加新的Book;
- updateBook:修改Book;
- deleteBook:删除Book。
每一个业务,都需要进行参数检查
,日志记录
,事务处理
等操作。
代码类似这样:
public class BookService {
public void createBook(Book book) {
//检查参数
checkParam(book);
Transaction tx = startTransaction();
try {
// 核心业务逻辑
tx.commit();
} catch (RuntimeException e) {
tx.rollback();
throw e;
}
log.info("created book: " + book);
}
public void updateBook(Book book) {
checkParam(book);
Transaction tx = startTransaction();
try {
// 核心业务逻辑
tx.commit();
} catch (RuntimeException e) {
tx.rollback();
throw e;
}
log.info("update book: " + book);
}
public void deleteBook(Book book) {
checkParam(book);
Transaction tx = startTransaction();
try {
// 核心业务逻辑
tx.commit();
} catch (RuntimeException e) {
tx.rollback();
throw e;
}
log.info("delete book: " + book);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
对于参数检查
,日志记录
,事务处理
等操作,会频繁的,重复的出现在各个方法中。
使用OOP的思想,很难将这些代码模块化。
仔细观察以上的代码,我们发现BookService不关心参数检查,日志记录,事务处理这些,它关心的自身的核心业务逻辑
那么如何让BookService只关心自己的核心业务逻辑呢?
答案就是AOP,通过Proxy的方式,将重复的公共的代码抽离出去,动态的织入BookService中,而不改变原有的Service中的代码结构。
2. AOP原理
要实现上述的功能,就需要在执行Service方法的时候,对调用的方法进行拦截,并在拦截前后进行参数检查、日志、事务等处理。
在Java平台上,对于AOP的织入,有3种方式:
- 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
- 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
- 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。
Spring的AOP实现就是基于JVM的动态代理
AOP本质上就是动态代理,动态代理有两种:
- JDK动态代理,Spring的AOP的默认实现,要求必须实现接口
- CGLIB动态代理,Spring的AOP的可选配置,类和接口都支持
2.1 JDK动态代理示例
package com.mszlu.proxy;
import com.mszlu.service.UserService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JDKProxy {
public Object createBookServiceProxy(Class clazz){
ClassLoader cl = clazz.getClassLoader();
Class[] classes = clazz.getInterfaces();
InvocationHandler ih = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("检查参数");
//jdk9以上版本
Object ret=method.invoke(clazz.getDeclaredConstructor().newInstance(),args);
System.out.println("事务处理");
System.out.println("日志记录");
return ret;
}
};
Object o = Proxy.newProxyInstance(cl,classes,ih);
return o;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.mszlu.service;
public interface BookService {
void createBook();
}
package com.mszlu.service;
public class BookServiceImpl implements BookService {
@Override
public void createBook() {
System.out.println("create book...");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class App {
public static void main(String[] args) {
//jdk动态代理
BookService bookService = new BookServiceImpl();
BookService jdkProxy = (BookService) new JDKProxy().createBookServiceProxy(bookService.getClass());
jdkProxy.createBook();
}
2
3
4
5
6
7
8
3. AOP概念
- Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
- Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
- Pointcut:切入点,即一组连接点的集合;
- Advice:通知,指特定连接点上执行的动作;
- Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
- Weaving:织入,指将切面整合到程序的执行流程中;
- Interceptor:拦截器,是一种实现增强的方式;
- Target Object:目标对象,即真正执行业务的核心逻辑对象;
- AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。
4. 入门案例
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
2
3
4
5
上述依赖会自动引入AspectJ,使用AspectJ实现AOP比较方便。
4.1 需求
场景需求:
为之前实现的注册用户服务,添加日志功能。
要求:
- 打印执行的类和方法
- 打印执行方法的参数
- 计算方法执行的时间
- 如果有异常,记录异常
4.2 实现
步骤:
定义切面类
@Aspect @Component public class LogAspect { }
1
2
3
4
5定义切点
@Aspect @Component public class LogAspect { // 定义切点 在执行UserService的每个方法前执行: @Pointcut("execution(public * com.mszlu.service.UserService.*(..))") public void pt() {} }
1
2
3
4
5
6
7
8
9定义通知
@Aspect @Component public class LogAspect { // 定义切点 在执行UserService的每个方法前执行: @PointCut("execution(public * com.mszlu.service.UserService.*(..))") public void pt() {} //定义通知 @Around("pt()") public Object doLogging(ProceedingJoinPoint pjp) throws Throwable{ //方法调用 Object ret = pjp.proceed(); return ret; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15需求实现
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency>
1
2
3
4
5<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
1
2
3
4
5package com.mszlu.aop; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component @Slf4j public class LogAspect { // 定义切点 在执行UserService的每个方法前执行: @Pointcut("execution(public * com.mszlu.service.UserService.*(..))") public void pt() {} //定义通知 @Around("pt()") public Object doLogging(ProceedingJoinPoint pjp) throws Throwable{ try { log.info("----------------log start--------------------"); //1. 打印执行的类和方法 Signature signature = pjp.getSignature(); String methodName = signature.getName(); String interfaceName = signature.getDeclaringTypeName(); log.info("接口名称:{}",interfaceName); log.info("方法名称:{}",methodName); //2. 打印执行方法的参数 log.info("方法参数:{}", JSON.toJSONString(pjp.getArgs())); //3. 计算方法执行的时间 long startTime = System.currentTimeMillis(); //方法调用 Object ret = pjp.proceed(); long endTime = System.currentTimeMillis(); log.info("方法执行时间:{}ms", endTime-startTime); log.info("----------------log end--------------------"); return ret; }catch (Exception e){ //4. 如果有异常,记录异常 log.error("异常信息",e); throw e; } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48启动
package com.mszlu; import com.mszlu.config.SpringConfig; import com.mszlu.proxy.JDKProxy; import com.mszlu.service.BookService; import com.mszlu.service.BookServiceImpl; import com.mszlu.service.UserService; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class App { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); UserService bean = context.getBean(UserService.class); bean.registerUser("ddd@mszlu.com","123456","ddd"); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package com.mszlu.config; import com.mszlu.domain.MyImportBeanDefinitionRegistrar; import com.mszlu.domain.MyImportSelector; import com.mszlu.domain.User; import com.mszlu.domain.UserRegistrar; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.Import; @Configuration @ComponentScan("com.mszlu") //注意开启AOP的支持,并且代理设置为cglib动态代理,因为UserService没有接口 @EnableAspectJAutoProxy(proxyTargetClass = true) public class SpringConfig { }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
5. 通知类型
AOP通知共分为5种类型
前置通知
后置通知
环绕通知(重点)
返回后通知(了解)
抛出异常后通知(了解)
5.1 前置通知
- 名称:@Before
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
- 范例:
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
2
3
4
- 相关属性:value(默认):切点标识
5.2 后置通知
名称:@After
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
范例:
@After("pt()") public void after() { System.out.println("after advice ..."); }
1
2
3
4相关属性:value(默认):切点标识
5.3 环绕通知
名称:@Around(重点,常用)
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行
范例:
@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..."); Object ret = pjp.proceed(); System.out.println("around after advice ..."); return ret; }
1
2
3
4
5
6
7
@Around注意事项
环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,必须设定为Object类型
原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象
5.4 返回后通知
名称:@AfterReturning(了解)
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行
范例:
@AfterReturning("pt()") public void afterReturning() { System.out.println("afterReturning advice ..."); }
1
2
3
4
5
5.5 抛出异常后通知
名称:@AfterThrowing(了解)
类型:方法注解
位置:通知方法定义上方
作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
范例:
@AfterThrowing("pt()") public void afterThrowing() { System.out.println("afterThrowing advice ..."); }
1
2
3
4
6. 切点表达式
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)
execution(public User com.mszlu.service.UserService.findById(int))
- 动作关键字:描述切入点的行为动作,例如execution表示执行到指定切入点
- 访问修饰符:public,private等,可以省略
- 返回值
- 包名
- 类/接口名
- 方法名
- 参数
- 异常名:方法定义中抛出指定异常,可以省略
可以使用通配符描述切入点,快速描述
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现execution(public * com.mszlu.*.UserService.find*(*))
1匹配com.mszlu包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写execution(public User com..UserService.findById(..))
1匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
+
:专用于匹配子类类型
execution(* *..*Service+.*(..))
6.1 execution
execution在实际工作中,很少被使用,因为匹配的打击面非常大或者非常小,不能灵活应用
execution(public * com.mszlu.service.*.*(..))
基本能实现无差别全覆盖,即某个包下面的所有Bean的所有方法都会被拦截。
execution(public * update*(..))
从方法的前缀来区分,这种误伤的概率非常大,你不可能要求所有的程序员都按照这种书写习惯来。
使用AOP,可以将指定的方法装配到指定Bean的指定方法前后,如果自动装配时,因为不恰当的范围,容易导致意想不到的结果,特别是新入职的开发人员,不懂得规则的时候,非常容易出现生产事故
6.2 annotation
我们使用AOP的时候,常常使用annotation的形式
场景:
比如我们实现一个性能监控的需求,使用AOP实现
定义注解
package com.mszlu.aop; import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MsMetric { String value() default ""; }
1
2
3
4
5
6
7
8
9
10
11在需要监控的方法上,加上注解
@MsMetric public void registerUser(String mail,String password,String nickname){ //... }
1
2
3
4实现性能监控AOP,切点使用annotation
package com.mszlu.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component @Slf4j public class MetricAspect { // 定义切点 注解所在的方法 即是切点 @Pointcut("@annotation(MsMetric)") public void pt() {} //定义通知 @Around("pt()") public Object doLogging(ProceedingJoinPoint pjp) throws Throwable{ try { log.info("----------------metric start--------------------"); //计算方法执行的时间 long startTime = System.currentTimeMillis(); //方法调用 Object ret = pjp.proceed(); long endTime = System.currentTimeMillis(); log.info("方法执行时间:{}ms", endTime-startTime); log.info("----------------metric end--------------------"); return ret; }catch (Exception e){ log.error("异常信息",e); throw e; } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
7. AOP使用注意事项
- 访问被注入的Bean时,总是调用方法而非直接访问字段。
- 编写Bean时,如果可能会被代理,就不要编写
public final
方法。
示例:
在MailService:
package com.mszlu.service;
import com.mszlu.aop.MsMetric;
import com.mszlu.domain.User;
import org.springframework.stereotype.Component;
@Component
public class MailService {
public final User aaa = new User();
public User getUser(){
return aaa;
}
public final User getFinalUser(){
return aaa;
}
@MsMetric
public boolean sendRegisterMail(User user){
System.out.println(String.format("%s 正在注册码神之路,您可以点击以下的链接完成注册 %s,如果不是你注册的,请忽略本邮件",user.getNickname(),"http://www.mszlu.com"));
return true;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
UserService中:
public void registerUser(String mail,String password,String nickname){
users.forEach(user -> {
if(user.getMail().equalsIgnoreCase(mail)){
throw new RuntimeException("已经注册");
}
});
//输出null
System.out.println(mailService.aaa);
//输出null
System.out.println(mailService.getFinalUser());
//正常输出
System.out.println(mailService.getUser());
User user = new User(null,mail,password,nickname);
mailService.sendRegisterMail(user);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15