目录
2.2、方法二:在 application.properies 中配置
一、Spring 拦截器
1.1、背景
在原生的 Spring AOP 中实现统一的拦截的难点在于:1.定义拦截规则(表达式)很难,2.在切面类中拿到 HttpSession 比较难;如何解决这两个难点呢?使用拦截器!
1.2、实现步骤
实现一个普通的拦截器关键在于以下两步:
- 实现 HandlerInterceptor 接口,重写 preHandler 方法,在方法中编写自己的业务代码。
- 将拦截器添加到配置文件中,设置拦截规则。
具体的,首先步骤一,例如要实现一个用户登录判断,就需要创建一个类,这里起名叫LoginInterceptor 类,实现 HandlerInterceptor 接口,重写 preHeadler 方法(此方法返回的是以个 boolean 类型,如果为 true 表示验证成功,可以继续执行后面的流程,若是 false 表示验证失败,后面的流程就不执行了),通过是否可以获取到 Session 信息判断用户是否已经登陆,来返回 true 或 false。
a)实现 HandlerInterceptor 接口,重写 preHandler 方法,如下代码:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
//登录拦截器
public class LoginInterceptor implements HandlerInterceptor {
/**
* 此方法返回一个 boolean,若为 true 表示验证成功,否则验证失败,后面的流程不能执行了
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 用户登录业务判断
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute("userinfo") != null) {
//说明用户已经登陆
return true;
}
//可以调整登录页面,或者 返回一个 401/403 没有权限
response.sendRedirect("/login.html");
return false;
}
}
b)将拦截器添加到配置文件中,设置拦截规则
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("/**") //连接所有请求,值得注意的是,这里不能只写一个 *,一个 * 表示一级路径
.excludePathPatterns("/user/login") //不拦截的 url
.excludePathPatterns("/user/reg")
.excludePathPatterns("/**/*.html"); //不拦截所有的页面
}
}
注意:
addPathPatterns:表示需要拦截的 URL,“**”表示拦截任意⽅法(也就是所有⽅法)。 excludePathPatterns:表示需要排除的 URL。
说明:以上拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件、JS 和 CSS 等⽂件)
1.3、拦截原理
二、 统一url前缀路径
2.1、方法一:在系统的配置文件中设置
具体的,重写 WebMvcConfigurer 接口下的 configurePathMatch 方法,例如修改所有请求url添加前缀 /zhangsan ,如下代码:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/zhangsan", c -> true);
}
}
2.2、方法二:在 application.properies 中配置
例如修改所有请求url添加前缀 /zhangsan,如下代码:
server.servlet.context-path=/zhangsan
三、统一异常处理
统一异常处理是通过如下两个注解结合实现的:
- @ControllerAdvice:表示控制器通知类。
- @ExceptionHandler:表示异常处理器。
两个结合表示出现异常的时候执行某个通知方法,具体的步骤如下:
- 创建一个类,标识上 @ControllerAdvice;
- 在方法上添加 @ExceptionHandler;
Ps:如果是微服务项目,那么把上述内容写到一个单独的模块中引入当前微服务即可.
如下代码:
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
@ResponseBody
public class MyExHandler {
/**
* 拦截所有的空指针异常,进行统一的数据返回
* @param e
* @return
*/
@ExceptionHandler(Exception.class) //这里也可以根据实际情况,填写不同的异常
public HashMap<String, Object> nullException(NullPointerException e) {
HashMap<String, Object> reslut = new HashMap<>();
reslut.put("code", "-1");
reslut.put("msg", "空指针异常" + e.getMessage());
reslut.put("data", null);
//这里返回 HashMap ,就相当于项前端返回了一个 JSON 格式的数据
return reslut;
}
}
统一异常处理最佳实践
a)创建一个枚举类,用来自定义错误状态码和信息.
/**
* 错误码定义
*/
public enum ResultCode {
SUCCESS (0, "操作成功"),
FAILED (1000, "操作失败"),
FAILED_UNAUTHORIZED (1001, "未授权"),
FAILED_PARAMS_VALIDATE (1002, "参数校验失败"),
FAILED_FORBIDDEN (1003, "禁止访问"),
FAILED_CREATE (1004, "新增失败"),
FAILED_NOT_EXISTS (1005, "资源不存在"),
// 关于用户的错误描述
FAILED_USER_EXISTS (1101, "用户已存在"),
FAILED_USER_NOT_EXISTS (1102, "用户不存在"),
FAILED_LOGIN (1103, "用户名或密码错误"),
FAILED_USER_BANNED (1104, "您已被禁言, 请联系管理员, 并重新登录."),
FAILED_USER_ARTICLE_COUNT (1105, "更新帖子数量失败"),
FAILED_TWO_PWD_NOT_SAME (1106, "两次输入的密码不一致"),
// 关于版块的错误描述
FAILED_BOARD_ARTICLE_COUNT (1201, "更新帖子数量失败"),
FAILED_BOARD_BANNED (1202, "版块状况异常"),
FAILED_BOARD_NOT_EXISTS (1203, "版块不存在"),
FAILED_ARTICLE_NOT_EXISTS (1301, "帖子不存在"),
FAILED_ARTICLE_BANNED (1302, "帖子状况异常"),
FAILED_MESSAGE_NOT_EXISTS (1401, "站内信不存在"),
ERROR_SERVICES (2000, "服务器内部错误");
//状态码
private int code;
//状态描述
private String message;
ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public String toString() {
return "ResultCode{" +
"code=" + code +
", message='" + message + '\'' +
'}';
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
b)自定义异常信息
/**
* 自定义异常
*/
public class ApplicationException extends RuntimeException {
// 在异常中持有一个错误对象
protected AppResult errorResult;
public ApplicationException(AppResult appResult) {
super(appResult.getMsg());
this.errorResult = appResult;
}
public AppResult getErrorResult() {
return errorResult;
}
}
c)统一异常信息处理
@Slf4j
@ControllerAdvice
public class HandlerException {
/**
* 处理自定义已知异常
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler(ApplicationException.class)
public AppResult applicationException(ApplicationException e) {
e.printStackTrace();
log.error(e.getMessage());
//获取自己传递的异常信息
if(e.getErrorResult() != null) {
return e.getErrorResult();
}
//默认异常信息
return AppResult.fail(e.getMessage());
}
/**
* 处理未捕获的异常
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler(Exception.class)
public AppResult handlerException(Exception e) {
e.printStackTrace();
log.error(e.getMessage());
//非空校验
if(!StringUtils.hasLength(e.getMessage())) {
//为空就返回自定义异常
return AppResult.fail(ResultCode.ERROR_SERVICE);
}
//默认的异常信息
return AppResult.fail(e.getMessage());
}
}
Ps:AppResult 是统一返回数据类型的格式. ResultCode 是自定义的枚举类状态码
d)例如:登录服务(redis 结合 jwt)
@Override
public User login(String username, String password) {
//1.非空校验
User dbUser = null;
if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
log.error(ResultCode.FAIL_NULL_USERNAME_OR_PASSWORD.toString());
throw new ApplicationException(AppResult.fail(ResultCode.FAIL_NULL_USERNAME_OR_PASSWORD));
}
//2.校验是否正确
dbUser = userMapper.selectUserByUserName(username);
if(dbUser == null || !dbUser.getPassword().equals(password)) {
//密码错误
log.error(ResultCode.FAIL_ERROR_USERNAME_OR_PASSWORD.toString());
throw new ApplicationException(AppResult.fail(ResultCode.FAIL_ERROR_USERNAME_OR_PASSWORD));
}
//3.jwt 处理
HashMap<String, String> map = new HashMap<>();
map.put("username", username);
String token = JwtUtils.createToken(map);
//在 redis 上在保存一份的原因是为了避免以下情况:
//由于 JWT 一旦生成,有效期内一直有效,因此黑客攻击,已经拿到用户的权限,在服务器上乱搞,作为管理员也没有办法
//如果之前用 redis 存储过,此时,就可以就可以直接删除 redis 上的 token
//这样用户下次登录时,先用 Jwt 检验,然后在检验 redis 上有没有该用户 token 即可(没有就说明有问题)
redisTemplate.opsForValue().set(username, token, 30, TimeUnit.SECONDS);//设置 30 分钟超时时间
//3.校验通过,返回用户信息
return dbUser;
}
e)简单的测试
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/testException")
public AppResult testException() throws ApplicationException {
throw new ApplicationException("触发了自定义异常...");
}
}
效果如下:
四、统一返回数据返回格式处理
4.1、背景
为什么要统一数据返回格式处理?例如以下几个原因:
- 方便前端程序员更好的接收和解析后端返回的数据;
- 降低约定前后端交互接口的成本,按照某种格式实现即可,因为所有的接口都是这样返回的。
- 有利于项目的统一数据的维护和修改。
4.2、具体实现
统一数据格式返回的实现需要以下两个步骤:
- 创建一个类,并添加 @ControllerAdvice。
- 实现 ResponseBodyAdvice 接口,重写 supports 和 beforeBodyWrite。
Ps:
1、 supports 方法不用编写业务逻辑,而是像一个控制器一样,返回 true 则执行 beforeBodyWrite 方法,反之则不执行。
2、beforeBodyWrite 方法就是用来实现统一对象的。
具体的如下:
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;
import java.util.HashMap;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
//将 java 对象转化成 JSON 格式
@Autowired
private ObjectMapper objectMapper;
/**
* 此方法返回 true 则执行下面的 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) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "");
result.put("data",body);
// 这里需要进行特殊处理,因为 String 在转换的时候报错
if(body instanceof String) {
try {
return objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return result;
}
}
代码中为什么要进行特殊处理?(最易出错!)
在 java 程序中, String 是一个最特殊的类型(既不是基础类型也不是对象),并且在重写方法时也很特殊,除了 String 其他的都是用一个格式化工具,而 String 用的是自己的一套格式化工具,因此在转换成 HashMap 的时候还没有被加载好,而其他的转换器已经加载好了,最后就会引发如下异常:
因此就要判断 body 是否为 String ,若为 String 类型,就要进行特殊处理,使用 JSON 的writeValueAsString 方法将 Java 对象转换成 JSON 格式再返回。
4.3、实际项目中的写法
实际的项目中,统一数据格式的返回我们因该这样写:
- 创建一个类(自定义类名为:AjaxResult)约束为 JSON 返回格式。
- 创建一个类(自定义类名为:ResponseAdvice)实现 ResponseBodyAdvice 接口,重写其中的两个方法,一个是“开关”,另一个是用来对数据的格式进行校验和封装(注意:方法中的参数 body 若为 Spring 类型,比较特殊,需要使用 ObjectMapper 单独进行 JSON 格式处理)。
- 如果是微服务项目,那么把上述内容写到一个单独的模块中引入当前微服务即可.
具体的如下代码:
注意:@Data 注解一定不能忘!!!
a)AjaxResult 类
import lombok.Data;
import java.io.Serializable;
/**
* JSON统一数据格式返回
*/
@Data
public class AjaxResult implements Serializable {
//状态码
private Integer code;
//状态码描述信息
private String msg;
//返回的数据
private Object data;
/**
* 操作成功返回的结果
*/
public static AjaxResult success(Object data) {
AjaxResult result = new AjaxResult();
result.setCode(200);
result.setMsg("");
result.setData(data);
return result;
}
public static AjaxResult success(int code, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg("");
result.setData(data);
return result;
}
public static AjaxResult success(int code, String msg, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(200);
result.setMsg(msg);
result.setData(data);
return result;
}
/**
* 返回失败结果
*/
public static AjaxResult fail(int code, String msg) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(null);
return result;
}
public static AjaxResult fail(int code, String msg, Object data) {
AjaxResult result = new AjaxResult();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
}
b)ResponseAdvice 类
import com.example.demo.common.AjaxResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
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;
/**
* 开关
* @param returnType
* @param converterType
* @return
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 对数据格式进行校验和封装
* @param body
* @param returnType
* @param selectedContentType
* @param selectedConverterType
* @param request
* @param response
* @return
*/
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if(body instanceof AjaxResult) {
return body;
}
//String比较特殊,要单独处理
if(body instanceof String) {
return objectMapper.writeValueAsString(AjaxResult.success(body));
}
return AjaxResult.success(body);
}
}