多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

JavaEE进阶:SpringBoot统一功能处理

JavaEE进阶:SpringBoot统一功能处理 一.拦截器在上一章我们完成了强制登入这个功能,后端可以通过session来判断用户是否登入,但是实现方法是比较麻烦的,需要修改每个接口的处理逻辑和返回结果,接口定义修改,前端也需要跟着修改.所以在这里,我们学习一种新的解决办法,拦截器,用来统一拦截所有请求,并进行session校验.1.1 拦截器快速入门什么是拦截器(interceptor)?拦截器是spring框架提供的核心功能之一,主要是用来拦截用户的请求,在执行制定方法前后,根据业务需要执行预先设定的代码.下面我们先来学习下拦截器的基本使用.使用拦截器分为两步:定义拦截器和注册配置拦截器.自定义拦截器: 实现HandlerInterceptor接口,并重写其所有方法.HandlerInterceptor是SpringMVC 提供的拦截器接口作用于 Controller 请求执行前后对接口请求做拦截、预处理、后置处理。preHandle () 方法目标方法执行前执行。返回 true: 继续执行后续操作返回 false: 中断后续操作.postHandle () 方法目标方法执行后执行afterCompletion () 方法视图渲染完毕后执行最后执行 (后端开发现在几乎不涉及视图暂不了解)Slf4j Component public class LoginInterceptor 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视图渲染完毕后执行,最后执行...); } }这里我们已经重写了HandlerInterceptor的方法,接着注册配置拦截器.注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptor方法.Configuration public class WebConfig implements WebMvcConfigurer { //自定义拦截器 Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { //注册自定义拦截器对象 registry.addInterceptor(loginInterceptor) .addPathPatterns(/**);//设置拦截器拦截的请求路径,/**表示拦截所有请求. } }启动服务,试试访问任意请求,观察后端日志.这里我们可以看到我们在登入的时候,会先执行preHandle,然后再执行登入方法,最后执行postHandle和afterCompletion方法.我们把preHandle方法的返回值改为false,再观察运行结果.这里可以看到拦截器拦截了请求,没有进行响应.1.2 拦截器详解拦截器的入门程序完成之后,接下来我们来介绍一下拦截器的使用细节,分别是拦截器的拦截路径配置和拦截器的实现原理1.2.1 拦截路径拦截路径是指我们定义的这个拦截器,对哪些请求生效.我们在注册配置拦截器的时候,通过addPathPatterns()方法指定要拦截哪些请求.也可以通过excludePathPatterns()指定不拦截哪些请求.在上述代码中,我们配置的是/**,表示对所有路径生效.此时我们可以看到无论我们用什么接口都会经过拦截器.我们想要的是拦截器可以对除了登入以外的所有路径生效.在拦截器中,除了可以设置/**拦截所有资源以外,还有一些常见的拦截路径的设置.1.2.2 拦截器执行流程在没有拦截器的情况下,用户的正常调用顺序:有了拦截器之后,在调用Controller层之前,会进行拦截处理.添加拦截器后执行 Controller 的方法之前请求会先被拦截住。执行 preHandle () 方法这个方法需要返回一个布尔类型的值。如果返回 true, 就表示放行本次操作继续访问 controller 中的方法。如果返回 false则不会放行 (controller 中的方法也不会执行).controller 当中的方法执行完毕后再回过来执行 postHandle () 这个方法以及 afterCompletion () 方法执行完毕之后最终给浏览器响应数据.1.3 登入校验在学习拦截器的基本操作之后,我们需要来完成最后一步操作.通过拦截器来完成图书管理系统中的登入校验功能.1.3.1 定义拦截器我们需要从session中获取用户信息,如果session中不存在,则返回false,并将http状态码设置为401,否则返回true.Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); if(session ! null session.getAttribute(Constants.SESSION_USER_KEY) ! null){ return true; } response.setStatus(401); return false; }1.3.2 注册配置拦截器public class WebConfig implements WebMvcConfigurer { //自定义拦截器 Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { //注册自定义拦截器对象 registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/user/login)//排除登入界面 .excludePathPatterns(/**/*.js)//排除前端资源 .excludePathPatterns(/**/*.css) .excludePathPatterns(/**/*.png) .excludePathPatterns(/**/*.html); } }此时我们把之前写过的登入校验代码删除.RequestMapping(/getBookListByPage) public Result getBookListByPage(PageInfo pageinfo, HttpSession ssession){ log.info(获取图书列表: pageinfo); //判断用户是否登入 // if(ssession.getAttribute(Constants.SESSION_USER_KEY) null){ // log.info(用户未登入); // return Result.unlogin(); // } UserInfo userInfo (UserInfo) ssession.getAttribute(Constants.SESSION_USER_KEY); // if (userInfo null || userInfo.getId() null ||userInfo.getId() 0||.equals(userInfo.getUserName())){ // log.info(用户信息错误); // return Result.unlogin(); // } PageResultBookInfo list bookService.getBookListByPage(pageinfo); log.info(用户: userInfo.getUserName() 获取图书列表); return Result.success(list); }再次运行程序,通过postman来测试.查看图书列表:可以看到在没登入的时候状态码为401,并且被拦截了.然后我们先登入,再查看图书列表.此时就可以成功拿到数据.1.4 DispatcherServlet源码分析(了解)通过观察我们的服务启动日志.我们可以看到,在tomcat启动之后,有一个核心的类DispatcherServlet,他来控制程序的执行顺序.现在要了解源码还是有些困难,简单来说,DispatcherServlet是springMVC的总调度员,Tomcat负责接收请求,DispatcherServlet负责把请求分发给正确的controller.所有的请求都会先进行到DispatcherServlet,执行doDispatch调度方法,如果有拦截器,会先执行拦截器的preHandle()方法的代码.如果返回true,继续访问controller中的方法,controller中的方法执行完毕后,再返回来执行postHandle()和afterCompletion()方法,最后返回给DispatcherServlet,最终给浏览器响应数据.1.4.1 初始化(了解)1.4.2 处理请求(核心)DispatcherServlet接收到请求后,执行doDispatch调度方法,再将请求转给Controller.我们看一下doDispatch方法的具体实现.我们不需要全部看懂这段源码,简单来说流程就是:这里提到了HandlerAdapter,使用的是适配器模式,下面会详细介绍.HandlerAdapter的作用是把“怎么调用 Controller”这件复杂的事情封装起来让 DispatcherServlet 只需要调用统一的handle()。1.4.3 适配器模式HandlerAdapter 在SpringMVC中使用了适配器模式适配器模式也叫包装器模式。将一个类的接口转换成客户期望的另一个接口适配器让原本接口不兼容的类可以合作无间。简单来说就是目标类不能直接使用通过一个新类进行包装一下适配调用方使用。把两个不兼容的接口通过一定的方式使之兼容。之前我们使用slf4j就是用的是适配器模式,slf4j提供了一系列的打印日志的api,底层调用的是log4j或者logback来打印日志,但是我们作为调用者,只需要调用slf4j的api就可以了.可以看出,我们不需要改变log4j的api,只需要通过适配器转换,就可以更换日志框架,保障系统的平稳运行.一般来说适配器模式可以看作一种 补偿模式用来补救设计上的缺陷。应用这种模式算是 无奈之举如果在设计初期我们就能协调规避接口不兼容的问题就不需要使用适配器模式了所以适配器模式更多的应用场景主要是对正在运行的代码进行改造并且希望可以复用原有代码实现新的功能。比如版本升级等。二.统一数据返回格式在强制登录案例中,我们一共做了两部分的工作.1.通过session来判断用户是否登入.2.对后端返回数据进行封装,告知前端处理的结果.拦截器帮我们实现了第一个功能,接下来看SpringBoot对第二个功能如何支持.2.1 快速入门统一的数据返回格式使用ControllerAdvice和ResponseBodyAdvice的方式实现ControllerAdvice表示控制器通知类.添加类ResponseBodyAdvice,实现ResponseBodyAdvice接口,并在类上添加ControllerAdvice注解.Slf4j ControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice { Override public boolean supports(MethodParameter returnType, Class converterType) { return true; //返回true表示全部处理 } Override public Object beforeBodyWrite(Nullable Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { log.info(执行beforeBodyWrite); return Result.success(body); } }supports方法表示对那些方法进行处理,返回true表示全部都处理.beforeBodyWrite方法表示的是在返回body前做的处理.其中body就是方法返回的内容,这里我们再包装一层result.这个是没有进行统一结果处理的时候,只返回一个true.进行统一结果处理:此时就把true包装成Result类了.我们再测试其他接口.获取图书列表接口,这里我们就发现了问题,因为我们之前已经对图书列表进行了Result类的包装,所以这里我们不希望再次进行包装,后续需要单独进行处理.测试删除图书接口:这里又发现一个问题,我们先看看数据库结果是否更改.可以看到这里数据库中id为1的图书状态已经改为0,所以说删除图书已经生效,让我们看看报错信息.可以发现,报错信息大致说的是Result不能匹配String.这里我们进行几个测试.测试t1t2:t3:t4:有兴趣的也可以去测试一下别的类型,结果就是只有返回类型为String的时候会进行报错.2.2 存在问题1.会对已经包装过的返回结果再次进行包装.这里我们需要在beforeBodyWrite方法中进行判断,如果结果已经是Result类型直接返回即可.再次测试图书列表接口.不会被多次包装.2.当返回类型为String类型时会报错.当body为String类型的时候,我们需要转换为Json.此时测试删除图书接口,就正常了.但是此时这个结果是一个字符串,我们还需要转换为JSON类型.在注解后面添加produce转换为JSON格式.再次运行.结果就是JSON格式了.我们需要在返回类型为String的接口都加上.这么看String类型还是需要一个一个手动加有点麻烦,所以说我们在开发的时候还是尽量避免使用string类型.前端处理起来比较麻烦.2.3 优点1.方便前端程序员更好的接收和解析后端数据接口返回的数据2.降低前端程序员和后端程序员的沟通成本按照某个格式实现就可以了因为所有接口都是这样返回的。3.有利于项目统一数据的维护和修改。4.有利于后端技术部门的统一规范的标准制定不会出现稀奇古怪的返回内容。三.统一异常处理统一异常处理使用的是 ControllerAdvice ExceptionHandler 来实现的 ControllerAdvice表示控制器通知类 ExceptionHandler 是异常处理器两个结合表示当出现异常的时候执行某个通知也就是执行某个方法事件添加图书,漏掉一个publish参数,应该报错,但是这边给前端的code还是200,这样就不合理了.出现这种情况的原因,是因为我们在这边对结果统一用的success进行包装,所以接下来我们要对这种情况进行处理.当我们发现接口中出现异常情况,就不应该使用success进行包装,要对不同的异常进行不同处理.这里我们先小试一下,创建Exception.class文件,然后通过ControllerAdvice来处理所有Controller抛出的异常,ExceptionHandler是用来对不同异常分别进行处理.我们之前已经了解过不同异常,这里我们先写个笼统的Exception,然后做个测试.测试一下t1,这里会发生异常.看看能不能进行正确处理可以看到这边处理结果和预想一样尝试去除统一异常处理,再次测试.再次运行t1.就还是会出现之前不合理的情况.这个是对所有的异常进行一个统一处理,我们还可以细分异常的各种类型.比如数组越界异常,空指针异常等等.接下来我们来测试其他异常分别运行t2,t3,t1至于前端代码我们只需要修改一下对应的接口就可以正常运行了.有关SpringBoot统一功能处理相关的内容就讲到这里了.
返回列表