SpringBoot 统一功能处理

在我们进行项目编写时,有时相同的一段代码在不同的地方使用多次,对于这种情况,spring 帮我们实现了统一功能处理,下面介绍一些常用的统一功能处理。

一、拦截器

在有些项目中,是需要用户登录才能进行后续访问的。但如果这时有人拿到了后端接口的 url,这时就能跳过用户登录进而访问我们的网站,这就有可能对网站造成攻击,对于这种情况,我们可以将用户的部分信息存到 session 中,在每次访问接口前,都可以先获取到 session,再将用户信息取出,看看这个用户是否登录,若登录,就允许继续访问,若没有登陆,就强制登录。

但是,在我们的项目中,接口的数目可不小,若是在每个接口中都加上这段逻辑,就会显得我们的代码过于冗余,于是 Spring 就为我们构造了拦截器。

拦截器主要用来拦截用户的请求,在指定方法前后执行拦截器中预设的代码,也就是说,拦截器可以在用户的请求之前发挥作用,也可以在用户的请求之后发挥作用。

对于上述判断用户登录的业务,我们就可以在拦截器中写明判断用户是否登录的代码,这样在用户发送请求时,就会先执行这段代码,若用户没有登录,就会阻止用户进行访问,并跳转到登陆页面(这是前端干的事)。

下面介绍拦截器的具体用法。

在实现拦截器时,需要实现 HandlerInterceptor 类中的 preHandle,postHandle,afterCompletion 方法,preHandle 是在目标方法执行前执行的,postHandle 是在目标方法执行之后执行的,afterCompletion 是在试图渲染完成之后执行的,但现在后端开发基本上都不会涉及到试图,那么这个方法就不重点介绍了。

代码如下:

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

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

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

preHandle 有返回值,返回 true 表示不拦截请求,返回 false 表示拦截请求。

定义好拦截器的功能后,接下来就需要注册拦截器。注册拦截器需要实现 WebMvcConfigurer 中的 addInterceptors,代码如下:

java 复制代码
@Slf4j
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test/**");
    }
}

使用 addInterceptor 即表示需要注册的是哪个拦截器,使用 addPathPatterns 即表示需要拦截的路径,即拦截器对哪些请求生效,这里的 /test/** 表示对 test 路径下的所有方法均生效,还有其它的路径表示,如下:

/* : 表示一级路径,只能匹配 /test,不能匹配 /test/t1等;

/** : 表示任意级路径,能匹配 /test,/test/t1等;

/test/* : 表示 /test 下的一级路径,可以匹配 /test/t1,但不能匹配 /test/t1/t2等;

/test/** : 表示 /test 下的任意级路径,可以匹配 /test/t1,/test/t1/t2,但不能匹配 /user/login 等。

当我们配置完拦截器和注册完拦截器后,就可以开始运行代码,我们使用下面的代码来进行测试:

java 复制代码
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

    @RequestMapping("/t1")
    public void t1() {
        log.info("拦截器测试...");
    }
}

代码运行结果如下:

在这里我们可以看到,当访问被拦截的方法时,首先执行的是 preHandle,由于 postHandle 返回值为 true,就不会拦截用户请求,于是就会先执行 t1 方法,之后会顺序执行 postHandle 和 afterCompletion。

若 preHandle 返回值为 false,代码的运行结果如下:

从结果中可以看出,程序只执行了 preHandle,由于 t1 被拦截,于是 t1、postHandle、afterCompletion 就不会执行。

在上述的拦截路径中,我们将 /test 下的所有路径都给拦截了,但是有的请求我们不需要拦截,那么我们就可以使用 excludPathPatterns 去去除掉不需要拦截的路径,改动后的代码如下:

java 复制代码
@Slf4j
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test/**")
                .excludePathPatterns("/test/t2");
    }
}


@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

    @RequestMapping("/t1")
    public void t1() {
        log.info("t1拦截器测试...");
    }

    @RequestMapping("/t2")
    public void t2() {
        log.info("不拦截t2测试");
    }
}

TestController 中新增了 t2 方法,只拦截 t1,不拦截 t2,代码运行结果如下:

从图中可以看出,t1被拦截,但 t2 没有被拦截。

当我们知道如何使用拦截器之后,就可以在项目中进行运用。

当用户在进行访问时,就可以对其访问进行拦截,在拦截器中判断用户是否登录,若登录就放行,若没有登录就跳转到登录页面进行登录,代码如下:

java 复制代码
package com.gjm.demo.interceptor;

import com.gjm.demo.constant.Constants;
import com.gjm.demo.enums.ResultStatusEnums;
import com.gjm.demo.model.Result;
import com.gjm.demo.model.UserInfo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/**
 * 登录拦截器
 */
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    /**
     * 目标方法前执行, 拦截器拦截到后,就会将response返回给前端,前端的error中就能就受到response
     * @param request
     * @param response
     * @param handler
     * @return true: 继续执行, false: 中断后续操作
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("登录之前执行");

        //用户未登录的处理
        if (!checkUserInfo(request.getSession())) {
            response.setContentType("text/html;character=utf-8");
            response.setStatus(401);
            String errorMessage = "用户未登录";
            response.getOutputStream().write(errorMessage.getBytes("UTF-8"));
            return false;
        }

        return true;
    }

    /**
     * 对检查用户信息进行封装
     * @param session
     * @return
     */
    public boolean checkUserInfo(HttpSession session) {
        //用户未登录
        if (session == null || session.getAttribute(Constants.SESSION_USER_KEY) == null) {
            return false;
        }

        //获取用户信息
        UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);

        //用户信息不对
        if (userInfo == null || userInfo.getId() <= 0) {
            return false;
        }

        return true;
    }
}

其中 check 的用处是检查用户是否登录。

二、统一数据返回格式

在进行项目开发时,我需要向前端返回一个统一的数据,这个数据是经过封装的,但是,在我们的接口中,有的返回的是 String,有的返回的是 boolean等,这就需要我们针对每一个接口都编写封装结果的代码,这样的代码就有点冗余了。于是,Spring 就封装了能够统一返回数据格式的方法。

现在有一个结果类 Result,对于接口中的每一种返回值,都需要将其封装为 Result 对象,实现的代码如下:

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        //false为不处理,true为处理
        return true;
    }

    @SneakyThrows//异常处理
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

        if (body instanceof String) {
            return objectMapper.writeValueAsString(Result.success(body));//将Result类型转化为String类型
        }

        //body已经是result类型,就不需要封装
        if (body instanceof Result) {
            return body;
        }

        return Result.success(body);

    }
}

该类需实现 ResponseBodyAdvice 接口,实现 support 和 beforeBodyWrite 方法,而且需要加上 @ControllerAdvice 注解。

support 的返回值为 boolean,返回 true 表示对各类型的返回值都进行处理,返回 false 表示不处理。

beforeBodyWrite 即具体的实现类。在这里需要强调,若接口中的返回值为 String,那么就需要对其进行单独处理,不然就会报错。

对于本来就是 Result 类型的对象,就不要进行处理,直接返回即可。

三、统一异常处理

在我们编写代码的时候,有时候可能判断的不准确,不知道哪里会出现异常,那么如果我们不进行处理,就会影响程序的运行。对于这种情况,Spring 为我们封装了一个能统一处理异常的方法,代码如下:

java 复制代码
@Slf4j
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {

    @ExceptionHandler
    public Result handler(Exception e) {
        log.error("发生异常, e: ", e);
        return Result.fail(ResultStatusEnums.ERROR, "内部错误,请联系管理员");
    }

    @ExceptionHandler
    public Result handler(NullPointerException e) {
        log.error("发生异常, e: ", e);
        return Result.fail(ResultStatusEnums.ERROR, "空指针异常,请联系管理员");
    }

    @ExceptionHandler
    public Result handler(IndexOutOfBoundsException e) {
        log.error("发生异常, e: ", e);
        return Result.fail(ResultStatusEnums.ERROR, "数组越界异常,请联系管理员");
    }

}

ExceptionAdvice 需要使用 @ControllerAdvice 和 @ResponseBody 注解。

在各个方法上需要使用 @ExceptionHandler 注解。

在这个类中,一共处理了三个异常,也可以继续增加异常的种类。若代码中抛出了异常并且没有手动捕获,就会抛给 ExceptionAdvice 类,在这个类中寻找与之距离最近的异常,若没有,就会直接使用 Exception 代替。

上述的代码还有另一种写法,代码如下:

java 复制代码
@Slf4j
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {

    @ExceptionHandler(Exception.class)
    public Result handler1(Exception e) {
        log.error("发生异常, e: ", e);
        return Result.fail(ResultStatusEnums.ERROR, "内部错误,请联系管理员");
    }

    @ExceptionHandler(NullPointerException.class)
    public Result handler2(Exception e) {
        log.error("发生异常, e: ", e);
        return Result.fail(ResultStatusEnums.ERROR, "空指针异常,请联系管理员");
    }

    @ExceptionHandler(IndexOutOfBoundsException.class)
    public Result handler3(Exception e) {
        log.error("发生异常, e: ", e);
        return Result.fail(ResultStatusEnums.ERROR, "数组越界异常,请联系管理员");
    }

}

在每个 @ExceptionHandler 注解中标明捕获的是什么类型的异常,这样在每个方法的参数中就能直接使用 Exception 代替。

相关推荐
AI智能科技用户7946329782 分钟前
okcc呼叫中心两个sip对接线路外呼任务怎么设置才能一个任务对应yigesip中继?
人工智能·后端
中国lanwp10 分钟前
Spring Boot 版本与对应 JDK 版本兼容性
java·开发语言·spring boot
懒虫虫~14 分钟前
Spring源码中关于抽象方法且是个空实现这样设计的思考
java·后端·spring
码银15 分钟前
【Java】接口interface学习
java·开发语言·学习
雷渊22 分钟前
DDD的分层架构是怎么样的?
后端
DKPT28 分钟前
重构之去除多余的if-else
java·开发语言·笔记·学习·面试
蓝黑202029 分钟前
Java如何在遍历集合时删除特定元素
java
会有猫30 分钟前
阿里云OSS挂载到Linux
后端
雷渊34 分钟前
聊一聊贫血模型和充血模型区别
后端
掘金詹姆斯34 分钟前
如何基于状态机对订单状态实现统一管理?
java·状态机