首页 > 其他分享 >Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!

时间:2022-10-09 20:36:07浏览次数:52  
标签:场景 ControllerAdvice request public return error 注解 异常 class


一、前言

springboot默认发生4xx错误时候,pc端响应的页面如下

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_html

如果是移动端(手机端)将会响应json格式的数据,如下

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_java_02

二、Springboot异常处理

为什么我们请求错误的路径,boot会给我们返回一个上面错误页面或者json格式数据呢?原理是怎样的呢?

Springboot项目启动之后,执行有@SpringBootApplication注解的启动类的main方法,通过@EnableAutoConfiguration加载

springbootAutoConfiguration.jar包下的META-INF/spring.factories中的所有配置类(这些配置类加载之后,会将每个配置类里面的组件注入容器然后使用),其中一个自动配置类ErrorMvcAutoConfiguration,位置如下:

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_java_03

通过代码可以看到用到了以下四个组件

DefaultErrorAttributes、BasicErrorController、errorPageCustomizer、DefaultErrorViewResolver,

以DefaultErrorAttributes为例(其他三个组件类似)

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_数据_04

 当出现4xx或者5xx等错误时,errorPageCustomizer就会生效,this.properties.getError().getPath())并来到/error请求,核心代码

//errorPageCustomizer
@Value("${error.path:/error}")
private String path = "/error";

而这个/error请求再由BasicErrorController处理,BasicErrorController是一个Controller,其中里面有两种处理方法,一种是HTML形式,一种是JSON格式。其中访问者的信息可以从getErrorAttributes从获取。DefaultErrorAttributes是ErrorAttributes的实现类。

关键代码

@RequestMapping(produces = "text/html") //HTML
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}

@RequestMapping
@ResponseBody //JSON
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}

当为HTML模式时,就会构建一个resolveErrorView类,而resolverErrorView继续调用ErrorViewResolver。关键代码

protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}

在我们没有做自定义配置时,ErrorViewResolver就会指向DefaultErrorViewResolver。

static {
//可以用4xx,5xx文件名来统一匹配错误,但是会以精确优先的原则
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
//将错误代码拼接到error后
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
//如果模版引擎可用就让模版引擎进行解析如:Template/error/404
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
//如果模版引擎不可用,就在静态资源文件夹下找资源文件,error/404
return resolveResource(errorViewName, model);
}

三、简单处理异常

统一异常处理返回标识码

@ControllerAdvice
@ResponseBody
public class AllExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(AllExceptionHandler.class);

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.OK)
public CommonResult handlerUnexcptedServer(Exception ex) {
ex.printStackTrace();
logger.error("发生系统异常,错误信息为:" + ex.getMessage());
//返回自己定义的统一类
return new CommonResult(ResultConstant.FAIL_CODE, ex.getMessage());
}
}

 统一异常处理返回页面

/**
* 定义全局的异常处理
*/
@ControllerAdvice
public class GlobalExceptionHandler {
@Resource
private MessageSendService messageSendService;

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

/**
* 全局异常处理方法
*
* @param request 请求对象
* @param exception 异常对象
*/
@ExceptionHandler(Exception.class)
public String handleException(HttpServletRequest request, Exception exception) {
logger.error("====================================发生异常=====================================");
logger.error("url===:[{}]", request.getRequestURI());
logger.error("异常信息===:" + exception.getMessage(), exception);

//前端统一相应错误页面
return "error/500";
}
}

四、如何定制错误、异常响应

明白了boot处理异常机制,我们如何自定义异常响应规则呢?

第一种:pc端返回静态错误页面,手机端返回boot默认的json数据

如果项目中有模板引擎(jsp,thmeleaf,freemarker)的情况下,可以将错误页面命名为状态码.html放在模板引擎文件夹下的error文件夹下,发生异常,不管是前端请求还是后端程序错误会来到对应的错误页面。可以将错误页面命名为4xx和5xx匹配所有的错误,但是优先返回精确状态码.html页面;并且在模板引擎页面可以获取如下相关信息

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_spring_05

这里模版引擎使用thmeleaf

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_html_06

4xx代码

<!DOCTYPE html>
<html lang="en">
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="'状态码:'+${status}"></title>
</head>
<body>
< img src="../images/404.jpg" style="width: 40%;">
<h1 th:text="'时间:'+${timestamp}"></h1>
<h1 th:text="'错误提示:'+${error}"></h1>
<h1 th:text="'异常对象:'+${exception}"></h1>
<h1 th:text="'异常信息:'+${message}"></h1>
</body>
</html>

5xx代码

<!DOCTYPE html>
<html lang="en">
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="'状态码:'+${status}"></title>
</head>
<body>
<h2>500</h2>
<h1 th:text="'时间:'+${timestamp}"></h1>
<h1 th:text="'错误提示:'+${error}"></h1>
<h1 th:text="'异常对象:'+${exception}"></h1>
<h1 th:text="'异常信息:'+${message}"></h1>
</body>
</html>

我们请求一个错误的地址路径

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_java_07

我们在程序代码中人为制造一个异常,请求响应

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_spring_08

上面是有模版引擎的情况下处理错误以及异常的方式,

如果项目中没有模板引擎,(模板引擎找不到这个错误页面),静态资源文件夹static下找对应的4xx或者5xx或者更精确的错误页面。但是如果不用模板引擎,页面不能获取上面说的页面信息;

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_java_09

上面两种方式使用手机访问返回都是boot默认的json数据

第二种:pc端返回动态的页面 ,手机端返回动态json数据

上面第一种可以轻松的的处理异常,只需在指定的路径下放静态页面(无模版引擎的情况)或者携带相关信息的页面(有模版引擎),缺点就是不能在页面携带我们想要展示的数据,比如当我们程序某处放生异常,我们要返回我们自己提示的错误信息。这种异常如果处理呢?

默认情况下,在 Spring Boot 中,所有的异常数据其实就是第一种所展示出来的 5几条数据,这些数据定义在 org.springframework.boot.web.reactive.error.DefaultErrorAttributes 类中,具体定义在 getErrorAttributes 方法中 :核心代码如下

@Override
public Map<String, Object> getErrorAttributes(ServerRequest request,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
errorAttributes.put("path", request.path());
Throwable error = getError(request);
HttpStatus errorStatus = determineHttpStatus(error);
errorAttributes.put("status", errorStatus.value());
errorAttributes.put("error", errorStatus.getReasonPhrase());
errorAttributes.put("message", determineMessage(error));
handleException(errorAttributes, determineException(error), includeStackTrace);
return errorAttributes;
}

DefaultErrorAttributes 类本身则是在 org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration 异常自动配置类中定义的,如果开发者没有自己提供一个 ErrorAttributes 的实例的话,那么 Spring Boot 将自动提供一个 ErrorAttributes 的实例,也就是 DefaultErrorAttributes 。

基于此 ,开发者自定义 ErrorAttributes 有两种方式 实现自定义数据:

    1.直接实现 ErrorAttributes 接口
    2.继承 DefaultErrorAttributes(推荐),因为 DefaultErrorAttributes 中对异常数据的处理已经完成,开发者可以直接使用。

package com.javayihao.top.config;

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;

import java.util.Map;

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
if ((Integer)map.get("status") == 500) {
//这里根据自己需求设置
map.put("message", "服务器内部错误!");
}
if ((Integer)map.get("status") == 404) {
map.put("message", "路径不存在!");
}
return map;
}
}

我们服务器访问 错误路径

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_html_10

客户端响应

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_spring boot_11

访问有异常的的控制器

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_java_12

客户端响应

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_数据_13

当然上面我可以在程序任意位置抛出异常,使用全局异常处理器处理

自定义异常

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_html_14

全局异常处理器

@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(MyException.class)
public String jsonErrorHandler(HttpServletRequest request, Exception e) {
Map<String, Object> map = new HashMap<>();
request.setAttribute("java.servlet.error.status_code", 500);
map.put("code", -1);
map.put("msg", e.getMessage());
request.setAttribute("ext", map);
//转发到error
return "forward:/error";
}
}

 自定义ErrorAttributes

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
//返回的map就是页面或者json能获取的所有字段
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
//可以额外添加内容
map.put("company", "javayihao");
//取出异常处理器中的携带的数据
Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);//传入0代表从request中获取
map.put("ext", ext);
return map;
}
}

第三种:pc端返回动态json数据,手机端也返回动态json数据

定义一个全局异常处理器

使用 ​​@ControllerAdvice​​​ 结合​​@ExceptionHandler​​​ 注解可以实现统一的异常处理,​​@ExceptionHandler​​​注解的类会自动应用在每一个被 ​​@RequestMapping​​ 注解的方法。当程序中出现异常时会层层上抛

import lombok.extern.slf4j.Slf4j;
import net.codingme.boot.domain.Response;
import net.codingme.boot.enums.ResponseEnum;
import net.codingme.boot.utils.ResponseUtill;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/**
* <p>
* 统一的异常处理
*
* @Author niujinpeng
* @Date 2019/1/7 14:26
*/

@Slf4j
@ControllerAdvice
public class ExceptionHandle {

@ResponseBody
@ExceptionHandler(Exception.class)
public Response handleException(Exception e) {
log.info("异常 {}", e);
if (e instanceof BaseException) {
BaseException exception = (BaseException) e;
String code = exception.getCode();
String message = exception.getMessage();
return ResponseUtill.error(code, message);
}
return ResponseUtill.error(ResponseEnum.UNKNOW_ERROR);
}
}
{
"code": "-1",
"data": [],
"message": "未知错误"
}

 我们还可以自定义一个异常,在程序中用于抛出

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_spring_15

定义一个返回结果对象(也可以不用定义,直接使用map)存储异常信息

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_java_16

/*ControllerAdvice用来配置需要进行异常处理的包和注解类型,
比如@ControllerAdvice(annotations = RestController.class)
只有类标有rescontrolle才会被拦截
*/
@ControllerAdvice
public class MyExceptionHandler {
//自己创建的异常按照自己编写的信息返回即可
@ExceptionHandler(value = MyException.class)
@ResponseBody
public ErrorInfo<String> errorInfo(HttpServletRequest req, MyException e) {
ErrorInfo<String> r = new ErrorInfo<>();
r.setCode(ErrorInfo.ERROR);
r.setMessage(e.getMessage());
r.setData("测试数据");
r.setUrl(req.getRequestURL().toString());
return r;
}
//系统异常时返回的异常编号为 -1 ,返回的异常信息为 “系统正在维护”;不能将原始的异常信息返回
@ExceptionHandler(value = Exception.class)
@ResponseBody
public ErrorInfo<String> errorInfo(HttpServletRequest req, Exception e) {
ErrorInfo<String> r = new ErrorInfo<>();
r.setCode(ErrorInfo.ERROR);
r.setMessage("系统维护中");
return r;
}
}

参考


五、@ControllerAdvice 注解的三种使用场景!

@ControllerAdvice是一个@Component,用于定义@ExceptionHandler(最主要用途),@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法(拦截)。除了上面的全局异常处理,@ControllerAdvice其他两种使用场景

1、全局数据绑定

全局数据绑定功能可以用来做一些初始化的数据操作,我们可以将一些公共的数据定义在添加了 @ControllerAdvice 注解的类中,这样,在每一个 Controller 的接口中,就都能够访问导致这些数据。

使用步骤,首先定义全局数据,如下:

使用 @ModelAttribute 注解标记该方法的返回数据是一个全局数据,默认情况下,这个全局数据的 key 就是返回的变量名,value 就是方法返回值,当然开发者可以通过 @ModelAttribute 注解的 name 属性去重新指定 key。

@ControllerAdvice

public class ControllerAdviceTest {

/**

* 把值绑定到Model中,使全局@RequestMapping可以获取到该值

* @param model

*/

@ModelAttribute

public void addAttributes(Model model) {

System.out.println("添加全局变量");

model.addAttribute("userName", "Jack");

}

}



@RestController

public class ExceptionController {

/**

* 使用注入的ModelMap来取变量

* @param modelMap

* @return

*/

@RequestMapping("modelMapTest1")

public Object modelMapTest1(ModelMap modelMap){

Object globalVal = modelMap.get("userName");

System.out.println("全局变量为:"+globalVal);

return globalVal;

}

}

 

2、全局数据预处理

 考虑我有两个实体类,Book 和 Author,分别定义如下:

public class Book {
    private String name;
    private Long price;
    //getter/setter
}
public class Author {
    private String name;
    private Integer age;
    //getter/setter
}

此时,如果我定义一个数据添加接口,如下:

@PostMapping("/book")
public void addBook(Book book, Author author) {
System.out.println(book);
System.out.println(author);
}

这个时候,添加操作就会有问题,因为两个实体类都有一个 name 属性,从前端传递时 ,无法区分。此时,通过 @ControllerAdvice 的全局数据预处理可以解决这个问题

解决步骤如下:

1.给接口中的变量取别名

@PostMapping("/book")
public void addBook(@ModelAttribute("b") Book book, @ModelAttribute("a") Author author) {
System.out.println(book);
System.out.println(author);
}

 2.进行请求数据预处理
在 @ControllerAdvice 标记的类中添加如下代码:

@InitBinder("b")
public void b(WebDataBinder binder) {
binder.setFieldDefaultPrefix("b.");
}
@InitBinder("a")
public void a(WebDataBinder binder) {
binder.setFieldDefaultPrefix("a.");
}

@InitBinder(“b”) 注解表示该方法用来处理和Book和相关的参数,在方法中,给参数添加一个 b 前缀,即请求参数要有b前缀.

3.发送请求

请求发送时,通过给不同对象的参数添加不同的前缀,可以实现参数的区分.

Springboot异常处理和自定义错误页面及@ControllerAdvice 注解的三种使用场景!五、@ControllerAdvice 注解的三种使用场景!_spring boot_17

再如:转换日期格式

@ControllerAdvice

public class ControllerAdviceTest {

/**

* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器

* WebDataBinder是用来绑定请求参数到指定的属性编辑器

* @param binder

*/

@InitBinder

public void initBinder(WebDataBinder binder) {

System.out.println("initBinder执行");

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

dateFormat.setLenient(false); //日期格式是否宽容(只能判断是否需要跳到下个月去)

/*

* spring mvc在绑定表单之前,都会先注册这些编辑器,

* Spring自己提供了大量的实现类,诸如CustomDateEditor,CustomBooleanEditor,CustomNumberEditor等

* 使用时候调用WebDataBinder的registerCustomEditor方法注册

*/

binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat,false));

}

}





@RestController

public class ExceptionController {

@RequestMapping("/date")

public Date index(Date date){

System.out.println("date="+date);

return date;

}

}

浏览器访问:localhost:8080/date?date=2019-3-20

控制台输出:initBinder执行 date=2019-3-20

浏览器显示:"2019-3-20"

交流公众号:java一号

标签:场景,ControllerAdvice,request,public,return,error,注解,异常,class
From: https://blog.51cto.com/u_11334685/5741388

相关文章

  • 用代码模拟用户登录输入密码场景
    #每日美图分享#描述:用代码模拟用户登入输入密码的场景,输入密码三次错误即退出程序。#define_CRT_SECURE_NO_WARNINGS1#include<stdio.h>intmain(){inti=0;charpi......
  • Flink架构优势及应用场景
       相对于传统的离线计算会存在数据反馈不及时的问题,很难满足急需实时数据做决策的场景Flink是对有界数据和无界数据进行有状态计算的分布式引擎,它是纯流式处理模式。......
  • Flink架构优势及应用场景
    相对于传统的离线计算会存在数据反馈不及时的问题,很难满足急需实时数据做决策的场景Flink是对有界数据和无界数据进行有状态计算的分布式引擎,它是纯流式处理模式。纯流式模......
  • 【SpringBoot】常用注解
    @Controller标记在类上面,类就是一个Controller对象;只是定义了一个控制器类。@RestController写在Controller类之前,添加该注解即可返回JSON格式的数据;@RestController......
  • 物联网开发平台,覆盖各行业物联应用场景
    物联网时代的到来,对工作于现场的机器设备提出了新的要求,即可异地远程监控减少现场维护,同时远程连接的安全性要得到保证,特别是新冠疫情以来,远程服务的需求大大增多。将设备接......
  • SQL抽象语法树及改写场景应用
    1背景我们平时会写各种各样或简单或复杂的sql语句,提交后就会得到我们想要的结果集。比如sql语句,”select*fromt_userwhereuser_id>10;”,意在从表t_user中筛选出user......
  • 数据填报平台能满足什么样的业务场景?_光点科技
    无论你在哪个国家,无论你做什么,你每天都会处理各种信息和数据收集。我们收集数据的形式也在发生变化,比如注册和统计,这以一种奇妙的方式改变了我们的生活。1.校园学生信息收集......
  • SQL抽象语法树及改写场景应用
    1背景我们平时会写各种各样或简单或复杂的sql语句,提交后就会得到我们想要的结果集。比如sql语句,”select*fromt_userwhereuser_id>10;”,意在从表t_user中筛选出us......
  • Springboot传参时通过注解转换RequestBody中的枚举类型
    请求对象为@RequestBodyMyRequestrequest时,MyReqeust中包含了枚举类型。如果不加处理,前端只能传递枚举名或者枚举数组下标。经过改造,可以传递自已定义的枚举值。参考......
  • 注解和反射
    注解注解入门Annotation是从JDK5.0开始引入的新技术Annotation的作用:不是程序本身,可以对程序作出解释(这一点和注释没有什么区别)可以被其他程序(比如:编译器)读取An......