初始JavaEE篇 —— SpringBoot 统一功能处理

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页: 我要学编程程(ಥ_ಥ)-CSDN博客

所属专栏: JavaEE

目录

前言

拦截器

基本使用

拦截器的路径配置

统一数据返回格式

统一异常处理


前言

在实际开发中,某些功能需要强制用户登录才能使用。例如,csdn与博客园的点赞、收藏、评论,甚至于经常使用的抖音,每次打开界面如果不登录的话,就是提示你要登录(登录之后,就可以记录用户的使用啥操作....让一切操作有迹可循)。面对这种强制登录的情况,该如何实现呢?

对应到代码的话,就是下面:

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

    /**
     * 登录接口
     * @return true-登录成功,false-登录失败
     */
    @RequestMapping("/login")
    public boolean login(String username, String password) {
        // 检验参数是否正常
        if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return false;
        }
        // 检验账号密码是否正确
        if ("admin".equals(username) && "123456".equals(password)) {
            return true;
        }
        return false;
    }

    /**
     * 测试不登录是否可以访问该接口
     * @return
     */
    @RequestMapping("/testLogin")
    public boolean testLogin() {
        log.info("成功访问到testLogin接口");
        return true;
    }

}

我们前面学习的是通过Cookie-Session机制来判断用户是否登录。先来回顾一下流程:客户端首次登录时,会将用户名和密码通过HTTP请求发送到服务器,服务器验证正确之后,就会将该用户信息存储到session中,服务器就会将该session存储到本地,同时将session-id存储到HTTP响应头的set-Cookie中,当客户端接收到该请求之后,就会将set-Cookie中的session-id存储到本地,后续再次请求时,就会将session-id存储到HTTP请求头的Cookie中,这样服务器拿到Cookie中session-id之后,就会去本地查找对应的session信息,如果找到并且session并未过期,就会认为用户已经登录;反之,则会让用户重新登录。

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

    /**
     * 登录接口
     * @return true-登录成功,false-登录失败
     */
    @RequestMapping("/login")
    public boolean login(String username, String password, HttpSession session) {
        // 检验参数是否正常
        if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return false;
        }
        // 检验账号密码是否正确
        if ("admin".equals(username) && "123456".equals(password)) {
            // 设置session信息,检验后续访问其他接口时,判断用户是否登录
            session.setAttribute("user", username);
            return true;
        }
        return false;
    }

    /**
     * 测试不登录是否可以访问该接口
     * @return
     */
    @RequestMapping("/testLogin")
    public boolean testLogin(HttpServletRequest request) {
        // 检验用户是否登录
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("username") != null) {
            // 这里处理具体的逻辑
            log.info("成功访问到testLogin接口");
            return true;
        }
        return false;
    }

}

通过上述代码中的判断session信息是否正确的方式,可以来拦截一下并未登录的用户。

上述方式虽能正确处理强制用户登录的情况,但需要在每个接口的逻辑中都添加判断session信息是否存在,非常的麻烦。而今天,我们就来学习SpringBoot的统一功能处理。

拦截器

要实现上述统一拦截请求,并进行Session校验的流程就需要用到拦截器。那什么是拦截器呢?拦截器是Spring框架提供的核心功能之一,主要用来拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码。也就是说,允许开发人员提前预定义一些逻辑,在用户的请求响应前后执行,也可以在用户请求前阻止其执行。在拦截器当中,开发人员可以在应用程序中做一些通用性的操作,比如通过拦截器来拦截前端发来的请求,判断Session中是否有登录用户的信息,如果有就可以放行,如果没有就进行拦截。这样就避免了在每个方法内部重复编码的操作。

基本使用

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

1、定义拦截器;

2、注册拦截器;

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

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

注意:实现该接口之后,可以选择啥也不重写或者选择性重写部分方法。

默认重写完成之后,就是上述代码。

preHandle方法的含义是目标方法执行前执行,如果该方法的返回值为true的话,就代表当前方法放行;反之,则代表当前方法被拦截,就不能到达Controller层的逻辑了。

postHandle方法的含义是目标方法执行后执行(也就是HTTP请求响应返回前执行)。

afterCompletion()方法:视图渲染完毕后执行,这是最后执行的,但由于现在都是前后端分离开发,因此这里无需了解。

注意:这里的目标方法就是HTTP请求的资源路径对应的方法(并不是完整的URL),也就是Controller层@RequestMapping所对应的value值。

接下来,就是注册配置拦截器:

java 复制代码
@Configuration // 加上,是为了spring能够在初始化容器时,去执行下面的方法
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器并配置拦截路径(不拦截路径)
        LoginInterceptor loginInterceptor = new LoginInterceptor();
        // 方法需要的参数是拦截器对象
        // "/**" 表示拦截所有的请求
        registry.addInterceptor(loginInterceptor).addPathPatterns("/**");
    }
}

上述就是实现拦截器的基本框架,如果想要实现拦截未登录的用户的话,需要将 preHandle 方法内部的逻辑具体修改:

java 复制代码
// 拦截器
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    // 父类默认返回的是true,即对拦截的请求都放行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("目标方法执行前...");
        HttpSession session = request.getSession(false);
        // 检验用户是否登录,如果登录放行该请求;反之,则拦截改请求
        if (session != null && session.getAttribute("username") != null) {
            return true;
        }
        return false;
    }

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

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

接下来,测试未登录的用户是否可以访问该方法:

由此看来,当我们注册并配置了拦截器之后,同时在拦截器中实现了具体要拦截的请求是啥样时,Spring会准确无误的帮我们拦截该请求。

拦截器的路径配置

拦截器路径是指我们定义的拦截器,需要对哪些路径进行拦截。前面在注册拦截器时,我们是通过 addPathPatterns() 方法指定要拦截哪些路径下的请求,也可以通过 excludePathPatterns() 方法指定哪些请求不拦截。

|------------|---------------|----------------------------------------|
| 拦截路径 | 含义 | 说明 |
| /* | 一级路径 | 只能拦截 /login,而不能拦截 /user/login |
| /** | 任意级路径 | 能拦截 /login, /user/login |
| /user/* | /user 下的一级路径 | 只能拦截 /user/login,而不能拦截 /user/login/123 |
| /user/** | /user 下的任意级路径 | 能拦截 /user/login,/user/login/123 |

当拦截的是一级路径,我们来观察一级路径以及二级路径的访问情况:

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

    @RequestMapping("/login")
    public Boolean login(String username, String password, HttpSession session) {
        // 检验参数是否正常
        if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return false;
        }
        // 判断参数是否正确
        if ("admin".equals(username) && "123456".equals(password)) {
            // 存储用户信息到session
            log.info("用户登录成功...");
            session.setAttribute("username", username);
            return true;
        }
        return false;
    }

    @RequestMapping("/testLogin")
    public String testLogin(HttpServletRequest request) {
        log.info("用户已经访问该接口");
        // 检验用户是否登录
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("username") != null) {
            return "用户已经登录: "+session.getAttribute("username");
        }
        return "用户并未登录";
    }
}



@RestController
@Slf4j
public class LoginController2 {

    @RequestMapping("/testLogin2")
    public String testLogin2(HttpServletRequest request) {
        log.info("用户已经成功访问该接口");
        // 检验用户是否登录
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("username") != null) {
            return "用户已经登录: "+session.getAttribute("username");
        }
        return "用户并未登录";
    }
}

上面是访问二级路径的情况,我们再来看访问一级路径:

我们会发现,如果只要是经过了 拦截器的方法 都会是打印出 目标方法执行前 这个日志的,这也从侧面体现出 打印日志 的重要性。

至于其余的情况这里就不再演示了。

注意:如果我们在配置拦截器时,将任意级路径都拦截的话,不仅是后端接口会被拦截掉,连前端等静态资源也会被拦截。如下所示:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>测试静态资源是否会被拦截</title>
</head>
<body>
    <div style="color: orangered;">测试静态资源是否会被拦截</div>
</body>
</html>

启动项目之后,通过浏览器访问到对应的地址,然而我们会发现页面信息的展示是乱码,如果我们再通过 vscode 去打开对应的页面信息,发现可以正常显示:

这是由于配置的拦截器信息对 静态资源 也进行了拦截(/*),在 Spring Boot 中处理静态资源(比如.html,·js,·css,·png 等)时,默认是由ResourceHttpRequestHandler 处理的。这个处理器会自动根据文件后缀设置对应的 Content-Type。虽然设置了 Content-Type: text/html,但并没有去设置编码信息,因此浏览器会根据系统语言来自动解析编码,我们的Windows系统语言默认为中文,因此解码使用的是 GBK,即出现了乱码的情况。

如果我们把编码信息手动设置为 UTF-8 或者不拦截所有的一级路径的话,最终页面就可以正常显示。

接下来,继续访问:

因此得排除这些路径:

由于我们这里的静态资源只有 html 文件,因此只排出 html 文件,实际开发中,可能会出现多种静态资源文件。例如,html、css、js、jpg等。如果想要正常显示的话,都得进行排除。

统一数据返回格式

在前后端分离开发中,接口返回的数据格式设计至关重要。例如,对于登录接口,如果后端返回的数据格式不统一,前端在处理时会面临诸多不便。假设后端返回的数据格式为对象或JSON数据,前端需要通过对象的属性来判断登录是否成功。但如果后端直接返回布尔类型(Boolean),前端只需判断值是否为true即可。这仅仅是处理成功情况的对比。对于HTTP请求失败的情况,前端还需要根据后端的返回来处理错误,这也增加了前端的开发复杂性。在这种情况下,前端每次编写代码时都需要参考后端的具体实现,这无疑增加了开发成本和沟通成本。那么,前后端分离开发的意义何在呢?

因此,后端接口在返回数据时,必须设置一个统一的数据返回格式。这样,前端只需熟悉一次返回格式,即可高效地进行开发,大大减少重复劳动和沟通成本,真正发挥前后端分离开发的优势。

现在我们就可以对前面登录接口和测试登录接口来进行一个封装:

java 复制代码
@Data
public class Result {
    ResultCode code; // 业务响应码
    String message; // 业务信息
    Object data; // 响应的具体数据

    public static Result success() {
        Result result = new Result();
        result.setCode(ResultCode.success);
        result.setMessage("success");
        return result;
    }

    public static Result success(Object data) {
        Result result = new Result();
        result.setCode(ResultCode.success);
        result.setMessage("success");
        result.setData(data);
        return result;
    }

    public static Result error(String message) {
        Result result = new Result();
        result.setCode(ResultCode.client_error);
        result.setMessage(message);
        return result;
    }

    public static Result error() {
        Result result = new Result();
        result.setCode(ResultCode.server_error);
        result.setMessage("服务开小差了,请稍后再试...");
        return result;
    }
}



public enum ResultCode {
    success(200),
    client_error(400),
    server_error(500);

    private int code;

    private ResultCode(int code) {
        this.code = code;
    }
}

封装成上述对象之后,接口在返回数据时,就需要严格按照该对象来,这里就省略了。

接下来,测试上述封装是否成功(先把配置拦截器的代码注释掉):

如果要想把这里的code改成具体的数值的话,可以在 ResultCode 类中,加上下面的代码:

java 复制代码
    @JsonValue
    public int getCode() {
        return code;
    }

这表明在序列化时,将枚举对象序列化为对应的数字。 这个注解的作用:把枚举对象序列化时,使用 getCode() 返回的值。

上面的方式就是对返回结果进行了一个封装,但还是不够完美。如果每个接口返回的结果都需要进行封装的话,那也是比较麻烦的,因此Spring也提供了类似拦截器这样的方式,来让我们可以对返回结果进行统一处理。

1、自定类实现 ResponseBodyAdvice,并重写父类的 supports 和 beforeBodyWrite 方法;

2、将 supports 方法的内容,改为返回 true,接着在 beforeBodyWrite 方法内,使用统一数据返回对象来封装 body对象,再返回即可。

java 复制代码
@ControllerAdvice // 交由Spring管理
public class ResponseAdvice implements ResponseBodyAdvice {
    /**
     *
     * @param returnType the return type
     * @param converterType the selected converter type
     * @return false 表示不处理,true 表示处理
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        // 这里同样也是和拦截器的处理方式一样,都是可以使用 if 语句来进行判断是否需要进行处理的
        // 为了先观察结果,我们先统一设置返回true,即表示都进行处理。
        // 这里的处理是指是否去执行下面的方法
        return true;
    }

    /**
     *
     * @param body 表示返回的正文信息
     * @param returnType the return type of the controller method
     * @param selectedContentType the content type selected through content negotiation
     * @param selectedConverterType the converter type selected to write to the response
     * @param request the current request
     * @param response the current response
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 这里的Body表示HTTP请求需要返回的正文信息
        // 如果需要对响应数据进行统一处理的话,也是在这里进行响应数据的封装
        return Result.success(body); // 对 body 进行封装
    }
}

接着再次启动项目,使用 Postman 去访问测试登录接口:

之所以会出现封装了两层的现象,是因为我们前面已经将登录接口的返回对象进行了封装,而这里再次对 body 进行了一层封装。因此,我们可以先将前面封装的结果修改为最原始的状态,再次启动项目,访问接口观察:

发现服务器发生错误,我们再去看另外一个接口:

具体封装失败的原因,需要我们去看错误日志:

那为什么会出现需要将 Result 类型转换为 String 类型的情况呢?这是因为Postman(前端)请求的方法的返回值为 String 类型,但经过统一数据格式的处理之后,实际的返回类型变为 Result 类型,Spring 看到方法的返回类型是String,就默认用 StringHttpMessageConverter 去处理,但却遇到了一个非字符串对象,于是报错。而我们要做的就是将 Result对象 转换为 String对象,怎么做呢?很简单,通过 Spring 内置的 Jackson 将 Result对象 序列化为 JSON字符串,这下就可以兼容了。

java 复制代码
// 将 Result 对象序列化为 JSON数据
ObjectMapper objectMapper = new ObjectMapper();
try {
    return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
    throw new RuntimeException(e);
}

而当我们加上上述转换为 JSON 的处理之后,会导致所有的数据都会转换为 JSON,但实际上只需要 String 类型,因此我们可以先判断 是否为 String 类型的数据。

java 复制代码
 // 将 Result 对象序列化为 JSON数据
 if (body instanceof String) {
     ObjectMapper objectMapper = new ObjectMapper();
     try {
         return objectMapper.writeValueAsString(Result.success(body));
     } catch (JsonProcessingException e) {
         throw new RuntimeException(e);
     }
 }
 return Result.success(body); // 对 body 进行封装

注意:可能只有当方法的返回值为 String 时,才会出现上述情况,而当方法的返回值为 int 或者 Boolean等类型时,却不会出现上述情况呢?这是因为Spring在传输数据时,使用的JSON数据格式,其本质是字符串,因此当方法返回值为非 String 类型时,会自动转换为 JSON数据,Boolean类型或 int类型转换字符串,Result对象 转换为 JSON数据,在传输时,JSON数据和String类型的数据并无啥区别,但是对于方法的返回值为String类型的话,Spring就不会去转换为 JSON数据了,但是在准备传输时却发现不是String类型,于是强行转换为 String 就报错了。

统一异常处理

我们在编码时,对于可能抛出异常的代码需要手动进行 try-catch,但即使我们再细致,也可能会忽略某些异常的处理,倘若恰好这个异常信息直接导致了整个程序崩溃了,如果是在测试环境倒还好,就怕是发生在实际的生产环境中。因此,我们接下来要学习如何对异常进行统一处理。

自定类异常处理类,加上 @ControllerAdvice、@ResponseBody,定义异常处理方法,加上@ExceptionHandler,方法内部定义异常处理逻辑。

java 复制代码
@ControllerAdvice // 交由Spring管理
@ResponseBody // 返回的具体数据,而不是页面
public class ExceptionAdvice {

    @ExceptionHandler // 处理异常
    public Result handleException(Exception e) {
        // 对所有的异常进行返回
        return Result.error(e.getMessage());
    }
}



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

    /**
     * 下面是常见的异常,看是否可以捕获到
     */

    // 除0异常
    @RequestMapping("/t1")
    public Integer test1() {
        int a = 10 / 0;
        return a;
    }

    // 数组越界异常
    @RequestMapping("/t2")
    public Boolean test2() {
        int[] array = new int[10];
        int a = array[11];
        return a > 0;
    }

    // 空指针异常
    @RequestMapping("/t3")
    public String test3() {
        int[] array = null;
        int len = array.length;
        return array.toString();
    }
}

接下来就需要对上述代码进行测试,观察是否捕获到了异常信息:

都是能够正常捕获的,这里就只给出 test1 方法的测试结果。

但在实际开发中,可以异常的处理也分的非常细致,需要我们对不同的异常信息,对前端或者直接打印在控制台的信息中,发送不同的错误信息。

java 复制代码
@Slf4j
@ControllerAdvice // 交由Spring管理
@ResponseBody // 返回的具体数据,而不是页面
// 也可以使用 @RestControllerAdvice 代替上面两个注解
public class ExceptionAdvice {

    @ExceptionHandler // 处理异常
    public Result handleException(Exception e) {
        log.error(e.getMessage());
        return Result.error("服务器内部错误,请联系管理员...");
    }


    @ExceptionHandler // 处理异常
    public Result handleException(NullPointerException e) {
        log.error(e.getMessage());
        return Result.error("空指针异常,请联系管理员...");
    }


    @ExceptionHandler // 处理异常
    public Result handleException(ArrayIndexOutOfBoundsException e) {
        log.error(e.getMessage());
        return Result.error("数组越界异常,请联系管理员...");
    }
}

在捕获异常时,和我们前面学习的 try-catch 并不是一样的,try-catch的方式是从上往下依次去捕获异常信息,如果遇到了 父类的话,就直接走父类的处理流程了,用一句话概括的话,就是谁先catch到了,就走谁的流程,但是这里在捕获异常时,是优先去匹配与当前异常信息一致的异常,其次再是匹配度稍低的异常。如果实在是匹配不到的话,就会先把异常抛出去。因此一般我们都会定义一个顶级Exception来处理预料之外的情况。

在处理详细的异常时,有两种方式:

1、在参数列表中指定异常信息的种类;

2、为@ExceptionHandler注解的value赋值为具体的异常类

上述代码使用的是第一种方式,接下来,我们使用第二种方式:

java 复制代码
@Slf4j
@ControllerAdvice // 交由Spring管理
@ResponseBody // 返回的具体数据,而不是页面
public class ExceptionAdvice {

    @ExceptionHandler(Exception.class) // 处理异常
    public Result handleException(Exception e) {
        log.error(e.getMessage());
        return Result.error("服务器内部错误,请联系管理员...");
    }


    @ExceptionHandler(NullPointerException.class) // 处理异常
    public Result handleException2(Exception e) {
        log.error(e.getMessage());
        return Result.error("空指针异常,请联系管理员...");
    }


    @ExceptionHandler(ArrayIndexOutOfBoundsException.class) // 处理异常
    public Result handleException(Exception e) {
        log.error(e.getMessage());
        return Result.error("数组越界异常,请联系管理员...");
    }

}

注意:当通过注解的方式来表示是那个异常信息之后,参数列表内部的异常信息就会失效,也就是说Spring会优先根据注解去捕获异常信息,而不是参数列表。

好啦!本期 初始JavaEE篇 ------ SpringBoot 统一功能处理 的学习之旅 就到此结束啦!我们下一期再一起学习吧!

相关推荐
Hanson Huang1 小时前
【数据结构】堆排序详细图解
java·数据结构·排序算法·堆排序
慕容静漪1 小时前
如何本地安装Python Flask并结合内网穿透实现远程开发
开发语言·后端·golang
ErizJ1 小时前
Golang|锁相关
开发语言·后端·golang
路在脚下@1 小时前
Redis实现分布式定时任务
java·redis
xrkhy1 小时前
idea的快捷键使用以及相关设置
java·ide·intellij-idea
巨龙之路1 小时前
Lua中的元表
java·开发语言·lua
烛阴2 小时前
手把手教你搭建 Express 日志系统,告别线上事故!
javascript·后端·express
良许Linux2 小时前
请问做嵌入式开发C语言应该学到什么水平?
后端
Pitayafruit2 小时前
SpringBoot整合Flowable【08】- 前后端如何交互
spring boot·后端·workflow
花花鱼2 小时前
itext7 html2pdf 将html文本转为pdf
java·pdf