• SpringBoot学习04-[定制SpringMVC]


    定制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
            );
        }
    
    }
    
  • 相关阅读:
    紫色调城市和奔跑人物剪影背景工会工作总结汇报PPT模板
    web前端面试-10大经典题(HTML基础)
    解决一个Qt程序崩溃的问题
    如何经营咖啡加盟店和独立咖啡店?用好国外成功经营咖啡馆四大法则
    python 本地降级pip
    C语言笔记(六)
    python集合的应用场景
    SpringCloud学习笔记(十四)Sentinel 实现熔断与限流
    Windows---命令打开截图工具,.bat文件执行
    直接缓存访问DCA
  • 原文地址:https://blog.csdn.net/weixin_43216437/article/details/139983950