SpringBoot统一功能处理

拦截器

拦截器快速入门

什么是拦截器?

拦截器是Spring框架提供的核心功能之一, 主要用来拦截用户的请求, 在指定方法前后, 根据业务需要执行预先设定的代码.

也就是说, 允许开发人员提前定义一些逻辑, 在用户的请求响应前执行. 也可以在用户请求前阻止其执行. 就比如我们要通过一个url访问一个页面, 但是这个页面只有登录后才能访问, 这时就需要拦截, 比如你们可以试着登入这个url: CSDN.

然后就会被传送到这:

在拦截器当中, 开发人员可以在应用程序中做一些通用性的操作, 比如通过拦截前端发来的请求, 判断Session中是否有登陆用户的信息. 如果有就可以放行, 如果没有就进行拦截.

比如抗日战争中, 想要在沦陷区进城, 就要向"太君"出示良民证(session),

出示了就可以放行, 放行之后还可能会有其它太君来对你搜身;

没有出示"太君"就会大大咧咧地骂"八嘎!你滴, 滚远地干活".

下面我们先来学习一下守城太君, 哎不对, 拦截器的基本使用.

拦截器的使用步骤分为两步:

1.定义拦截器(这个拦截器是用来干什么的, 比如守城太君不能让没带良民证的人进)

2.注册配置拦截器(拦截器要怎么做, 守城太君要查良民证)

**自定义拦截器:**实现HandlerInterceptor接口, 并重写其所有方法.

java 复制代码
@Slf4j
@Component
public class LoginInterceptorTest implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("LoginInterceptor 目标方法执行前执行..");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("LoginInterceptor 目标方法执行后执行..");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("LoginInterceptor 视图渲染完毕后执行, 最后执行");
    }
}

preHandle()方法: 目标方法执行前执行, 返回true: 继续执行后续操作; 返回false: 中断后续操作.

postHandle()方法: 目标方法执行后执行.

afterCompletion()方法: 视图渲染完毕后执行, 最后执行(后端开发现在几乎不涉及视图, 暂不了解).

**注册配置拦截器:**实现WebMvcConfigurer接口, 并重写addInterceptors方法.

java 复制代码
@Configuration
public class WebConfigTest implements WebMvcConfigurer {
    //自定义的拦截器对象
    @Autowired
    private LoginInterceptorTest loginInterceptorTest;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册自定义的拦截器对象
        registry.addInterceptor(loginInterceptorTest)
                .addPathPatterns("/**"); //设置拦截器拦截的请求路径(/**表示拦截所有路径)
    }
}

启动服务, 访问任意请求, 观察后端日志:

可以看到preHandle方法执行之后就放行了, 开始执行目标方法, 目标方法执行完成之后执行postHandle和afterCompletion方法.

我们把拦截器中的preHandle方法的返回值改为false, 再观察运行结果:

可以看到, 拦截器拦截了请求, 没有进行响应.

拦截器详解

拦截器的入门程序完成之后, 接下来我们来介绍拦截器的使用细节. 拦截器的使用细节我们主要介绍两个部分:

1.拦截器的拦截路径配置

2.拦截器的实现原理

拦截路径

拦截路径是指我们定义的这个拦截器, 对哪些请求生效.

我们在注册配置拦截器的时候, 通过addPathPatterns()方法指定要拦截哪些请求. 也可以通过excludePathPatterns()指定不拦截哪些请求.

在拦截器中除了/**表示拦截所有资源之外, 还有一些常见的拦截路径设置:

|------------|--------------|-----------------------------------------------------------|
| 拦截路径 | 含义 | 举例 |
| /* | 一级路径 | 能匹配/user, /book, /login, 不能匹配/user/login |
| /** | 任意级路径 | 能匹配/user, /user/login, /user/reg |
| /book/* | /book下的一级路径 | 能匹配/book/addBook, 不能匹配/book/addBook/1, /book |
| /book/** | /book下的任意级路径 | 能匹配/book, /book/addBook, /book/addBook/2, 不能匹配/user/login |

以上拦截规则可以拦截此项目中使用的URL, 包括静态文件.

拦截器的执行流程

正常的调用顺序:

有了拦截器之后就是这样的流程:

1.添加拦截器后, 执行Controller方法之前, 请求会先被拦截器拦截住. 执行preHandle()方法, 这个方法需要返回一个布尔类型的值. 如果返回true, 就表示放行本次操作, 继续访问controller中的方法. 如果返回false, 就不会放行. (controller中的方法也不会执行).

2.controller当中的方法执行完毕之后, 再回来执行postHandle()这个方法以及afterCompletion()方法, 执行完毕后, 最终给浏览器响应数据.

适配器模式

适配器模式定义

适配器模式, 也叫包装器模式. 将一个类的接口, 转换成客户希望的另一个接口, 适配器让原本接口不兼容的类可以合作无间.

简单来说就是目标类不能直接使用, 通过一个新类包装一下, 适配调用方使用. 把两个不兼容的接口通过一定的方式使之兼容.

比如下面两个接口, 本身是不兼容的(参数类型不一样, 参数个数不一样等等).

可以通过适配器, 使之兼容:

比如博主在大一时, 天气热了, 需要空调, 最后我们合资买了一个, 但是最后才知道学校规定, 必须要在指定平台租, 否则就不给办空调插口的电卡(非常sb的二求规定), 由于其它插口功率不高,不能直接给空调用, 我最后想了一个绝妙点子, 买了个功率转换头, 这里就可以通过其它非空调插口的插口直接使用. 这里这个功率转换头就是适配器.

适配器模式角色

Target: 目标接口(可以是抽象类或接口), 用户希望直接用的接口.(非空调插口的其它插口)

Adapee: 适配者, 但是与Target不兼容(空调)

Adapter: 适配器类, 通过继承或者引用适配者的对象, 把适配者转为目标接口.(功率转换头)

client: 需要使用适配器的对象.(劳累了一天非常热的灰灰)

适配器使用场景

一般来说, 适配器模式可以看作一种"补偿模式", 用于补救设计上的缺陷. 这种模式的应用算是"吾乃治具", 如果设计初期, 我们就能协调规避接口不兼容的问题, 就不需要使用适配器模式了.

所以适配器模式更多的应用场景主要是对正在运行的代码进行改造, 并且可以复用原有代码实现新功能.

统一数据返回格式

**统一数据返回格式是指在软件开发中约定一种统一的数据结构格式, 用于向客户端或者调用方返回数据.**这种格式通常包含固定的字段, 以便客户端能够统一地解析和处理返回的数据, 从而提高开发效率和降低沟通成本.

快速入门

统一的数据返回格式使用@ControllerAdvice和ResponseBodyAdvice的方式实现@ControllerAdvice表示控制器通知类.

添加类ResponseAdvice, 实现ResponseBodyAdvice接口, 并在类上添加@ControllerAdvice注解.

java 复制代码
@ControllerAdvice
public class ResponseAdviceTest implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectContentType,
                                  Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return Result.success(body);
    }
}

supports方法: 判断是否要执行beforeBodyWrite方法. true为执行, false不执行. 通过该方法可以选择哪些类或哪些方法的response要进行处理, 其它的不进行处理.

beforeBodyWrite方法: 对response方法进行具体操作处理.

存在问题

这里就直接开门见山: 首先提供以下代码:

java 复制代码
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        return "t1";
    }

    @RequestMapping("/t2")
    public boolean t2() {
        return true;
    }

    @RequestMapping("/t3")
    public Integer t3() {
        return 200;
    }
}

将以上代码的每一个入口都走一遍(先走t2t3,最后t1), 我们发现:

当返回类型为String时, 这里报错了.

结论: 当返回类型为String时, 就会发生错误.

解决方案:

java 复制代码
@ControllerAdvice //这时一个用于定义全局控制器通知的注解
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    //用于java对象和json格式之间的转换
    private ObjectMapper objectMapper;

    @Override
    //supports方法: 判断是否要执行beforeBodyWrite方法, true为执行, false为不执行.
    //通过该方法可以选择哪些类或哪些方法的response要进行处理, 其它的不处理
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    //该方法用于实际处理响应体
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        //检查是否是Result类型, 如果是的话,则直接返回, 因为已经是统一的格式了.
        if(body instanceof Result) {
            return body;
        }
        //如果返回结果为String类型, 使用内部的SpringBoot内置提供的Jackson来实现信息的序列化
        if(body instanceof String) {
            return objectMapper.writeValueAsString(Result.success(body));
        }
        //如果既不是Result类型又不是String类型, 就将其包装成Result.success(body)的形式返回
        return Result.success(body);
    }
}

再次运行返回为String的接口, 正常.

优点

1.方便前端程序员更好地接受和解析后端数据接口返回的数据

2.降低前端程序员和后端程序员沟通成本, 按照某个格式实现即可, 因为所有的接口都是这样返回的

3.有利于项目统一数据的维护和修改.

4.有利于后端技术部门的统一规范的标准制定, 不会出现稀奇古怪的返回内容.

统一异常处理

统一异常处理使用的是@ControllerAdvice + @ExceptionHandler来实现的, @ControllerAdvice表示控制器通知类, @ExeptionHandler是异常处理器, 两个结合表示当出现异常时执行某个通知, 也就是执行某个方法事件.

具体执行如下:

java 复制代码
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {
    @ExceptionHandler
    public Object handler(Exception e) {
        return Result.fail(e.getMessage());
    }
}

类名, 方法名和返回值都可以自定义, 重要的是注解.

接口返回为数据时, 需要加@ResponseBody注解.

以上代码表示, 如果代码出现Exception异常(包括Exception的子类), 就返回一个Result对象, Result对象的设置参考Result.fail(e.getMessage()).

java 复制代码
    public static Result fail(String msg) {
        Result result = new Result<>();
        result.setErrMsg(msg);
        result.setCode(ResultStatus.FAIL);
        return result;
    }

我们可以针对不同的异常, 返回不同的结果:

java 复制代码
@Slf4j
@ResponseBody
//控制器通知类
@ControllerAdvice
//统一处理异常
public class ExceptionAdvice {

    @ExceptionHandler
    public Result handerException(Exception e) {
        log.error("发生异常, e:()", e);
        return Result.fail("内部错误");
    }

    @ExceptionHandler
    public Result handlerException(NullPointerException e) {
        log.error("发生异常: e", e);
        return Result.fail("发生空指针异常");
    }

    @ExceptionHandler
    public Result handlerException(ArithmeticException e) {
        log.error("发生异常, e:", e);
        return Result.fail("发生算术异常");
    }
}

模拟制造异常:

java 复制代码
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        return "t1";
    }

    @RequestMapping("/t2")
    public boolean t2() {
        int a = 10 / 0;  // 抛出ArithmeticException
        return true;
    }

    @RequestMapping("/t3")
    public Integer t3() {
        String a = null;
        System.out.println(a.length()); // 抛出NullPointerException
        return 200;
    }
}

运行t2:

运行t3:

相关推荐
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei2 小时前
java的类加载机制的学习
java·学习
码农小旋风3 小时前
详解K8S--声明式API
后端
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7894 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet