目录
1.1 AOP(Aspect Oriented Programming)面向切面编程
3.5在Spring切面类中,如何将一个方法设置为通知方法——加注解
6.2.2 具体实现@ControllerAdvice+ResponseBodyAdvice
6.3 统一异常处理@ControllerAdvice+@ExceptionHandler
一、Spring AOP的定义
1.1 AOP(Aspect Oriented Programming)面向切面编程
AOP是一种编程思想,他是指对一类事情进行集中处理。以博客系统为例,跳转到博客列表页、博客编辑页等页面,每个页面都需要判断用户是否登录,这样代码的冗余度较高。当我们学习了AOP,只需要进行配置一下,所有的页面都可以实现判断用户登录的功能。这样,我们的代码就更加简洁明了。
AOP是一种编程思想,SpringAOP是一个框架,是对AOP思想的具体实现。
二、Spring AOP的作用
我们主要借助AOP实现功能统一,并且使用的地方较多的功能。AOP的常见实现功能有以下几种:
统一的用户判断登录、统一日志记录、统一方法执行时间统计、统一的返回格式设置、统一的异常处理、事务的开启和提交等
使用AOP可以扩充多个对象的某个能力,AOP是对OOP(Object Oriented Programming,⾯向对象编程)的补充和完善。
三、AOP组成
3.1 切面Aspect
切面是由切点和通知组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
切面相当于包含了切点、通知和切面的类,相当于AOP实现的某个功能的集合。
3.2 连接点Join Point
代码执行过程中能够插入切面的一个点,这个点可以是方法调用时、抛出异常时、修改字段时。切面代码可以利用这个点插入到应用的正常流程之中,并添加新的行为。
连接点相当于需要被增强的某个AOP功能的所有方法。
3.3 切点PointCut
切点的作用是提供一组规则来匹配切面,给满足规则的连接点添加通知。
切点相当于保存了众多连接点的一个集合。
3.4 通知Advice
切面的工作叫做通知。
在通知中,需要定义切面是什么、切面什么时候适用,需要描述切面要完成的工作以及何时完成这个工作
3.5在Spring切面类中,如何将一个方法设置为通知方法——加注解
在方法上加上不同的注解,可以将其设置为通知,并且会在合适的时机调用这个通知方法。通知分为以下6种。
前置通知@Before:在目标方法调用前执行
后置通知@After:会在⽬标⽅法返回或者抛出异常后调⽤
返回之后通知@AfterReturning:在目标方法返回后调用
抛出异常后通知@AfterThrowing:在目标方法抛出异常后调用
环绕通知@Around:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
四、Spring AOP的实现
4.1 添加Spring AOP框架支持
在pom.xml添加以下配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4.2 定义切面和切点
切面:创建一个类,加上@Aspect注解,即可声明此类是一个切面
切点表达式的语法:
切点表达式由切点函数组成,其中execution()是最常用的切点函数,语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
其中,修饰符和异常可以省略。
其中:
* :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)
.. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。
+ :表示按照类型匹配指定类的所有类,必须跟在类名后⾯
接下来,举一个例子,拦截Controller.UserController类下的所有方法。
@Component
@Aspect // 声明此类是一个切面
public class UserAspect {
@Pointcut("execution(* com.example.demo.Controller.UserController.*(..)) ")
public void pointcut(){}
// pointcut()为空方法,不需要有方法体,只是声明方法,具体实现在通知里面写
}
pointcut ⽅法为空⽅法,它不需要有⽅法体,此⽅法名就是起到⼀个“标识”的作⽤,标识下⾯的通 知⽅法具体指的是哪个切点(因为切点可能有很多个)。
4.3 定义通知
通知中的内容是对切点方法的具体实现。举个例子:切点定义一个验证用户登录的空方法,通知中需要具体的实现这个方法要执行的任务。
通知分为前置通知、后置通知、返回之后通知、抛出异常之后通知、环绕通知五种。以下是五种通知的简单实现:
package com.example.demo.component;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect // 声明此类是一个切面
public class UserAspect {
@Pointcut("execution(* com.example.demo.Controller.UserController.*(..))")
public void pointcut(){}
// pointcut()为空方法,不需要有方法体,只是声明方法,具体实现在通知里面写
//前置通知
@Before("pointcut()")
public void doBefore(){
System.out.println("前置通知");
}
// 后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("后置通知");
}
// 返回之前通知
@AfterReturning("pointcut()")
public void afterReturn(){
System.out.println("返回之前通知");
}
// 抛出异常之后通知
@AfterThrowing("pointcut()")
public void doAfterThrowing(){
System.out.println("抛出异常之前通知");
}
// 环回通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object obj = null;
System.out.println("执行环绕通知的前置方法");
try {
obj = joinPoint.proceed(); // 执行拦截的方法
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("执行环绕通知的后置方法");
return obj;
}
}
注意:环绕通知比较麻烦,需要程序员手动的去触发
以下代码是UserController类的具体实现,
package com.example.demo.Controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@ResponseBody
@Controller
public class UserController {
@RequestMapping("/hello")
public String sayhi() throws InterruptedException {
System.out.println("------hello-------");
return "hello";
}
}
小练习:适用环绕通知计算程序的运行时间
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object obj = null;
System.out.println("执行环绕通知的前置方法");
long t1 = System.currentTimeMillis();
// 在UserController中的方法被执行之前干的事情
try {
obj = joinPoint.proceed(); // 执行拦截的方法,即在UserController中的方法
} catch (Throwable throwable) {
throwable.printStackTrace();
}
// 在UserController中的方法被执行之后干的事情
System.out.println("执行环绕通知的后置方法");
long t2 = System.currentTimeMillis();
System.out.println(String.format("程序的执行时间:%d",t2-t1));
return obj;
}
@RequestMapping("/runtime")
public void runtime() throws InterruptedException {
Thread thread = new Thread();
thread.sleep(1000);
}
五、Spring AOP实现原理(使用动态代理)
SpringAOP是通过动态代理的方式,在运行期将AOP代码织入到程序中的,他的实现方式有两种,JDK Proxy 和 CGLIB。
Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的⽀持局限于⽅法级别的拦截。 Spring AOP ⽀持 JDK Proxy 和 CGLIB ⽅式实现动态代理。
JDK Proxy:JDK自身的动态代理,在有接口的场景下会使用此方式。
CGLIB :使用字节码技术实现的动态代理,性能高,在普通类中会使用CGLIB
调用者先调用代理类,代理类经过验证后,然后才能访问目标对象。
六、AOP实战
6.1 统一用户登录权限验证
博客系统中每个功能的实现都需要先判断用户是否登录,同样功能的代码写了多次,增加了程序的冗余度,同时增加了后期的修改成本和维修成本。我们现在可以使用SpringAOP的前置通知或者环绕通知来实现。在用户登录之后,会将用户的登录信息保存在session中,在实现其他的功能时,需要先读取session中的user信息。要想获取session,我们得先获得HttpSession对象。同时,并不是所有得方法都需要验证用户的登录信息,如登录和注册。将问题总结为以下两点:
①要想获取session,我们得先获得HttpSession对象。
②只需要拦截部分的方法,登录、注册这样的方法不用拦截。
我们可以使用Spring拦截器HandlerInterceptor,拦截器的实现分两个步骤:
①创建自定义拦截器,实现HandlerInterceptor接口的preHandle方法。
package com.example.demo.component;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
// 当前的类只是一个普通类,但是并没有加到Spring中。目前的情况就是:拦截器和拦截规则已经有了,但是还没有生效,要想让他生效,还需要配置一下
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 用户的登录验证
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute(ApplicationVariable.USER_SESSION_KEY)!= null){
// 如果是true,会访问当前你想要访问的目标方法
log.info("--------------拦截器验证通过--------------");
return true;
}
log.info("--------------拦截器验证未通过--------------");
response.setStatus(401);
// 如果是false。访问到此结束
return false;
}
}
②将自定义的拦截器加入WebMvcConfigure的addInterceptors方法中。
package com.example.demo.component;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 将拦截器添加到全局的配置项目文件中
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()) // 指明拦截器
.addPathPatterns("/**") // 表示需要拦截的URL,**表示拦截任意方法
.excludePathPatterns("/login");
// .excludePathPatterns("/**/register") // 需要排除的URL和静态文件,如图片、js、css等文件
// .excludePathPatterns("/**/*.js")
// .excludePathPatterns("/**/*.css")
// .excludePathPatterns("/**/*.jpg")
// .excludePathPatterns("/**/*.html")
// .excludePathPatterns("/**/login");
}
}
为了验证拦截器代码的正确性,写一个login的伪代码,约定name=admin,password=1111即登录成功。登录成功后将用户信息保存在session中。
先建立一个user类存储用户信息
package com.example.demo.component;
import lombok.Data;
import org.springframework.stereotype.Component;
@Data
@Component
public class User {
private String name;
private String password;
}
再写login的伪代码:
@RequestMapping("/login")
public String login(HttpServletRequest req, String name, String password){
if(StringUtils.hasLength(name) && StringUtils.hasLength(password)
&& "admin".equals(name) && "1111".equals(password)){
HttpSession session = req.getSession();
User user = new User();
user.setName(name);
user.setPassword(password);
System.out.println(user);
session.setAttribute(ApplicationVariable.USER_SESSION_KEY,user);
return "登录成功";
}
return "登录失败";
}
将用户的session的可以设置为一个静态常量。
package com.example.demo.component;
public class ApplicationVariable {
public final static String USER_SESSION_KEY = "USER_SESSION_KEY";
}
验证:
先写一个sayhi方法,不登陆直接访问,会返回一个401
@RequestMapping("/hello")
public String sayhi() throws InterruptedException {
System.out.println("------hello-------");
return "hello";
}
,也会提示验证未通过。
接下来,先登录再访问hello页面
6.2 统一数据返回格式
6.2.1 统一数据返回格式的原因
方便前端程序员更好的接收和解析后端数据接口返回的数据。
降低前端程序员和后端程序员的沟通成本,按照某个格式实现就行了,因为所有接口都是这样返回
的。
有利于项目统一数据的维护和修改。
有利于后端技术部门的统一规范的标准制定,不会出现奇奇怪怪的返回内容。
为了统一数据的返回格式,我们可以使用拼接字符串的形式:
@RequestMapping("/login")
public HashMap<String,Object> login(HttpServletRequest req, String name, String password){
HashMap<String,Object> result = new HashMap<>();
if(StringUtils.hasLength(name) && StringUtils.hasLength(password)
&& "admin".equals(name) && "1111".equals(password)){
HttpSession session = req.getSession();
User user = new User();
user.setName(name);
user.setPassword(password);
System.out.println(user);
session.setAttribute(ApplicationVariable.USER_SESSION_KEY,user);
result.put("success",1);
result.put("message","");
result.put("data",true);
return result;
}
result.put("success",-1);
result.put("message","");
result.put("data",false);
return result;
}
然而我们的理想代码是:
@RequestMapping("/login2")
public Boolean login2(HttpServletRequest req, String name, String password){
if(StringUtils.hasLength(name) && StringUtils.hasLength(password)
&& "admin".equals(name) && "1111".equals(password)){
HttpSession session = req.getSession();
User user = new User();
user.setName(name);
user.setPassword(password);
System.out.println(user);
session.setAttribute(ApplicationVariable.USER_SESSION_KEY,user);
return true;
}
return false;
}
借助SpringAOP,我们可以只关注我们的业务逻辑,统一返回数据格式可以依靠SpringAOP。
6.2.2 具体实现@ControllerAdvice+ResponseBodyAdvice
package com.example.demo.common;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
/**
* 返回的内容是否需要重写
* @return 返回true表示重写
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 最终返回给前端封装的数据对象
* @param body 方法返回的原始数据
* @param returnType 方法的返回类型
* @param selectedContentType html标签、json
* @param selectedConverterType 返回值的类的类型
* @param request
* @param response
* @return
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 构造统一的返回对象
HashMap<String,Object> result = new HashMap<>();
result.put("success",1);
result.put("message","");
result.put("data",body);
return result;
}
}
6.3 统一异常处理@ControllerAdvice+@ExceptionHandler
统一异常处理使用的是@ControllerAdvice和@ExceptionHandler来实现的,@ControllerAdvice表示控制器通知类,@ExceptionHandler是异常处理器,两个结合表示当出现异常的时候执行某个通知。
处理空指针的异常:
package com.example.demo.common;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public HashMap<String,Object> nullPointerExceptionHandler(NullPointerException e){
//如果出现了异常,就给前端返回一个HashMap的对象
HashMap<String,Object> map = new HashMap<>();
System.out.println("exception");
map.put("success",0);
map.put("status",1);
map.put("msg",e.getMessage());
System.out.println(map);
return map;
}
}
在UserController类中的sayhi方法中写一个空指针异常,代码如下
@RequestMapping("/hello")
public String sayhi() throws InterruptedException {
int[] elem = null;
int a = elem[0];
return "hello";
}
如果此时写一个算数异常:
@RequestMapping("/hello")
public String sayhi() throws InterruptedException {
int a = 1/0;
return "hello";
}
刚刚写的方法不能处理算数异常,此时的解决方案有两个:
方案一,写一个函数啊,处理算数异常
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public HashMap<String,Object> nullPointerExceptionHandler(ArithmeticException e){
//如果出现了异常,就给前端返回一个HashMap的对象
HashMap<String,Object> map = new HashMap<>();
System.out.println("算数exception");
map.put("success",0);
map.put("status",1);
map.put("msg",e.getMessage());
System.out.println(map);
return map;
}
效果:
方案2:写一个可以处理所有异常的函数
@ExceptionHandler(Exception.class)
@ResponseBody
public Object handler(Exception e){
//如果出现了异常,就给前端返回一个HashMap的对象
HashMap<String,Object> map = new HashMap<>();
map.put("success",0);
map.put("status",1);
map.put("msg",e.getMessage());
return map;
}
由于无法将处理所有异常的方法都实现,因此,可以将处理Exception异常当作默认异常,不管出现啥异常,都可以将异常信息按照约定的格式返回给前端。
当处理算数异常和处理Exception的方法同时存在,发生算数异常时,调用的是前者。
总结
AOP是一种编程思想,他是指对一类事情进行集中处理。SpringAOP该思想的实现,是通过动态代理的方式,在运行期将AOP代码织入到程序中,实现方式有JDK Proxy 和 CGLIB两种。AOP由切面和连接点组成,切面由切点和通知组成。在Spring中,在类上加上@Aspect注解,即可声明此类是一个切面。切点由切点函数组成,常用的切点函数是execution()。切点函数是没有函数体,切点函数的具体实现由通知实现。最后通过统一用户登录权限验证、统一数据返回格式、统一异常处理介绍了AOP的应用。