这个专栏给大家介绍一下 Java 家族的核心产品 - SSM 框架
JavaEE 进阶专栏Java 语言能走到现在 , 仍然屹立不衰的原因 , 有一部分就是因为 SSM 框架的存在
接下来 , 博主会带大家了解一下 Spring、Spring Boot、Spring MVC、MyBatis 相关知识点
并且带领大家进行环境的配置 , 让大家真正用好框架、学懂框架
来上一篇文章复习一下吧
点击即可跳转到前置文章
CSDN 平台观感有限 , 可以私聊作者获取源笔记链接
一 . 什么是 Spring AOP ?
Spring AOP = Spring + AOP
Spring 我们已经知道了 , Spring 是一个包含了众多工具和方法的 IoC 容器
那么这个 AOP 是什么呢 ?
我们之前学习过 OOP ( 面向对象编程 ) , 那这个 AOP 与 OOP 有什么关联呢 ?
AOP ( Aspect Oriented Programming ) : 面向切面编程 , 所谓的切面就是针对某一方面的功能进行处理 , 是一种通过分离功能性需求和横切关注点的方式 , 将特定功能从业务逻辑中独立出来的编程思想
比如说 : 用户的登录授权功能
这个功能是一类功能 , 在发布文章的时候要检验用户是否登录 , 在评论文章的时候要检验用户是否登录 , 在用户删除文章的时候要检验用户是否登录 …
在进行这些需要验证用户登录的操作的时候 , 我们都需要做某件事 , 那么我们就可以把这件事提取出来 , 做统一处理 , 这个功能我们就可以用 AOP 来完成
这里就不能用 OOP 处理了 , 这种高度抽象的事情我们就需要用 AOP 处理了 , 但是还离不开 OOP 思想
AOP 就相当于对 OOP 的补充
对于一样功能 , 我们可以分为好几个阶段 :
- 开发初期阶段 : 所有的方法都去实现一遍 , 比如添加文章、评论文章、删除文章 , 我们都需要各自实现一遍 , 同样的代码写了三遍 , 封装性不太好
- 开发中期阶段 : 封装成公共的方法 , 在需要的位置进行调用 . 但是我们仍然需要调用这个方法 , 这个方法与业务无关 , 这就造成了我们代码仍然是比较冗余 , 混杂进去了许多与业务无关的代码 , 并未实现代码的高度业务化
- 开发高级阶段 : 使用 AOP ( 拦截器 / 过滤器 ) 对某个功能做统一的处理 , 在业务代码中就不再需要去混杂非业务代码 ( 比如 : 登录状态校验 ) 了
举个栗子 :
开发中期阶段 : 火车站有 5 个站台 , 每个站台列车员都要检查违禁物品
开发高级阶段 : 火车站虽然有 5 个站台 , 但是违禁物品我们在进火车站的时候就统一检验了
使用 AOP , 更加实现了代码的解耦合
开发中期阶段 : 我们安全校验的函数需要传入两个参数 , 但是某一天业务变了 , 需要传入三个参数 , 因为我们还在业务代码中调用安全校验函数 , 所以业务代码中函数调用的部分也需要更改参数个数
开发高级阶段 : 我们把安全校验的函数统一处理 , 如果发生参数个数的改变 , 也与业务代码无关 , 实现了代码的解耦合
不使用 AOP , 我们的程序也能正常实现 , 但是一旦出现问题 , 我们的代码可维护性是非常差的 .
AOP 还可以实现 :
- 统⼀日志记录
- 统⼀方法执行时间统计
- 统⼀的返回格式设置
code : 返回错误码
message : 返回错误信息
data : 返回数据
- 统⼀的异常处理
- 声明型事务的开启和提交等
也就是说使用 AOP 可以扩充多个对象的某个能力 , 所以 AOP 可以说是 OOP ( Object Oriented Programming , 面向对象编程 ) 的补充和完善。
AOP 是一种思想 , Spring AOP 就是 AOP 思想的一种具体实现 ( 类似于 IoC 与 DI )
二 . AOP 基础组成
2.1 切面 (Aspect)
切面指的是当前 AOP 的作用 , 我们可以笼统的认为当前的 AOP 是针对谁的
切面可以看做是一个与业务逻辑无关 , 但对多个对象产生影响的模块化单元
比如 : 用户登录的判断 , 这就是一个切面 . 我还可以设计其他切面 , 比如 : 记录日志
这是一个很大的概念
切面由切点 ( Pointcut ) 和通知 ( Advice , 也叫做增强 ) 组成 . 它既包含了横切逻辑的定义 , 也包括了连接点的定义
2.2 连接点 (Join Point)
应用程序执行过程中 , 能够插入切面的一个点 , 就叫做连接点
举个栗子 : 我现在提供了一个功能 , 程序中有哪些位置需要这个功能 , 在哪些位置需要调用 AOP , 就称为连接点
2.3 切点 (Pointcut)
切点的作用 : 提供一组规则 , 用来匹配通知的
切面就是定义了哪件事需要重复调用 , 比如登录检查 , 切点就是制定我们拦截的规则
比如 : 注册是不需要进行登录检查的 , 因为我都没登录 , 如果注册阶段就进行登录检查的话 , 那这个账号就永远不会被注册成功 .
2.4 通知 (Advice)
通知就是具体要执行的动作 , 比如 : 借助切点 , 我们把某项操作拦截下来 , 但是拦截下来我们要干嘛呢
通知分为 5 种 :
- 前置通知使用 @Before : 通知方法会在目标方法调用之前执行
执行某个业务 , 执行之前先执行前置通知方法 (前置增强方法)
- 后置通知使用 @After : 通知方法会在目标方法返回或者抛出异常后调用
执行完这个业务之后 , 我再执行后置通知方法
- 返回之后通知使用 @AfterReturning : 通知方法会在目标方法返回后调用
在 return 之后 , 再通知一下
- 抛异常后通知使用 @AfterThrowing : 通知方法会在目标方法抛出异常后调用
在抛出异常之后 , 进行通知
- 环绕通知使用 @Around : 通知包裹了被通知的方法 , 在被通知的方法通知之前和调⽤之后执行自定义的行为
在被通知的方法执行之前或者执行之后执行的通知 , 叫做环绕通知
总结一下 : AOP 基础组成
- 切面 : 定义 AOP 业务类型的 (当前 AOP 是干嘛的)
- 连接点 : 有可能调用 AOP 的地方就叫做一个连接点
- 切点 : 定义 AOP 拦截规则
- 通知 / 增强方法 : 定义什么时候干什么事的
- 前置通知 : 拦截的目标方法之前执行的通知
- 后置通知 : 拦截的目标方法之后执行的通知
- 返回之前通知 : 拦截的目标方法返回数据之后通知
- 抛出异常之后的通知 : 拦截的目标方法抛出异常之后执行的通知
- 环绕通知 : 在拦截方法执行前后都执行的通知
可以再通过这个图片再理解一下
三 . 实现 Spring AOP
3.1 添加 Spring AOP 框架支持
我们不新创建项目了 , 用之前 MyBatis 的项目即可
把这段内容复制到 pom.xml 中
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2 定义切面和切点
之前我们编写的登录功能 , 我们就不在每个业务里面去写了 , 我直接就定义一个切面 , 我们执行业务代码的时候 , 他就会先去执行 AOP , 我们就不需要每个地方都去写登录功能了
我们在 demo 包底下新建一个 aop 包 , 再新建一个类 , 叫做 LoginAOP
声明切面的注解是 @Aspect , 声明切点的注解是 @Pointcut
然后编写以下代码
package com.example.demo.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 登录的 AOP 实现代码
*/
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
// 定义切点(拦截的规则)
// 括号里面内容先不用管
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
// 写一个返回值为void的空方法,方法名无所谓
public void pointcut() {
}
}
3.3 定义通知
前置通知
前置通知使用 @Before 注解
package com.example.demo.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 登录的 AOP 实现代码
*/
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
// 定义切点(拦截的规则)
// 括号里面内容先不用管
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
// 写一个返回值为void的空方法,方法名无所谓
public void pointcut() {
}
// 前置通知
// 在调用 UserController 里面的方法之前,先执行的方法
@Before("pointcut()")
public void before() {
System.out.println("执行了前置通知");
}
}
我们启动程序 , 通过浏览器查看效果
运行之后
那么我们把这里改成 TestController
但是我们访问的是 UserController 里面的方法 , 前置通知还能被打印吗
编写以下代码
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@RequestMapping("hi")
public String sayHi() {
return "hi";
}
}
接下来 , 我们去浏览器里面访问 127.0.0.1:8080/user/getall
, 看一看效果
那我们访问 127.0.0.1:8080/hi
呢 ?
把 TestController 改回来
后置通知
后置通知实现 @After 注解
package com.example.demo.aop;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 登录的 AOP 实现代码
*/
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
// 定义切点(拦截的规则)
// 括号里面内容先不用管
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
// 写一个返回值为void的空方法,方法名无所谓
public void pointcut() {
}
// 前置通知
// 在调用 UserController 里面的方法之前,先执行的方法
@Before("pointcut()")
public void before() {
System.out.println("执行了前置通知");
}
// 后置通知
@After("pointcut()")
public void after() {
// 后置通知实现的具体业务代码
System.out.println("执行了后置通知");
}
}
在浏览器查看一下效果
返回通知
使用注解 @AfterReturning
package com.example.demo.aop;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 登录的 AOP 实现代码
*/
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
// 定义切点(拦截的规则)
// 括号里面内容先不用管
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
// 写一个返回值为void的空方法,方法名无所谓
public void pointcut() {
}
// 前置通知
// 在调用 UserController 里面的方法之前,先执行的方法
@Before("pointcut()")
public void before() {
System.out.println("执行了前置通知");
}
// 后置通知
@After("pointcut()")
public void after() {
// 后置通知实现的具体业务代码
System.out.println("执行了后置通知");
}
// 返回通知
@AfterReturning("pointcut()")
public void afterReturning() {
// 返回通知实现的具体业务代码
System.out.println("执行了返回通知");
}
}
异常通知
使用注解 @AfterThrowing
package com.example.demo.aop;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 登录的 AOP 实现代码
*/
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
// 定义切点(拦截的规则)
// 括号里面内容先不用管
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
// 写一个返回值为void的空方法,方法名无所谓
public void pointcut() {
}
// 前置通知
// 在调用 UserController 里面的方法之前,先执行的方法
@Before("pointcut()")
public void before() {
System.out.println("执行了前置通知");
}
// 后置通知
@After("pointcut()")
public void after() {
// 后置通知实现的具体业务代码
System.out.println("执行了后置通知");
}
// 返回通知
@AfterReturning("pointcut()")
public void afterReturning() {
// 返回通知实现的具体业务代码
System.out.println("执行了返回通知");
}
// 异常通知
@AfterThrowing("pointcut()")
public void afterThrowing() {
// 异常通知实现的具体业务代码
System.out.println("执行了异常通知");
}
}
环绕通知
使用注解 @Around
注解部分 : @Around(“pointcut()”) 同上
返回值 : Object , 代表目标方法在执行之后 , 把生成的对象再返回给 Spring 框架 , 因为 Spring 框架执行完一个方法之后 , 可能还会去执行后续操作 , 比如 : 释放资源… , 所以他需要拿到这个对象
固定参数 : ProceedingJoinPoint joinPoint
, 代表正在执行的目标方法
Proceeding : 正在加工的
JoinPoint : 连接点
正在加工的连接点 -> 要去拦截的目标方法 -> 把目标方法拦截之后 , 变成了 ProceedingJoinPoint 的对象
接下来 , 我们自己去实现一个环绕通知
package com.example.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 登录的 AOP 实现代码
*/
@Component // Spring Boot 项目要添加该注解
@Aspect // 标识当前类为一个切面
public class LoginAOP {
// 定义切点(拦截的规则)
// 括号里面内容先不用管
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
// 写一个返回值为void的空方法,方法名无所谓
public void pointcut() {
}
// 前置通知
// 在调用 UserController 里面的方法之前,先执行的方法
@Before("pointcut()")
public void before() {
System.out.println("执行了前置通知");
}
// 后置通知
@After("pointcut()")
public void after() {
// 后置通知实现的具体业务代码
System.out.println("执行了后置通知");
}
// 返回通知
@AfterReturning("pointcut()")
public void afterReturning() {
// 返回通知实现的具体业务代码
System.out.println("执行了返回通知");
}
// 异常通知
@AfterThrowing("pointcut()")
public void afterThrowing() {
// 异常通知实现的具体业务代码
System.out.println("执行了异常通知");
}
// 环绕通知
// 注解部分:@Around("pointcut()") 同上
// 返回值:Object,代表目标方法在执行之后,把生成的对象再返回给 Spring 框架,因为 Spring 框架执行完一个方法之后,还会去执行后续操作,比如:释放资源...,所以他需要拿到这个对象
// 固定参数:ProceedingJoinPoint joinPoint
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object result = null;
// 前置业务代码
System.out.println("环绕通知的前置执行方法");
// 执行目标方法
try {
// 实际调用的是 UserController 里面的 getUsers 方法
result = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
// 后置业务代码
System.out.println("环绕通知的后置执行方法");
return result;
}
}
我们可以猜一下 : 环绕通知出现在前置通知的前还是后 , 环绕通知出现在后置通知的前还是后呢 ?
那么我们运行一下
可以发现 , 环绕通知执行顺序比前置通知还要早 , 执行顺序比后置通知还要晚 , 相当于龙头蛇尾 , 全给霸占上了 .
环绕通知最经典的用途就是记录方法执行时间
3.4 AOP 表达式
固定写法 :execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
四 . AOP 的实现原理
使用了 AOP 之后 , 我们就实现了一种特定的功能
比如 : 我们的登录验证 , 需要调用所有登录验证的位置 , 都不需要再关注登录验证这个事了 , 因为我们的 AOP 类里面已经做了相关处理了 , 并且我们也指定了拦截路径
这就代表我们拦截的话 , 就拦截 UserController 文件夹下面所有的方法 , 只要是需要登录验证的功能 , 全部写在 UserController 文件夹下 , 这样就可以实现登录拦截了 , 并且我们写业务的时候也不用关注登录验证功能了 .
那么 AOP 确实好用 , 但是 AOP 是咋回事呢 ?
4.1 AOP 原理
Spring AOP 是构建在动态代理基础上的 , 因此 Spring 对 AOP 的支持局限于方法级别的拦截
那么什么叫动态代理呢 ?
动态代理 静态代理
我们之前在讲 Fiddler 的时候 , 提到过代理
代理就可以理解为代购 , 我们想要去买海外商品就需要通过代购来帮我们购买
在程序执行期间生成的代理 , 就叫做动态代理
代理分为 : 静态代理、动态代理
以新年放烟花举例
静态代理指的是 : 一年四季一直卖烟花的人 , 实际上就是在程序还没执行之前就产生的代理
动态代理指的是 : 赶上正月十五放烟花的人多 , 有的人就进点炮去广场卖 , 卖几天就完事 , 这就指的是在程序运行期间生成的代理
动态代理也是一种思想 , 他在 Spring AOC 中的具体实现就是 JDK Proxy 和 CGLIB 方式
JDK Proxy 是 Java 官方提供给我们的动态代理
CGLIB 是第三方的动态代理
具体什么时候使用 JDK Proxy , 什么时候使用 CGLIB , 这要看我们的业务场景
假如说你的目标对象实现了接口或者实现了接口的子类 , 那么他就会使用 JDK Proxy
目标对象没有实现接口 , 只是一个普通的类 , 他就会使用 CGLIB
JDK 动态代理实现(了解)
不用具体了解 , 看看即可
package com.example.demo;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被代理类必须实现接⼝
public class PayServiceJDKInvocationHandler implements InvocationHandler {
//⽬标对象即就是被代理对象
private Object target;
public PayServiceJDKInvocationHandler(Object target) {
this.target = target;
}
//proxy代理对象
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过反射调⽤被代理类的⽅法
Object retVal = method.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target = new AliPayService();
//⽅法调⽤处理器
InvocationHandler handler =
new PayServiceJDKInvocationHandler(target);
//创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
PayService proxy = (PayService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{PayService.class},
handler
);
proxy.pay();
}
}
CGLIB 动态代理实现(了解)
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.Method;
public class PayServiceCGLIBInterceptor implements MethodInterceptor {
//被代理对象
private Object target;
public PayServiceCGLIBInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过cglib的代理⽅法调⽤
Object retVal = methodProxy.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target = new AliPayService();
PayService proxy = (PayService)
Enhancer.create(target.getClass(), new
PayServiceCGLIBInterceptor(target));
proxy.pay();
}
}
也是要实现接口 , 重写里面的 invoke 方法
创建对象也不能去 new , 需要通过其他方式
JDK 和 CGLB 实现的区别(面试题)
- JDK 实现 , 要求被代理类必须实现接口 , 之后是通过 InvocationHandler 及 Proxy , 在运行时动态的在内存中生成了代理类对象 , 该代理对象是通过实现同样的接口实现 ( 类似静态代理接口实现的方式 ) , 只是该代理类是在运行期时 , 动态的织入统⼀的业务逻辑字节码来完成
- CGLIB 实现 , 被代理类可以不实现接口 , 是通过继承被代理类,在运行时动态的生成代理类对象
- CGLIB 实现要比 JDK 实现 性能要高
织入 (Weaving) : 代理的生成时机
织入就是 代理生成的时机
代理生成的时机分为三类 :
- 编译期 : 编译时期生成的代理 (运行之前)
- 类加载期 : 类加载的时候 , 生成代理 (预运行阶段)
- 运行期 : 运行代码的时候生成的代理对象
动态代理生成在代码运行期 , 动态织入到字节码
总结
- AOP 是什么 : AOP 是面向切面编程 , 他是对某一方面的功能去进行处理
- Spring AOP 是对 AOP 思想的具体实现
分为 : JDK 和 CGLB 两种功能去实现的文章来源:https://www.toymoban.com/news/detail-682196.html
- 我们自己写的 Spring AOP , 是通过注解实现的 , 而这些注解 , 他来自于 ASpectJ , 而 AspectJ 又是来自于 Spring AOP 的
如果我们把 import 删除掉文章来源地址https://www.toymoban.com/news/detail-682196.html
- Spring AOP 实现步骤 :
- 添加 AOP 框架支持
- 定义切面和切点
- 定义通知 : 分为五种
- Spring AOP 是通过动态代理的方式 , 在运行期将 AOP 代码织入到程序中的
到了这里,关于Java EE 突击 14 - Spring AOP的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!