【Spring AOP】如何统一“拦截器校验、数据格式返回、异常返回”处理?

目录

一、Spring  拦截器

1.1、背景

1.2、实现步骤

1.3、拦截原理

二、 统一url前缀路径

2.1、方法一:在系统的配置文件中设置

2.2、方法二:在 application.properies 中配置

三、统一异常处理

四、统一返回数据返回格式处理

4.1、背景

4.2、具体实现

4.3、实际项目中的写法


一、Spring  拦截器


1.1、背景

在原生的 Spring AOP 中实现统一的拦截的难点在于:1.定义拦截规则(表达式)很难,2.在切面类中拿到 HttpSession 比较难;如何解决这两个难点呢?使用拦截器!

1.2、实现步骤

实现一个普通的拦截器关键在于以下两步:

  1. 实现 HandlerInterceptor 接口,重写 preHandler 方法,在方法中编写自己的业务代码。
  2. 将拦截器添加到配置文件中,设置拦截规则。

具体的,首先步骤一,例如要实现一个用户登录判断,就需要创建一个类,这里起名叫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:表示异常处理器。

两个结合表示出现异常的时候执行某个通知方法,具体的步骤如下:

  1. 创建一个类,标识上 @ControllerAdvice;
  2. 在方法上添加 @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、具体实现

统一数据格式返回的实现需要以下两个步骤:

  1. 创建一个类,并添加 @ControllerAdvice。
  2. 实现 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、实际项目中的写法

实际的项目中,统一数据格式的返回我们因该这样写:

  1. 创建一个类(自定义类名为:AjaxResult)约束为 JSON 返回格式。
  2. 创建一个类(自定义类名为:ResponseAdvice)实现 ResponseBodyAdvice 接口,重写其中的两个方法,一个是“开关”,另一个是用来对数据的格式进行校验和封装(注意:方法中的参数 body 若为 Spring 类型,比较特殊,需要使用 ObjectMapper 单独进行 JSON 格式处理)。
  3. 如果是微服务项目,那么把上述内容写到一个单独的模块中引入当前微服务即可.

具体的如下代码:

注意:@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);
    }
}

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,我明白了。对于你的问题,我可以这样回答: 利用 SpringMVC 拦截器可以控制 Controller 返回值。拦截器是一种 AOP 的技术,可以在请求处理过程中对请求进行拦截处理,如校验用户权限、记录请求日志等。在处理 Controller 返回值时,可以通过拦截器返回值进行处理,如对返回值进行加密、压缩等操作。 具体实现步骤如下: 1. 自定义一个拦截器类并实现 HandlerInterceptor 接口。 2. 在 Spring 配置文件中注册拦截器。 3. 在拦截器中重写 preHandle() 和 postHandle() 方法,分别在请求处理前和请求处理后进行拦截处理。 4. 在 postHandle() 方法中获取 Controller 返回值并进行处理,如加密、压缩等操作。 下面是一个简单的示例代码: ```java public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 在请求处理前进行拦截处理 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 在请求处理后进行拦截处理 if (modelAndView != null) { // 对返回值进行加密、压缩等操作 } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 在请求完成后进行拦截处理 } } ``` 在 Spring 配置文件中注册拦截器: ```xml <mvc:interceptors> <bean class="com.example.MyInterceptor"/> </mvc:interceptors> ``` 这样就可以利用 SpringMVC 拦截器控制 Controller 返回值了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈亦康

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值