目录
一、什么是Spring AOP
AOP
(
Aspect Oriented Programming
):面向切面编程,它是一种思想,
它是对某一类事情的集中处
理
。比如用户登录权限的效验,没学
AOP
之前,我们所有需要判断用户登录的页面(中的方法),都要各自实现或调用用户验证的方法,然而有了 AOP
之后,我们只需要在某一处配置一下,所有需要判断用户登录页面(中的方法)就全部可以实现用户登录验证了,不再需要每个方法中都写相同的用户登录验证了。
AOP 是一种思想,而 Spring AOP 是一个框架,提供了一种对 AOP 思想的实现,它们的关系和 IoC与 DI 类似。
二、Spring AOP的组成
1.切面(Aspect)
切面(
Aspect
)由切点(
Pointcut
)和通知(
Advice
)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
2.连接点(Join Point)
应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
3.切点(Pointcut)
Pointcut
是匹配
Join Point
的谓词。Pointcut 的作用就是提供一组规则(使用
AspectJ pointcut expression language
来描述)来匹配
Join Point,给满足规则的
Join Point
添加
Advice
。切点相当于保存了众多连接点的集合。
4.通知(Advice)
切面也是有目标的
——
它必须完成的工作。在
AOP
术语中,
切面的工作被称之为通知
。
通知:定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的问题。Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
前置通知
使用
@Before
:通知方法会在目标方法调用之前执行。
后置通知
使用
@After
:通知方法会在目标方法返回或者抛出异常后调用。
返回之后通知
使用
@AfterReturning
:通知方法会在目标方法返回后调用。
抛异常后通知
使用
@AfterThrowing
:通知方法会在目标方法抛出异常后调用。
环绕通知
使用
@Around
:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行 自定义的行为。
切点相当于要增强的方法。
三、Spring AOP 的实现
1.定义切面和切点
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect // 表明此类为一个切面
@Component
public class UserAspect {
// 定义切点,这里使用 AspectJ 表达式语法
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){ }
}
其中
pointcut
方法为空方法,它不需要有方法体,此方法名就是起到一个
“
标识
”
的作用,标识下面的通知方法具体指的是哪个切点(因为切点可能有很多个)。
2.定义相关通知
四、Spring AOP 的实现原理
Spring AOP
是构建在
动态代理
基础上,因此
Spring
对
AOP
的支持局限于方法级别的拦截
。
Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用 AOP 会基于 JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类。
1.织入:代理的生成时机
织入
是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
编译期:
切面在目标类编译时被织入。这种方式需要特殊的编译器。
AspectJ
的织入编译器就是以
这种方式织入切面的。
类加载器:
切面在目标类加载到
JVM
时被织入。这种方式需要特殊的类加载器(
ClassLoader
)
,
它
可以在目标类被引入应用之前增强该目标类的字节码。
AspectJ5
的加载时织入(
load-time
weaving. LTW
)就支持以这种方式织入切面。
运行期:
切面在应用运行的某一时刻被织入。一般情况下,在织入切面时,
AOP
容器会为目标对象
动态创建一个代理对象。
SpringAOP就是以这种方式织入切面的
。
2.动态代理
此种实现在设计模式上称为动态代理模式,在实现的技术手段上,都是在
class
代码运行期
,动态的织入字节码。
我们学习
Spring
框架中的
AOP
,主要基于两种方式:
JDK
及
CGLIB
的方式。这两种方式的代理目标都是被代理类中的方法,在运行期,动态的织入字节码生成代理类。
CGLIB
是
Java
中的动态代理框架,主要作用就是根据目标类和方法,动态生成代理类。Java中的动态代理框架,几乎都是依赖字节码框架(如
ASM
,
Javassist
等)实现的。字节码框架是直接操作 class
字节码的框架。可以加载已有的
class
字节码文件信息,修改部分信息,或动态生成一个 class
。
JDK动态代理模式
这里的对象必须实现一个接口
代码展示:
接口:
public interface Executable {
void execute();
}
被代理的类:
public class SayHelloCommand implements Executable {
@Override
public void execute() {
System.out.println("SayHelloCommand.execute(): 你好世界");
}
}
代理对象的生成:
@Slf4j
@Configuration
public class AppConfig {
static public class ExecutableProxy implements InvocationHandler {
private final SayHelloCommand command = new SayHelloCommand();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("调用方法前: {}", method);
Object returnValue = method.invoke(command, args);
log.info("调用方法后: {}", returnValue);
return returnValue;
}
}
@Bean
public Executable executable() {
return (Executable) Proxy.newProxyInstance(
Executable.class.getClassLoader(),
new Class[] { Executable.class },
new ExecutableProxy()
);
}
}
程序主界面:
@Component
public class CommandLine implements CommandLineRunner {
private final Executable executable;
@Autowired
public CommandLine(Executable executable) {
// 实际上注入的已经是被代理过的对象了
this.executable = executable;
}
@Override
public void run(String... args) throws Exception {
// SpringBoot 启动的时候会去调用
System.out.println("CommandLine.run() 被调用了");
executable.execute();
}
}
网上找了一份比较接近的:
JDK
实现时,先通过实现
InvocationHandler
接口创建方法调用处理器,再通过
Proxy
来创建代理类。
3.JDK 和 CGLIB 的区别
1. JDK
实现,要求被代理类必须实现接口,之后是通过
InvocationHandler
及
Proxy
,在运行时动态的在内存中生成了代理类对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实现的方式),只是该代理类是在运行期时,动态的织入统一的业务逻辑字节码来完成。
2. CGLIB
实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类对象。
五、MyBatis的动态代理
这里只是演示一个大体的流程:
定义一个注解类:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQL {
String value();
}
接口:
public interface UserDao {
@SQL("select uid, username, password from users where uid = ?")
Map<String, Object> selectOneByUid(int uid);
@SQL("select uid, username, password from users order by uid limit 1 offset ?")
Map<String, Object> selectOneByOffset(int offset);
}
注册代理对象:
@Configuration
public class AppConfig {
public static class DaoProxy implements InvocationHandler {
private final DataSource dataSource;
public DaoProxy() {
MysqlDataSource m = new MysqlDataSource();
m.setUrl("jdbc:mysql://127.0.0.1:3306/java?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai");
m.setUser("root");
m.setPassword("123456");
dataSource = m;
}
@Override
@SneakyThrows
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
SQL annotation = method.getAnnotation(SQL.class);
String sql = annotation.value();
System.out.println(sql);
// 可以真实的去做查询,而不需要有被代理对象
try (Connection c = dataSource.getConnection()) {
try (PreparedStatement ps = c.prepareStatement(sql)) {
// 我们知道参数一定是 int 类型
int uid = (Integer) args[0];
ps.setInt(1, uid);
System.out.println(ps);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return null;
}
LinkedHashMap<String, Object> map = new LinkedHashMap<>();
map.put("uid", rs.getInt("uid"));
map.put("username", rs.getString("username"));
map.put("password", rs.getString("password"));
return map;
}
}
}
}
}
@Bean
public UserDao userDao() {
return (UserDao) Proxy.newProxyInstance(
UserDao.class.getClassLoader(),
new Class[] { UserDao.class },
new DaoProxy()
);
}
}
主界面:
@Component
public class CommandLine implements CommandLineRunner {
private final UserDao userDao;
@Autowired
public CommandLine(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void run(String... args) throws Exception {
Map<String, Object> map = userDao.selectOneByOffset(1);
if (map != null) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
String column = entry.getKey();
Object value = entry.getValue();
System.out.println(column + " = " + value);
}
} else {
System.out.println("null");
}
}
}