0


SpringBoot统一功能处理

    上一篇博客使用了拦截器,强制登录案例做了两步:1、通过Session来判断用户是否登录;2、如果未登录,对后端返回数据进行封装,告知前端处理的结果。

    但是因为刚开始的图书管理系统的接口返回的不是Result类,我们现在想**把后端接口返回的类型都改成Result**,那么就可以使用SpringBoot的第二个统一功能处理。

一、统一数据返回格式

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

    **添加类 ResponseAdvice,实现 ResponseBodyAdvice 接口,并在类上添加 @ControllerAdvice 注解**,代码如下:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @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) {
        return Result.success(body);
    }
}

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

beforeBodyWrite() 方法:对 response 方法进行具体操作处理。

测试

    测试接口:http://127.0.0.1:8080/book/queryBookById?bookId=1

    添加统一数据返回格式之前:

    添加统一数据返回格式之后:

    可以看到,返回的数据从 **BookInfo 对象变为 Result 对象**,是 SpringBoot 帮我们处理了返回的数据格式。

二、存在问题

    现在测试 updateBook 接口

    URL:http://127.0.0.1:8080/book/updateBook?id=1&count=99

    可以看到,发生了内部错误,再看看数据库的数据,确发生改变了。

    **后端报错日志**:不能把 Result类 转换成 String类 ![](https://i-blog.csdnimg.cn/direct/ffd27febf1b849a0ab2aeaa2be15fd50.png)

    说明是 SpringBoot 帮我们处理数据返回的格式,原本返回的Result类型转为String类型有问题。

    现在再多测几种不同的返回类型,看是否会报错,代码如下:
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        return "string";
    }
    @RequestMapping("t2")
    public Integer t2() {
        return 1;
    }
    @RequestMapping("/t3")
    public Boolean t3() {
        return true;
    }
}
    测试上面几个接口:

    t1报错了,报错原因和上面的一样。

    t2、t3 不会报错,如图:

解决方案

    添加一行判断语句,如果返回的数据 body 是 String类型,就使用SpringBoot内置提供的 Jackson 来实现信息的序列化。
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    ObjectMapper objectMapper;
    @Override
    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) {
        if(body instanceof  String){//如果返回结果为String类型,使用SpringBoot内置提供的Jackson来实现信息的序列化
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}
    测试:使用更新图书的接口,如图,返回的是Result类型,没有报错了

案例代码修改

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    ObjectMapper objectMapper;
    @Override
    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) {
        //使返回的结果更加灵活
        if(body instanceof Result) {//如果返回结果为Result类型,则直接返回数据
            return body;
        }
        if(body instanceof  String){//如果返回结果为String类型,使用SpringBoot内置提供的Jackson来实现信息的序列化
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

三、统一功能的优点

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

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

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

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

四、统一异常处理

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

    代码如下:
@ResponseBody
@ControllerAdvice
public class ExceptionAdvice {
    @ExceptionHandler
    public Result handlerException(Exception e) {
        return Result.fail(e.getMessage());
    }
}
    其中返回对象也可以写成 Object,代码如下:
@ResponseBody
@ControllerAdvice
public class ExceptionAdvice {
    @ExceptionHandler
    public Object handlerException(Exception e) {
        return Result.fail(e.getMessage());
    }
}
    类名,方法名和返回值可以自定义,**重要的是注解,接口返回为数据时,需要加 @ResponseBody 注解**。

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

    我们现在修改一下后端代码,把后端删除图书的mapper层xml文件的接口改错,如图:

    现在访问URL:http://127.0.0.1:8080/book/deleteBook?bookId=14 ,返回的信息如图:

    其中 **加@ResponseBody 注解非常重要,下面使用 fiddler 抓包看看加和不加的区别**:

    我们也可以**针对不同的异常,返回不同的结果**,代码如下:
@Slf4j
@ResponseBody
@ControllerAdvice
public class ExceptionAdvice {
    @ExceptionHandler
    public Result handlerException(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("发生算数异常");
    }
}
    模拟制造异常,代码如下:
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1() {
        return "string";
    }
    @RequestMapping("t2")
    public Integer t2() {
        int a = 10 / 0;//抛出ArithmeticException
        return 1;
    }
    @RequestMapping("/t3")
    public Boolean t3() {
        String a = null;
        System.out.println(a.length());//抛出NullPointerException
        return true;
    }
}
    当有多个异常通知时,**匹配顺序为当前类及其子类向上依次匹配**,/test/t2 抛出 ArithmeticException算术异常,/test/t3 抛出NullPointerException 空指针异常。

    测试上面这上面的算术异常和空指针异常的接口,如图:

    **当异常和子类异常不匹配时,就会去找父类异常(优先捕获子类异常,再捕获父类异常)**,下面输入一个错的URL,显示信息如下:

    ![](https://i-blog.csdnimg.cn/direct/18b36c0c5c2649c1ba6f80b83985d68b.png)

五、案例代码(图书管理系统)

    因为上面使用了统一功能处理(统一了数据返回的格式:Result;统一了异常的处理),所以**所有接口返回的数据类型都改变了,相对前端的接口也要进行修改**,其中图书链表页面已经修改好了(book_list.html)

1、login.html

    稍微修改一下后端的代码,原本错误信息是放进data里的,现在改成放进errMsg里面,代码如下:
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @RequestMapping(value = "/login", produces = "application/json")
    public Result login(String userName, String password, HttpSession session) {
        //1、校验参数
        //2、校验密码是否正确

        //3、返回响应结果
        System.out.println(userName + " " + password);
        if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
            return Result.fail("用户名或者密码为空");
        }

        UserInfo userInfo = userService.getUserInfoByName(userName);

        if(userInfo == null) {
            return Result.fail("用户不存在");
        }
        if(!password.equals(userInfo.getPassword())) {
            return Result.fail("密码错误");
        }

        //根据用户名称,去数据库查询用户信息,如果未查询到,说明用户不存在
        //如果查询到用户信息,比对密码是否正确
        //正确的情况
        session.setAttribute(Constant.USER_SESSION_KEY, userInfo);
        return Result.success("");
    }
}
    用 Postman 测试接口,看返回的数据是什么,再进行修改代码,现在后端返回的数据如图:

    代码如下:
    <script>
        function login() {
            var userName = $("#userName").val();
            var password = $("#password").val();
            $.ajax({
                url: "/user/login",
                type: "post",
                data: {
                    userName: $("#userName").val(),
                    password: $("#password").val()
                },
                success: function(result) {
                    if(result.code == "SUCCESS" && result.data == "") {
                    location.href = "book_list.html?pageNum=1";
                    } else{
                        alert(result.errMsg);
                    }
                }
            });
        }
    </script>

2、book_update.html

    ![](https://i-blog.csdnimg.cn/direct/4f7eaaa978b14f7f927be267acf7b2ad.png)

    修改图书要切换到另一个界面,这个界面就要把刚刚所选中的图书,从数据库把所有信息都放到这个界面,如图:

    现在修改前端接受的信息,现在后端发来的数据个数如下:

    ![](https://i-blog.csdnimg.cn/direct/3704c4ef5ca94415bc1edff680b29e98.png)

    根据上面返回的信息修改前端代码,代码如下:
    <script>
        $.ajax({
            url: "/book/queryBookById" + location.search,
            type: "get",
            success: function (result) {
                if (result.code == "SUCCESS" && result.data != null) {
                    var book = result.data;
                    $("#bookId").val(book.id);
                    $("#bookName").val(book.bookName);
                    $("#bookAuthor").val(book.author);
                    $("#bookStock").val(book.count);
                    $("#bookPrice").val(book.price);
                    $("#bookPublisher").val(book.publish);
                    $("#bookStatus").val(book.status)
                }
            },
            error: function (error) {
                if (error != null && error.status == 401) {
                    //用户未登录
                    location.href = "login.html";
                }
            }
        });

        function update() {
            $.ajax({
                url: "/book/updateBook",
                type: "post",
                data: $("#updateBook").serialize(),
                success: function (result) {
                    if (result.code == "SUCCESS" && result == "") {
                        //更新成功
                        location.href = "book_list.html";
                    } else {
                        alert(result);
                    }
                },
                error: function () {
                    if (error != null && error.status == 401) {
                        //用户未登录
                        location.href = "login.html";
                    }
                }
            })
            // alert("更新成功");
            // location.href = "book_list.html"
        }
    </script>

3、book_add.html

    现在测试一下 /book/addBook 接口,如图:

    其错误信息放进 data 里面,是不合理的,所以现在修改一下后端代码,代码如下:
    <script>
        function add() {
            $.ajax({
                url: "/book/addBook",
                type: "post",
                data: $("#addBook").serialize(),
                success: function (result) {
                    if (result.code == "SUCCESS" && result.data == "") {
                        //添加成功
                        location.href = "book_list.html";
                    } else {
                        alert(result.data);
                    }
                },
                error: function () {
                    if (error != null && error.status == 401) {
                        //用户未登录
                        location.href = "login.html";
                    }
                }
            })
        }
    </script>

4、book_list.html

    删除接口的代码如下:
            function deleteBook(id) {
                var isDelete = confirm("确认删除?");
                if (isDelete) {
                    //删除图书
                    $.ajax({
                        url: "/book/deleteBook",
                        type: "post",
                        data: {
                            bookId: id
                        },
                        success: function (result) {
                            if (result != null && result.data == "") {
                                // location.href = "book_list.html" + location.search;
                                location.href = "book_list.html";
                            } else {
                                alert(result.data);
                            }
                        },
                        error: function () {
                            if (error != null && error.status == 401) {
                                //用户未登录
                                location.href = "login.html";
                            }
                        }
                    });
                    // alert("删除成功");
                }
            }
    批量删除接口的代码如下:
            function batchDelete() {
                var isDelete = confirm("确认批量删除?");
                if (isDelete) {
                    //获取复选框的id
                    var ids = [];
                    $("input:checkbox[name='selectBook']:checked").each(function () {
                        ids.push($(this).val());
                    });
                    console.log(ids);
                    $.ajax({
                        url: "/book/batchDeleteBook?ids=" + ids,
                        type: "post",
                        success: function (result) {
                            if (result != null && result.data == "") {
                                //删除成功
                                location.href = "book_list.html";
                            } else {
                                alert(result.data);
                            }
                        },
                        error: function () {
                            if (error != null && error.status == 401) {
                                //用户未登录
                                location.href = "login.html";
                            }
                        }
                    });
                    // alert("批量删除成功");
                }
            }

六、总结

    gitte源码:https://gitee.com/cool_tao6/studying-java-ee-advanced/tree/master/spring-interceptor

    统一处理包含了**拦截器、统一数据返回格式、统一异常处理**。

1、拦截器的实现主要分两部分

    (1)、定义拦截器(实现 **HandlerInterceptor 接口 **+ **@Component注解**)。

    (2)、配置拦截器(实现 **WebMvcConfigurer 接口 **+ **@Configuration注解**)。其中**@Configuration注解包含 @Component注解**

2、统一数据返回格式通过 @ControllerAdvice + ResponseBodyAdvice接口 来实现。

3、统一异常处理使用 @ControllerAdvice + @ExceptionHandler 来实现,并且可以细分不同的异常来处理。

标签: spring boot 后端 java

本文转载自: https://blog.csdn.net/cool_tao6/article/details/140340262
版权归原作者 tao滔不绝 所有, 如有侵权,请联系我们删除。

“SpringBoot统一功能处理”的评论:

还没有评论