定制SpringMVC
- 定制SpringMvc的自动配置
- 定制springmvc-configurePathMatch配置
- 定制SpringMVC-拦截器Interceptor
- 定制SpringMVC-CORS配置
- 全局cors配置
- 针对某个方法加跨域解决
- WebMvcConfigurer原理
- 定制SpringMVC-JSON
- JSON开发
- jackson的使用
- 定制化json序列化和反序列化
- JSON国际化
- 添加国际化资源文件
- 配置messageResource设置国际化资源文件
- @Conditional
- 源码解读
- 配置国际化资源环境路径让MessageSourceAutoConfiguration 配置类生效
- 需要去解析请求头中的Accept-Language或者获取url参数中的?local=,来判断语言
- 源码解读
- 随意切换本地语言,进行缓存
- 通过messageResource获取国际化信息
- 在handler方法参数中加入Local参数,注入MessageSource 对象
- 使用自定义工具类
- 统一异常处理
- SpringBoot统一异常处理
- 源码解读-ErrorMvcAutoConfiguration配置类
- BasicErrorController
- errorHtml:怎么去定制它的返回页面?
- resolveErrorView源码解读
- 定制化页面
- error:看它是怎么返回json数据的,从而要定制自己的json数据
- SpringBoot定制浏览器请求和ajax请求,返回异常处理
- 自定义异常处理类(覆盖BasicErrorController)
定制SpringMvc的自动配置
SpringMvc的自动配置类:WebMvcAutoConfiguration
1、在大多数情况下,SpringBoot在自动配置中标记了很多@ConditionalOnMissingBean,我们只需要在自己的配置类中配置对应的bean,就可以覆盖原有的bean
定制springmvc-configurePathMatch配置
配置configurePathMatch的setUseTrailingSlashMatch
setUseTrailingSlashMatch:是否与 URL 匹配,而不考虑是否存在尾部斜杠。如果启用,映射到“users”的方法也与“users”匹配。默认值为 true。
- 自定义配置
@Configuration
public class CustomWebMvcConfig implements WebMvcConfigurer {
/**
*
* @param configurer
*/
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(false);
}
}
- 测试
此时访问:http://localhost:8080/person/14/,如果最后加上/ 则找不到
改为true,则可以找到
定制SpringMVC-拦截器Interceptor
自定义一个拦截器用来计算用户请求执行时间
preHandle:请求执行前,返回值设置为true,不然请求无法走下去
postHandle:请求执行后,执行的方法
- 自定义拦截器
public class CustormInterceptor implements HandlerInterceptor {
LocalDateTime beginTime;
Logger log=LoggerFactory.getLogger(CustormInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//开始时间
beginTime = LocalDateTime.now();
log.info("当前请求,{}+用户请求开始时间:{}",request.getRequestURI(),beginTime);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//结束时间
LocalDateTime endTime = LocalDateTime.now();
//计算两个时间
Duration between = Duration.between(beginTime, endTime);
//获得相差毫秒
long l = between.toMillis();
log.info("当前请求,{}+用户请求时间:{}毫秒",request.getRequestURI(),l);
}
}
- 在自定义配置中使用我们的自定义的拦截器
@Configuration
public class CustomWebMvcConfig implements WebMvcConfigurer {
/**
*
* @param configurer
*/
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(true);
}
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CustormInterceptor()) //添加拦截器
.addPathPatterns("/**"); //拦截映射规则
}
}
- 测试
可以看到控制台打印出了请求的开始时间和请求执行的耗时时间
定制SpringMVC-CORS配置
跨域
全局cors配置
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //映射服务器中哪些http接口运行跨域访问
.allowedOrigins("http://localhost:63343/") //配置哪些来源有权限跨域
.allowedMethods("GET","POST","DELETE","PUT"); //允许访问的方法
}
@Configuration
public class CustomWebMvcConfig implements WebMvcConfigurer {
/**
*
* @param configurer
*/
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(true);
}
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CustormInterceptor()) //添加拦截器
.addPathPatterns("/**"); //拦截映射规则
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //映射服务器中哪些http接口运行跨域访问
.allowedOrigins("http://localhost:63343/") //配置哪些来源有权限跨域
.allowedMethods("GET","POST","DELETE","PUT"); //允许访问的方法
}
}
针对某个方法加跨域解决
添加@CrossOrigin注解
@GetMapping("/{id}")
@ApiOperation("根据id获取用户")
@CrossOrigin
public Result<Person> getPerson(@PathVariable("id") Integer id){
Person person = personService.getPersoById(id);
return new Result<>("200","查询成功",person);
}
- 测试
WebMvcConfigurer原理
实现WebMvcConfigurer接口可以扩展MVC实现,又既保留SpringBoot的自动配置
1、在自动配置类WebMvcAutoConfiguration也有一个实现了WebMvcConfigurer接口的配置类WebMvcAutoConfigurationAdapter
2、WebMvcAutoConfigurationAdapter也是通过重写了方法来自定义配置,帮助我们进行自动配置,我们只需定制(拦截器、视图控制器、CORS)等在开发中需要额外定制的功能。
3、导入了EnableWebMvcConfiguration
@Import({EnableWebMvcConfiguration.class})
4、EnableWebMvcConfiguration的父类上setConfigurers使用了@Autowired注解
- 它会去容器中将所有实现了WebMvcConfigurer接口的bean都自动注入进来,添加到configurers变量中
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
- 添加到delegates委派器中
private final List<WebMvcConfigurer> delegates = new ArrayList<>();
public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.delegates.addAll(configurers);
}
}
- 底层调用WebMvcConfigurer对应的方法时,就是去拿到之前添加到delegates中的 WebMvcConfigurer
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
for (WebMvcConfigurer delegate : this.delegates) {
delegate.configurePathMatch(configurer);
}
}
- @EnableWebMvc
当自定义WebMvcConfigurer 添加了@EnableWebMvc就不会使用SpringMVC自动配置类的默认配置了,默认配置就失效了
- 为什么呢?原理
- 在@EnableWebMvc中导入了DelegatingWebMvcConfiguration.class
- DelegatingWebMvcConfiguration类又继承了WebMvcConfigurationSupport类
配置类继承某个类会把继承的这个类也装配进spring容器中所以WebMvcConfigurationSupport会被装配进spring容器中
- 我们在看WebMvcAutoConfiguration配置类(springMVC核心配置类)上有个条件注解@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class}):如果容器中没有WebMvcConfigurationSupport. bean,该配置类才生效
复习:@ConditionalOnMissingBean(类.class):没有某个Bean的时候才会注册某个bean到spring容器中
定制SpringMVC-JSON
1、JSON开发
SpringBoot提供了与三个JSON映射库的集成
- Gson
- Jackson
- JSON-B
Jackson是SpringBoot的默认json库
2、国际化
3、统一异常处理
JSON开发
1、jackson的使用
2、根据自己的业务需求进行json的序列号和反序列化
jackson的使用
- @JsonIgnore
进行排除json序列化,将它标注在属性上
- @JsonFormat
数据库中birthday:date类型
实体类birthday:date类型
如果不加@JsonFormat输出为
@JsonFormat:进行日期格式化,添加在字段上
@ApiModelProperty("出生日期")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date birthday;
测试
- @JsonInclude(JsonInclude.Include.NON_NULL)
字段不为空才包含在json中,为空就不包含
不设置的话,输出的json串字段为空
设置的话,就不进行输出,不显示
@ApiModelProperty("出生日期")
@JsonFormat(pattern = "yyyy-MM-dd",locale = "zh")
@JsonInclude(JsonInclude.Include.NON_NULL)
private Date birthday;
- @JsonProperty
对字段属性名设置别名
@ApiModelProperty("姓名")
@JsonProperty("u_name")
private String name;
定制化json序列化和反序列化
SpringBoot还提供了@JsonComponent来根据自己的业务需求进行json的序列化和反序列化
@JsonComponent
public class CustomeJsonComponent {
public static class Serializer extends JsonSerializer<Person>{
@Override
public void serialize(Person person, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
}
}
public static class DeSerializer extends JsonDeserializer<Person>{
@Override
public Person deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
return null;
}
}
}
SpringBoot还提供了JsonObjectSerializer和JsonObjectDeserializer基类,它们在序列对象时为标准JackJson版本提供了有用的替代方法。
- 序列化
@JsonComponent
public class CustomeJsonComponent {
public static class Serializer extends JsonObjectSerializer<Person> {
@Override
protected void serializeObject(Person value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeObjectField("id",value.getId());
jgen.writeObjectField("uname","dgdg");
//一次查不出完整的数据返回给前端,需要根据需求去做一些个性化调整
//根据不同的权限给他返回不同的序列化数据
}
}
public static class DeSerializer extends JsonObjectDeserializer<Person> {
@Override
protected Person deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec, JsonNode tree) throws IOException {
return null;
}
}
}
可以看到输出是由我们自定义的json来进行输出
- 反序列化
@JsonComponent
public class CustomeJsonComponent {
public static class Serializer extends JsonObjectSerializer<Person> {
@Override
protected void serializeObject(Person value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeObjectField("id",value.getId());
jgen.writeObjectField("uname","dgdg");
//一次查不出完整的数据返回给前端,需要根据需求去做一些个性化调整
//根据不同的权限给他返回不同的序列化数据
}
}
public static class DeSerializer extends JsonObjectDeserializer<Person> {
@Override
protected Person deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec, JsonNode tree) throws IOException {
Person person = new Person();
person.setAge(tree.findValue("age").asInt());
return person;
}
}
}
JSON国际化
- 实现步骤
1、添加国际化资源文件
2、配置messageResource设置国际化资源文件
3、需要去解析请求头中的Accept-Language获取url参数中的?local= 来获取请求语言
3.1、随意切换本地语言进行缓存
4、通过messageResource获取国际化信息
添加国际化资源文件
在resources文件夹下新建i18n文件夹用了存放国际化资源文件
分别创建message.properties、message_en_US.properties文件和message_zh_CN.properties文件,名称是固定的(约定大于配置)
- 添加一个key person-query.success对统一返回result做国际化
配置messageResource设置国际化资源文件
- 理解下面代码的前提是熟悉@Conditional注解
@Conditional
- 需要注册的bean
@Data
@Component
@Conditional(InjectTeacherCondition.class)
public class Teacher {
private String name;
private String job;
}
- 自定义条件
必须实现Condition 接口,并且重写matches方法,如果matches方法返回为true,则将teacher注册进spring容器中,返回为false则不注册到spring容器
public class InjectTeacherCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
源码解读
在springboot中提供了MessageSourceAutoConfiguration配置类,所以我们不需要去配置messageResource
@Configuration(
proxyBeanMethods = false
)
//如何自己配置了@bean 名字叫messageSource的bean,就会用自定义的
@ConditionalOnMissingBean(
name = {"messageSource"},
search = SearchStrategy.CURRENT
)
@AutoConfigureOrder(Integer.MIN_VALUE)
//自定义条件匹配 会传入实现Condition接口的类ResourceBundleCondition,
//ResourceBundleCondition类会重写matches,自定义匹配规则,如果该方法返回为true,就匹配成功
@Conditional({ResourceBundleCondition.class})
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
ResourceBundleCondition类会重写matches,自定义匹配规则,如果该方法返回为true,就匹配成功
public abstract class SpringBootCondition implements Condition {
private final Log logger = LogFactory.getLog(getClass());
@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
//getMatchOutcome:具体的匹配规则就在这个里面
ConditionOutcome outcome = getMatchOutcome(context, metadata);
logOutcome(classOrMethodName, outcome);
recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
}
- getMatchOutcome
- 只要在这个方法中将返回的ConditionOutcome .isMatch=true那就匹配成功了。
protected static class ResourceBundleCondition extends SpringBootCondition {
private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取配置文件中spring.message.basename,由于我们没有配置,默认值是:message
String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
ConditionOutcome outcome = cache.get(basename);
if (outcome == null) {
outcome = getMatchOutcomeForBasename(context, basename);
cache.put(basename, outcome);
}
return outcome;
}
- getMatchOutcomeForBasename
private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {
//根据message获取,该类路径下的所有的properties的资源文件
for (Resource resource : getResources(context.getClassLoader(), name)) {
if (resource.exists()) {
//new 了一个ConditionOutcome并且将match设置为true
return ConditionOutcome.match(message.found("bundle").items(resource));
}
}
}
return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
}
配置国际化资源环境路径让MessageSourceAutoConfiguration 配置类生效
通过阅读以上源码我们可以知道,要么在配置文件加spring.messages.basename,要么在resources下新建messages文件夹,这个时候MessageSourceAutoConfiguration 才会生效
- 没有生效的原因
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取配置文件中spring.message.basename,由于我们没有配置,默认值是:message
String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
ConditionOutcome outcome = cache.get(basename);
if (outcome == null) {
outcome = getMatchOutcomeForBasename(context, basename);
cache.put(basename, outcome);
}
return outcome;
}
添加配置文件让配置类MessageSourceAutoConfiguration 生效
spring
messages:
basename: i18n.message
- 配置文件设置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/personaltest
username: root
password: 123456
mvc:
pathmatch:
matching-strategy: ant_path_matcher
messages:
basename: i18n.message
mybatis:
mapper-locations: classpath*:mapper/*.xml
- 测试
在配置文件加debug: true,启动项目后通过控制台可以看到该配置类已经生效
总结:如果要让配置类MessageSourceAutoConfiguration 生效
- 必须保证,在类路径下的message文件夹中有国际化的资源文件
- 或者自己配置spring.messages.basename 告诉它资源文件在哪里
sping:
messages:
basename: i18n.message
- 只要找到了国际化的资源文件那就会设置ConditionOutcome .isMatch=true
- 当ConditionOutcome .isMatch=true,那么@Conditional({ResourceBundleCondition.class}),就匹配成功
- 一旦匹配成功,那自动配置类就会生效,就会帮我们配置一个meessageSource
需要去解析请求头中的Accept-Language或者获取url参数中的?local=,来判断语言
源码解读
- 其实WebMvcAutoConfiguration 配置类也帮我们配置了一个解析请求头中Accept-Language的localResolver
@Bean
@ConditionalOnMissingBean(
name = {"localeResolver"}
)
public LocaleResolver localeResolver() {
// 当配置spring.mvc.locale-resolver=fixed
if (this.webProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.WebProperties.LocaleResolver.FIXED) {
//就会使用配置文件中的本地化语言:spring.mvc.locale=en_US 就可以设死本地化语言
return new FixedLocaleResolver(this.webProperties.getLocale());
} else {
// 默认就是使用AcceptHeaderLocaleResolver作为本地化解析器
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
//spring.mvc.locale=en_US 作为默认的本地化语言
localeResolver.setDefaultLocale(this.webProperties.getLocale());
return localeResolver;
}
}
- AcceptHeaderLocaleResolver
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = getDefaultLocale();
//当Accept-Language为null 才会使用配置文件中设置的local:spring.mvc.locale
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
}
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = getSupportedLocales();
if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
return requestLocale;
}
Locale supportedLocale = findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
}
return (defaultLocale != null ? defaultLocale : requestLocale);
}
随意切换本地语言,进行缓存
在mvc配置类中自定义一个LocaleResolver 来覆盖原有的LocaleResolver ,因为原来的LocaleResolver 只会从Accept-Language获取语言:
先不考虑分布式session的情况,用cookie进行缓存(因为cookie在同一个域是可以共享的)
- 自定义配置bean
@Configuration
public class CustomWebMvcConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
//先不考虑分布式session的情况,用cookie进行缓存(因为cookie在同一个域是可以共享的)
CookieLocaleResolver cookie=new CookieLocaleResolver();
cookie.setCookieMaxAge(60*60*24*30); //设置cookie过期时间
cookie.setCookieName("local"); //设置cookie名称
return cookie;
}
}
- 将LocaleChangeInterceptor添加到拦截器中
这个拦截器会默认把local=zh_CN的url进行拦截并存储到cookie中,然后我们自定义的localeResolver,就会解析cookie获取当前语言
@Configuration
public class CustomWebMvcConfig implements WebMvcConfigurer {
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CustormInterceptor()) //添加拦截器
.addPathPatterns("/**"); //拦截映射规则
registry.addInterceptor(new LocaleChangeInterceptor())
.addPathPatterns("/**");
}
- 测试
后缀必须加?en_US或者zh_CN
通过messageResource获取国际化信息
- 第一种方法:在handler方法参数中加入Local参数,注入MessageSource 对象
messageSource.getMessage
- 第二种方法:使用自定义工具类
在handler方法参数中加入Local参数,注入MessageSource 对象
- 注入MessageSource 对象
@Autowired
private MessageSource messageSource;
- 在handler中使用
String message=messageSource.getMessage("person-query.success",null, LocaleContextHolder.getLocale());
@GetMapping("/{id}")
@ApiOperation("根据id获取用户")
public Result<Person> getPerson(@PathVariable("id") Integer id){
// LocaleContextHolder:就是一个Local的持有器 springmvc底层会自动将localeResolver中的语言设置进去
String message=messageSource.getMessage("person-query.success",null, LocaleContextHolder.getLocale());
Person person = personService.getPersoById(id);
return new Result<>("200",message,person);
}
- 测试
修改浏览器默认语言为en_US
使用自定义工具类
其实也是第一种方式
@Component
@Slf4j
public class I18nMessageUtil {
@Autowired
private MessageSource messageSource;
@Autowired
private HttpServletRequest request;
/**
* 根据code获取国际化信息
* @param code code
* @return
*/
public String getLocaleMessage(String code) {
return getLocaleMessage(code,null,null);
}
/**
* 根据code获取国际化信息,如果没有则使用默认提示信息
* @param code code
* @param defaultMsg 默认提示信息
* @return
*/
public String getLocaleMessage(String code, String defaultMsg) {
return getLocaleMessage(code,defaultMsg,null);
}
/**
* 根据code获取国际化信息,并且替换占位符
* @param code
* @param params
* @return
*/
public String getLocaleMessage(String code, String[] params) {
return getLocaleMessage(code,null,params);
}
/**
* 根据code获取国际化信息,没有就使用默认值,并且替换占位符
* @param code code
* @param defaultMsg 默认提示信息
* @param params 替换占位符的参数
* @return
*/
public String getLocaleMessage(String code, String defaultMsg, Object[] params) {
String language = request.getParameter("locale");
Locale locale = Objects.nonNull(language) ? new Locale(language) : LocaleContextHolder.getLocale();
try {
return messageSource.getMessage(code, params, locale);
} catch (Exception e) {
e.printStackTrace();
log.warn("本地化异常消息发生异常: {}, {}", code, params);
return defaultMsg;
}
}
}
统一异常处理
@ControllerAdvice是Spring3.2提供的新注解,它是对Controller的增强,可对controller中被@RequestMapping注解的方法加一些逻辑处理:
- 1、全局异常处理
- 2、全局数据绑定
- 3、全局数据预处理
@ExceptionHandler
- @ExceptionHandler加在Controller中:只处理当前控制器的异常,优先级比全局高
- @ExceptionHandler,加在ControllerAdvice中:处理全局异常
SpringBoot统一异常处理
SpingBoot 有统一异常处理的自动配置类:ErrorMvcAutoConfiguration
源码解读-ErrorMvcAutoConfiguration配置类
重要组件:
- DefaultErrorAttributes
- BasicErrorController
- DefaultErrorViewResolver:用了解析错误视图页面
BasicErrorController
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
原理图
如果发生了异常,springmvc就会转发一个到一个/error,再由springmvc中央调度器去匹配,最终匹配到了BasicErrorController
- 怎么处理的-怎么判断交给那个handler方法呢?
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE):produces 就是处理请求头中Accept的
如果@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)能捕捉到Accept并且value中含有text/html 就说明是一个网页。
- 当使用浏览器发送请求时,请求头是Accept:text/html,就会交给errorHtml方法进行处理,返回一个modeAndView
- 除了text/html的其他请求都会交给error方法进行处理
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) //public static final String TEXT_HTML_VALUE = "text/html";
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
errorHtml:怎么去定制它的返回页面?
- getErrorAttributes()
- 用来获取所需要的异常信息
resolveErrorView源码解读
- resolveErrorView:解析视图
- 会调用DefaultErrorViewResolver的resolveErrorView方法来进行解析
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
- resolve:先从模版视图去解析(由于我们没有配置模版视图,所以并不解析出来),没有模版所以调resolveResource,进一步从自定义的包里查找
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}
- resolveResource:这个方法就是去下面的路径去找:classpath:/static/error/xxx.html
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resources.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
定制化页面
由以上源码分析可知,springboot会从static/error/ 路径下去找
- 未配置定制化页面之前
- 配置定制化页面
在resources/static/error 文件夹下新建400.html页面
- 测试
总结: 从errorHtml方法可以得出结论:我们需要使用自定义的页面响应错误,只需要再对应的路径上创建对应错误代码的页面就行了,但是如果想记录日志的话就需要定制了。
error:看它是怎么返回json数据的,从而要定制自己的json数据
- error方法
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
//就是调用getErrorAttributes获取了异常信息
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
- getErrorAttributes如何获取错误信息呢?
就是获取errorAttributes来获取错误信息
那errorAttributes怎么来的呢?
在basicErrorController,被注册的时候,就已经注入进来了
SpringBoot定制浏览器请求和ajax请求,返回异常处理
- 对浏览器请求:扩展日志处理
- 对ajax请求:修改返回类型 加上日志处理
自定义异常处理类(覆盖BasicErrorController)
@Controller
@RequestMapping("/error")
public class CustomErrorController extends AbstractErrorController {
public CustomErrorController(ErrorAttributes errorAttributes, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
}
Logger log= LoggerFactory.getLogger(CustomException.class);
/**
* 处理浏览器请求的
* 在这个基础上加上日志处理
*
* @param request
* @param response
* @return
*/
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions()));
response.setStatus(status.value());
log.error(model.get("trace").toString());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
/**
* 处理ajax
* 修改返回类型:Result 加上异常日志记录
*
* @param request
* @return
*/
@RequestMapping
@ResponseBody
public Result<String> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new Result<String>("204", "No Content");
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions());
String code = body.get("status").toString();
String message = body.get("message").toString();
log.error(message);
return new Result<String>(code, message);
}
/**
* 异常信息的选项
* @return
*/
protected ErrorAttributeOptions getErrorAttributeOptions() {
//添加需要的异常信息
return ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE, ErrorAttributeOptions.Include.STACK_TRACE,
ErrorAttributeOptions.Include.EXCEPTION
);
}
}