目录
[1.1 每个方法验证](#1.1 每个方法验证)
[1.2 Spring AOP 用户统一登陆验证](#1.2 Spring AOP 用户统一登陆验证)
[1.3 拦截器](#1.3 拦截器)
[1.3.1 自定义拦截器](#1.3.1 自定义拦截器)
[1.3.2 将自定义拦截器配置到系统设置中,并且设置拦截规则](#1.3.2 将自定义拦截器配置到系统设置中,并且设置拦截规则)
[1.3.3 排除所有的静态资源](#1.3.3 排除所有的静态资源)
[1.4 登录拦截器(练习)](#1.4 登录拦截器(练习))
[1.5 拦截器原理](#1.5 拦截器原理)
[3.1 统一数据处理(强制执行)](#3.1 统一数据处理(强制执行))
统一处理的功能包括:
- 统一用户登陆权限验证
- 统一数据格式返回
- 统一异常处理
1.用户登陆权限验证
之前学到的是在每个方法中验证用户登录权限,现在是统一的用户登录验证处理
1.1 每个方法验证
最初的验证方法
java
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 某⽅法 1
*/
@RequestMapping("/m1")
public Object method(HttpServletRequest request) {
//有 session 就获取,没有不会创建
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute("userinfo") != null) {
// 说明已经登录,业务处理
return true;
} else {
// 未登录
return false;
}
}
}
每个方法中都有相同的用户登录验证权限,它的缺点是:
- 每个方法都要单独写一个用户登录验证的方法
- 添加的控制器越多,调⽤⽤户登录验证的⽅法也越多,这样就增加了后期的修改成本和维护成本。
- 这些⽤户登录验证的⽅法和接下来要实现的业务⼏何没有任何关联,但每个⽅法中都要写⼀遍
1.2 Spring AOP 用户统一登陆验证
通过前置通知或者环绕通知实现:
java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {
// 定义切点⽅法 controller 包下、⼦孙包下所有类的所有⽅法
@Pointcut("execution(* com.example.demo.controller..*.*(..))")
public void pointcut(){ }
// 前置⽅法
@Before("pointcut()")
public void doBefore(){
}
// 环绕⽅法
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object obj = null;
System.out.println("Around ⽅法开始执⾏");
try {
// 执⾏拦截⽅法
obj = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("Around ⽅法结束执⾏");
return obj;
}
}
缺点:
- 无法获取 HttpSession 对象
- 我们要对⼀部分方法机型拦截,⽽另⼀部分方法不拦截,如注册方法和登录⽅法是不拦截的,这样的话排除⽅法的规则很难定义,甚⾄没办法定义。
1.3 拦截器
Spring 提供了具体的实现拦截器:HandlerInterceptor
- 创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理)方法
- 将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中
1.3.1 自定义拦截器
用户登陆拦截器
- 实现 HandlerInterceptor 接口 的拦截器
- 实现 preHandle(执行具体方法之前的预处理)方法:执行目标方法之前判断是否有登录
- 重新 preHandle 方法:返回 true(拦截器验证成功,继续执行后续的方法);返回 false(拦截器验证失败,不会执行后续目标方法)
- 写业务方法:得到 Session 对象,判断 Session 中是否有登陆用户,有------true;无------false
java
package com.example.demo.config;
import com.example.demo.common.AppVar;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 自定义拦截器
*/
public class UserInterceptor implements HandlerInterceptor {
/**
* 返回 true:表示拦截器验证成功,继续执行后续的方法
* false:拦截器验证失败,不会执行后续目标方法
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//业务方法
//得到 Session 对象:判断 Session 中是否有登陆用户,有------true;无------false
HttpSession session = request.getSession(false);//默认 true
if (session != null && session.getAttribute(AppVar.SESSION_KEY) != null) {
//用户已经登陆
return true;
}
return false;
}
}
定义 Session Key 值:
java
package com.example.demo.common;
//全局变量
public class AppVar {
//Session Key
public static final String SESSION_KEY = "SESSION_KEY";
}
1.3.2 将自定义拦截器配置到系统设置中,并且设置拦截规则
配置文件 AppConfig:
- 实现 WebMvcConfigurer,表明是一个系统的配置文件
- 重写 addInterceptors 方法(支持添加多个拦截器)
添加多个拦截器注入有两种方法:
1️⃣
java
registry.addInterceptor(new UserInterceptor());
2️⃣ UserInterceptor 类中添加 @Component 注解,随着 spring 启动而启动;通过 @Autowired 注入进去
@Configuration 注解是随着 spring 启动而启动,将当前配置设置到系统中
java
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
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 {
@Autowired
private UserInterceptor userInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//registry.addInterceptor(new UserInterceptor());
registry.addInterceptor(userInterceptor)
.addPathPatterns("/**")//拦截规则:/**拦截所有的请求
.excludePathPatterns("/user/reg")//不拦截
.excludePathPatterns("/user/login");
}
}
拦截规则:
- addPathPatterns:表示需要拦截的 URL
- /**:表示拦截任意方法(也就是所有方法)
- excludePathPatterns:表示需要排除的 URL
新建 Controller 类(一个拦截、两个不拦截):
java
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getuser")
public String getUser() {
System.out.println("do getUser()");
return "user";
}
@RequestMapping("/reg")
public String reg() {
System.out.println("do reg()");
return "reg";
}
@RequestMapping("/login")
public String login() {
System.out.println("do login()");
return "login";
}
}
在拦截器里边执行一个 do UserInterceptor:
java
@Component
public class UserInterceptor implements HandlerInterceptor {
/**
* 返回 true:表示拦截器验证成功,继续执行后续的方法
* false:拦截器验证失败,不会执行后续目标方法
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("do UserInterceptor");
//业务方法
//得到 Session 对象:判断 Session 中是否有登陆用户,有------true;无------false
HttpSession session = request.getSession(false);//默认 true
if (session != null && session.getAttribute(AppVar.SESSION_KEY) != null) {
//用户已经登陆
return true;
}
return false;
}
}
运行启动类,首先我们先看不拦截的:访问 localhost:8080/user/reg
被拦截的:访问 localhost:8080/user/getuser
1.3.3 排除所有的静态资源
java
// 拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有接⼝
.excludePathPatterns("/**/*.js")
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.jpg")
.excludePathPatterns("/login.html")
.excludePathPatterns("/**/login"); // 排除接⼝
}
- /*:一级目录
- /**:所有目录
在这里有一个问题就是排除图片,图片的格式有很多,总不能一个一个排除,这样的情况我们可以在 resource 下的 static 创建一个 image 目录,把所有图片都放在这里边,排除 image 里边的所有东西
java
.excludePathPatterns("/image/**");
1.4 登录拦截器(练习)
1.登录、注册页面不拦截,其他页面拦截
2.当登录成功写入 session 之后,拦截的页面也可以正常访问
1.拦截功能
java
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
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 {
@Autowired
private UserInterceptor userInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//registry.addInterceptor(new UserInterceptor());
registry.addInterceptor(userInterceptor)
.addPathPatterns("/**")//拦截规则:/**拦截所有的请求
.excludePathPatterns("/login.html")
.excludePathPatterns("/reg.html")
.excludePathPatterns("/css/**")
.excludePathPatterns("/editor/**")
.excludePathPatterns("/img/**")
.excludePathPatterns("/js/**")
.excludePathPatterns("/**/login"); // 排除接⼝
;
}
}
2.拦截器
java
package com.example.demo.config;
import com.example.demo.common.AppVar;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 自定义拦截器
*/
@Component
public class UserInterceptor implements HandlerInterceptor {
/**
* 返回 true:表示拦截器验证成功,继续执行后续的方法
* false:拦截器验证失败,不会执行后续目标方法
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("do UserInterceptor");
//业务方法
//得到 Session 对象:判断 Session 中是否有登陆用户,有------true;无------false
HttpSession session = request.getSession(false);//默认 true
if (session != null && session.getAttribute(AppVar.SESSION_KEY) != null) {
//用户已经登陆
return true;
}
return false;
}
}
1.5 拦截器原理
所有的 Controller 执行都会通过⼀个调度器 DispatcherServlet来实现,这⼀点可以从 Spring Boot 空制台的打印信息看出
而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度方法,源码一部分:
java
//调用预处理【重点】
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
执行所有的拦截器:所有拦截器不等于 false,才会进行之后的代码
java
// 执⾏ Controller 中的业务
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
所有的 Controller 执行都会通过⼀个调度器 DispatcherServlet 来实现,而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度方法(其中会执行 applyPreHandle 方法,所有拦截器不等于 false,才会执行 Controller 方法)
applyPreHandle 源码:
java
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
执行一个目标方法的时候,循环所有的拦截器,拿到容器中的所有拦截器,再去执行拦截器的预执行方法(preHandle 方法),如果有一个 preHandle 方法为 false,就返回 false,就不执行后续流程,直接返回;返回true,执行 controller 方法
2.统一异常处理
统⼀异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件
默认返回的是一个页面(需要的是一个数据不是页面),这个时候需要加 @ResponseBody ,返回一个数据,而 @ResponseBody 既可以加到方法上也可以加在类上,所以可以直接使用注解 @RestControllerAdvice
假设构建一个空指针异常(UserController 类):
java
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/reg")
public String reg() {
System.out.println("do reg()");
Object obj = null;
System.out.println(obj.hashCode());
return "reg";
}
}
定义统一的返回对象:
java
package com.example.demo.common;
import lombok.Data;
//统一的对象
@Data
public class ResultAjax {
private int code;//状态码
private String msg;//状态码的描述信息
private Object data;//返回数据
}
空指针异常处理:
方法名和返回值可以自定义,其中最重要的是**@ExceptionHandler(Exception.class)**注解
java
package com.example.demo.config;
import com.example.demo.common.ResultAjax;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(NullPointerException.class)
public ResultAjax doNullPointerException(NullPointerException e) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(-1);
resultAjax.setMsg("空指针异常:" + e.getMessage());
resultAjax.setData(null);
return resultAjax;
}
}
访问 localhost:8080/user/reg
上述为空指针异常,那么异常有很多,我们难道全要写这些异常?
这个时候我们可以设置一个默认异常,所有异常都是基于 Exception
java
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
public ResultAjax doException(Exception e) {
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(-1);
resultAjax.setMsg("异常:" + e.getMessage());
resultAjax.setData(null);
return resultAjax;
}
}
3.统一数据返回格式
- 方便前端程序员更好的接收和解析后端数据接口返回的数据
- 降低前端程序员和后端程序员的沟通成本
- 有利于项⽬统⼀数据的维护和修改
- 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容
3.1 统一数据处理(强制执行)
- 使用 @ControllerAdvice
- 实现 ResponseBodyAdvice 接口,并重写它的两个方法,supports 必须返回 true,beforeBodyWrite 方法中进行重新判断和重写操作
java
package com.example.demo.config;
import com.example.demo.common.ResultAjax;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 听译返回值的保底实现类
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;//spring 自带的
/**
* true -> 才会调用 beforeBodyWrite 方法
* 反之则永远不会调用 beforeBodyWrite 方法
* @param returnType
* @param converterType
* @return
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 已经包装好的对象
if (body instanceof ResultAjax) {
return body;
}
//对字符串进行判断和处理
if (body instanceof String) {
ResultAjax resultAjax = ResultAjax.succ(body);
try {
return objectMapper.writeValueAsString(resultAjax);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return ResultAjax.succ(body);
}
}
java
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getnum")
public int getNum() {
return 1;
}
@RequestMapping("/getstr")
public String getStr() {
return "hello";
}
}
String 比较特殊,既不属于对象,也不属于基本数据类型,在返回值的时候也是需要单独判断