尚硅谷SpringBoot顶尖教程
1. web准备
首先创建SpringBoot应用,选择我们需要的模块;
SpringBoot已经默认将这些web场景配置好了,只需要在配置文件中指定少量配置就可以运行;
web场景, SpringBoot帮我们配置了什么?能不能修改?能修改哪些配置?能不能扩展?
- WebMvcAutoConfiguration:帮我们给容器中自动配置web组件
- WebMvcProperties:封装配置文件的内容
最后自己编写业务代码即可.
2. SpringBoot对静态资源的映射规则
SpringBoot对静态资源的处理在Web组件WebMvcAutoConfiguration自动配置类中.
@Configuration@ConditionalOnWebApplication@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class })@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class })public class WebMvcAutoConfiguration { // .... // WebMvcAutoConfigurationAdapter @Configuration @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter { // .... // addResourceHandlers @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); return; } Integer cachePeriod = this.resourceProperties.getCachePeriod(); if (!registry.hasMappingForPattern("/webjars this.resourceProperties.getStaticLocations()) .setCachePeriod(cachePeriod)); } } }}
2.1 访问/webjars this.resourceProperties.getStaticLocations()) .setCachePeriod(cachePeriod)); } }}
ResourceProperties中追踪this.resourceProperties.getStaticLocations()的源码
// ResourceProperties可以设置和静态资源有关的参数@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)public class ResourceProperties implements ResourceLoaderAware { private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" }; private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; private static final String[] RESOURCE_LOCATIONS; static { RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length + SERVLET_RESOURCE_LOCATIONS.length]; System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0, SERVLET_RESOURCE_LOCATIONS.length); // 将CLASSPATH_RESOURCE_LOCATIONS复制到RESOURCE_LOCATIONS System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length); } private String[] staticLocations = RESOURCE_LOCATIONS; // .... // 指向上面的静态资源路径 RESOURCE_LOCATIONS public String[] getStaticLocations() { return this.staticLocations; } // 可以指定staticLocations public void setStaticLocations(String[] staticLocations) { this.staticLocations = appendSlashIfNecessary(staticLocations); }}
准备好静态资源
启动应用后, 访问 http://localhost:8082/boot1/a.png, http://localhost:8082/boot1/b , http://localhost:8082/boot1/c.png, 测试结果都能访问到对应的静态资源,会去上面的静态资源目录下去找。
2.3 欢迎页映射
欢迎页,静态资源文件夹下的所有index.html页面;被**映射;
查看欢迎页映射的源码WebMvcAutoConfigurationAdapter#welcomePageHandlerMapping
// WebMvcAutoConfiguration#WebMvcAutoConfigurationAdapter@Configuration@Import(EnableWebMvcConfiguration.class)@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter { // .... // welcomePageHandlerMapping @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping( ResourceProperties resourceProperties) { // 查看下面的WebMvcAutoConfiguration#WelcomePageHandlerMapping return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(), // this.mvcProperties.getStaticPathPattern()=favicon.ico都是在静态资源文件下查找。查看源码 WebMvcAutoConfigurationAdapter#FaviconConfiguration
// WebMvcAutoConfiguration#WebMvcAutoConfigurationAdapter@Configuration@Import(EnableWebMvcConfiguration.class)@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter { // ... // WebMvcAutoConfigurationAdapter#FaviconConfiguration @Configuration @ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true) public static class FaviconConfiguration { private final ResourceProperties resourceProperties; public FaviconConfiguration(ResourceProperties resourceProperties) { this.resourceProperties = resourceProperties; } @Bean public SimpleUrlHandlerMapping faviconHandlerMapping() { SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", faviconRequestHandler())); return mapping; } // 查找favicon.ico的路径 -> resourceProperties.getFaviconLocations() @Bean public ResourceHttpRequestHandler faviconRequestHandler() { ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); requestHandler .setLocations(this.resourceProperties.getFaviconLocations()); return requestHandler; } }}ResourceProperties#getFaviconLocations
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)public class ResourceProperties implements ResourceLoaderAware { private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" }; private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; private static final String[] RESOURCE_LOCATIONS; static { RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length + SERVLET_RESOURCE_LOCATIONS.length]; System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0, SERVLET_RESOURCE_LOCATIONS.length); System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length); } private String[] staticLocations = RESOURCE_LOCATIONS; // ... // favicon.ico也是去项目类路径的静态资源默认目录下去查找 ListgetFaviconLocations() { List locations = new ArrayList ( this.staticLocations.length + 1); if (this.resourceLoader != null) { // this.staticLocations=RESOURCE_LOCATIONS -> CLASSPATH_RESOURCE_LOCATIONS for (String location : this.staticLocations) { locations.add(this.resourceLoader.getResource(location)); } } locations.add(new ClassPathResource("/")); return Collections.unmodifiableList(locations); }} 3. 模板引擎
JSP、Freemarker、Thymeleaf都是渲染界面的模板引擎技术. view + model :
SpringBoot推荐使用Thymeleaf,语法更简单,功能更强大。
3.1 引入thymeleaf
using-boot-starter 中有使用介绍, 引入thymeleaf的依赖.
org.springframework.boot spring-boot-starter-thymeleaf切换thymeleaf版本
1.8 3.0.2.RELEASE 2.1.1 3.2 Thymeleaf使用&语法
把html页面放在classpath:/templates/下,thymeleaf就能自动渲染。
@ConfigurationProperties(prefix = "spring.thymeleaf")public class ThymeleafProperties { private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8"); private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html"); // classpath:/templates/ 模板html页面放到目录下 public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html"; // .... private String prefix = DEFAULT_PREFIX; private String suffix = DEFAULT_SUFFIX; // ....}编写测试案例, 在classpath:/templates/下添加success.html页面
需要导入thymeleaf的名称空间 xmlns:th="http://www.thymeleaf.org"th:text 改变当前元素里面的文本内容;
th: 任意html属性;来替换原生属性的值。
hello success
这里是欢迎信息
编写controller映射方法
@Controllerpublic class HelloController { @RequestMapping("/success") public String success(Mapmap) { //classpath:/templates/success.html map.put("hello", " 你好
"); //map.put("users", Arrays.asList("zhangsan", "wangwu", "lisi")); return "success"; }}启动应用,访问 http://localhost:8082/boot1/success ,成功找到success.html页面。
th:id, th:class 替换了原来的属性值.
更多thymeleaf用法可以查看帮助文档Thymeleaf官方教程
Thymeleaf中文教程
4. SpringMVC自动配置
包 org.springframework.boot.autoconfigure.web下配置了web的所有自动化场景组件.
4.1 Auto-Configuration
[SpringMVC Auto-Configuration 官方文档]( Spring Boot Reference Guide ), 自动配置主要在下面几个方面:
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
The auto-configuration adds the following features on top of Spring’s defaults:
- Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
- 自动配置了视图解析器ViewResolver, 根据方法的返回值得到视图对象View, 视图对象决定如何渲染, 可能转发或重定向等等;
- ContentNegotiatingViewResolver 组合所有的视图解析器;
- 自定义视图解析器, 只需要给容器中添加一个自定义的视图解析器, SpringMVC会自动将其整合进来;
- Support for serving static resources, including support for WebJars (see below).
- 支持静态资源文件夹路径, webjars的访问
- Automatic registration of Converter, GenericConverter, Formatter beans.
- Convert 转换器, 可以实现类型转换
- Formatter 格式化器, 日期格式化等.
- Support for HttpMessageConverters (see below).
- HttpMessageConvert 用来转换http请求和响应的;
- 自己给容器中添加HttpMessageConvert, 只需要将自定义组件注册到容器中(@Bean, @Component)
- Automatic registration of MessageCodesResolver (see below).
- 定义错误代码生成规则
- Static index.html support. 支持静态首页的访问配置
- Custom Favicon support (see below). 支持自定义 favicon图标
- Automatic use of a ConfigurableWebBindingInitializer bean (see below).
- 配置ConfigurableWebBindingInitializer可以替换默认的web初始化器, 使用自定义的.
4.2 扩展SpringMVC
编写一个JavaConfig类, 继承WebMvcConfigurerAdapter, 从而来实现Web功能扩展开发.
注意: 不能在该配置类上标注@EnableWebMvc, 否则它会全面接管默认的MVC配置, 使用自定义的.
@Configurationpublic class MyMvcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { super.addViewControllers(registry); // 浏览器发送/atguigu请求来到success页面 registry.addViewController("/atguigu").setViewName("success"); }}
上面的配置对应到xml是这样的:
controller编写
@Controllerpublic class HelloController { @RequestMapping("/success") public String success(Mapmap) { //classpath:/templates/success.html map.put("hello", " 你好
"); map.put("users", Arrays.asList("zhangsan", "wangwu", "lisi")); return "success"; }}
打开浏览器, 访问 http://localhost:8082/boot1/atguigu, 会转发到success.html界面.
4.3 MVC自动配置原理
WebMvcAutoConfiguration是SpringMVC的自动配置类, 在自动配置WebMvcAutoConfigurationAdapter时会导入 EnableWebMvcConfiguration, 包含我们的扩展配置, 容器中所有的WebMvcConfigurer都会起作用。
@Configuration@ConditionalOnWebApplication@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class })@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class })public class WebMvcAutoConfiguration { // ... // WebMvcAutoConfigurationAdapter @Configuration @Import(EnableWebMvcConfiguration.class) // @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter { // ... } // Configuration equivalent to {@code @EnableWebMvc}. @Configuration public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration { // ... //从DelegatingWebMvcConfiguration继承过来的配置,容器中所有的WebMvcConfigurer都会起作用 private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); // 从容器中获取所有的WebMvcConfigurer @Autowired(required = false) public void setConfigurers(Listconfigurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } } // 将所有的注册到容器的,包含自定义的viewController添加到WebMvcConfigurer @Override protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); } }}
4.4 全面接管SpringMVC
如果SpringBoot不适用SpringMVC的默认自动配置,转而使用我们自定义的配置,只需要在配置类上添加@EnableWebMvc即可, 它会导致所有的SpringMVC的自动配置都失效。
@EnableWebMvc // 全面接管SpringMVC的自动配置@Configurationpublic class MyMvcConfig extends WebMvcConfigurerAdapter { // ...}
为什么添加@EnableWebMvc注解后,会导致所有的SpringMVC的自动配置都失效呢 ?
从源码分析EnableWebMvc导入了DelegatingWebMvcConfiguration
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@Documented@Import(DelegatingWebMvcConfiguration.class)public @interface EnableWebMvc {}
DelegatingWebMvcConfiguration对WebMvc各个场景的基本功能配置提供了支持。
@Configurationpublic class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); @Autowired(required = false) public void setConfigurers(Listconfigurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } } // .... }
SpringMvc的自动配置类WebMvcAutoConfiguration只会在容器中没有WebMvcConfigurationSupport组件时才会生效, 所以如果在自定义配置类上添加了@EnableWebMvc注解, 就相当于给容器中注册了WebMvcConfigurationSupport组件, 会导致mvc默认的自动配置类WebMvcAutoConfiguration失效.
@Configuration@ConditionalOnWebApplication@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class })// 容器中没有WebMvcConfigurationSupport时, 自动配置类才会生效.@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class })public class WebMvcAutoConfiguration { // ...}
5. 如何修改SpringBoot的默认配置
1)SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component),如果有就用用户自己配置的;如果没有,才自动配置;如果有些组件可以使用多个(比如ViewResolver),就会将用户配置的和系统默认的组合起来。
2)在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置。
3)在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置。
6. 访问指定的首页
映射器没有映射到指定url会去当前项目静态目录(/resources, /static, /public)下面去查找匹配index.html
如果映射器映射到指定url,会自动匹配映射逻辑指定的视图view。
@Controllerpublic class HelloController { @RequestMapping(path = {"/", "/index", "/index.html"}) public String index() { return "login"; }}
浏览器访问 http://localhost:8082/boot1/, http://localhost:8082/boot1/index和 http://localhost:8082/boot1/index.html , 映射器都会去静态资源目录下匹配指定的login.html. 这是SpringMvc中映射器的实现效果.
我们也可以不使用默认的映射器实现上面的效果, 我们首先注释掉 HelloController#index(...)
@Controllerpublic class HelloController { }
而是使用自定义的映射器去给url指定访问的资源, 可以通过实现WebMvcConfigurerAdapter来扩展SpringMvc的功能.
@Configurationpublic class MyMvcConfig extends WebMvcConfigurerAdapter { // 添加自定义url映射视图关系 @Bean public WebMvcConfigurerAdapter webMvcConfigurerAdapter() { WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() { @Override public void addViewControllers(ViewControllerRegistry registry) { // 映射到登录页面login.html registry.addViewController("/").setViewName("login"); // 映射到登录页面login.html registry.addViewController("/index.html").setViewName("login"); // 登录成功后的展示页面dashboard.html registry.addViewController("/main.html").setViewName("dashboard"); } }; return adapter; }}
重启应用后, 浏览器访问 http://localhost:8082/boot1/ , http://localhost:8082/boot1/index.html; 可以看到实现了映射到指定资源.
7. 国际化
SpringBoot应用中实现国际化的操作步骤有:
- 编写国际化配置文件
- 使用ResourceBundleMessageSource管理国际化资源文件
- 在页面使用fmt:message取出国际化内容
编写国际化配置文件,抽取页面需要展示的国际化消息
SpringBoot自动配置好了管理国际化资源文件的组件 MessageSourceAutoConfiguration
@Configuration@ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@Conditional(ResourceBundleCondition.class)@EnableConfigurationProperties@ConfigurationProperties(prefix = "spring.messages")public class MessageSourceAutoConfiguration { // 国际化配置文件可以直接放在类路径下, 定义为messages.properties, 系统默认识别并解析该文件 // 也可以在全局配置文件中使用spring.messages.basename修改国际化文件读取目录 private String basename = "messages";}
修改国际化配置文件的读取基础名
# 配置国际化文件基础名spring.messages.basename=international.login
去页面login.html获取国际化信息的值 , 使用了thymeleaf模板引擎.
登录页面 登录页面
SpringMvc默认根据当前请求头中携带的区域信息locale来进行配置国际化.
WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#localeResolver源码分析:
@Configuration@ConditionalOnWebApplication@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class })@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class })public class WebMvcAutoConfiguration { // ... // WebMvcAutoConfigurationAdapter @Configuration @Import(EnableWebMvcConfiguration.class) // @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter { // ... // LocaleResolver @Bean @ConditionalOnMissingBean // 读取全局配置文件中的spring.mvc.locale区域信息进行国际化配置 // FIXED : Always use the configured locale. // ACCEPT_HEADER: Use the "Accept-Language" header or the configured locale if the header is not set. @ConditionalOnProperty(prefix = "spring.mvc", name = "locale") public LocaleResolver localeResolver() { if (this.mvcProperties .getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.mvcProperties.getLocale()); } // 如果全局配置文件中的spring.mvc.locale没有指定, 就根据请求头中的AcceptHeaderLocale获取区域信息, 进行国际化配置 AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); localeResolver.setDefaultLocale(this.mvcProperties.getLocale()); return localeResolver; } }}
AcceptHeaderLocaleResolver中根据Accept-Language中的区域信息来配置国际化.
public class AcceptHeaderLocaleResolver implements LocaleResolver { private Locale defaultLocale; public void setDefaultLocale(Locale defaultLocale) { this.defaultLocale = defaultLocale; } public Locale getDefaultLocale() { return this.defaultLocale; } // resolveLocale @Override public Locale resolveLocale(HttpServletRequest request) { Locale defaultLocale = getDefaultLocale(); // 如果Accept-Language为空,就使用默认的国际化配置 if (defaultLocale != null && request.getHeader("Accept-Language") == null) { return defaultLocale; } Locale requestLocale = request.getLocale(); if (isSupportedLocale(requestLocale)) { return requestLocale; } Locale supportedLocale = findSupportedLocale(request); if (supportedLocale != null) { return supportedLocale; } return (defaultLocale != null ? defaultLocale : requestLocale); }}
也可以在界面点击切换链接进行国际化切换,在切换链接后面指定国际化区域信息 /login.html?l=zh_CN
中文 English
在WebMvcConfigurerAdapter中扩展自定义 LocaleResolver
@Configurationpublic class MyMvcConfig extends WebMvcConfigurerAdapter { // ... // 使用自定义的MyLocaleResolver @Bean public LocaleResolver localeResolver() { return new MyLocaleResolver(); }}public class MyLocaleResolver implements LocaleResolver { @Override public Locale resolveLocale(HttpServletRequest request) { // 获取/login.html?l=zh_CN中的请求参数l String l = request.getParameter("l"); Locale locale = null; if (!StringUtils.isEmpty(l)) { String[] sp = l.split("_"); // en_US zh_CN locale = new Locale(sp[0], sp[1]); // language,country } else { // 这里必须默认初始化一个bean,如果将该国际化配置到springBoot中,第一次进入界面调用国际化组件没有参数l,返回null,会报空指针 locale = new Locale("zh", "CN"); } return locale; } @Override public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) { }}
重启应用后,访问 http://localhost:8082/boot1/index.html, http://localhost:8082/boot1/index
点击【中文 English】链接切换国际化, 地址栏变化http://localhost:8082/boot1/index.html?l=en_US
开发期间thymeleaf模板引擎页面修改后要实时生效, 可以在全局配置文件禁用缓存
#禁用缓存spring.thymeleaf.cache=false
8. RestfulCRUD
8.1 拦截器
登录提交的校验可以使用拦截器, 下面我们写一个拦截器, 来实现如果用户没登录就不允许访问/main.html
public class LoginHandlerIntercepter implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { Object loginUser = httpServletRequest.getSession().getAttribute("loginUser"); System.out.println("拦截器登录校验 loginUser=" + loginUser); if (loginUser == null) { // 没有登录,返回登录页面 httpServletRequest.setAttribute("msg", "没有权限,请先登录"); httpServletRequest.getRequestDispatcher("/index.html").forward(httpServletRequest, httpServletResponse); return false; } // 已经登录,放行 return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { }}
在配置类中扩展SpringMvc的功能, 增加拦截器.
@Configurationpublic class MyMvcConfig extends WebMvcConfigurerAdapter { // 添加自定义url映射视图关系 @Bean public WebMvcConfigurerAdapter webMvcConfigurerAdapter() { WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("login"); registry.addViewController("/index.html").setViewName("login"); // /main.html请求会映射到dashboard.html界面 registry.addViewController("/main.html").setViewName("dashboard"); } // 添加拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { // public static final String DEFAULT_METHOD_PARAM = "_method"; private String methodParam = DEFAULT_METHOD_PARAM; public void setMethodParam(String methodParam) { Assert.hasText(methodParam, "'methodParam' must not be empty"); this.methodParam = methodParam; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest requestToUse = request; // 如果表单提交的是post请求,进入转换逻辑 if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) { // 读取_method参数的值 String paramValue = request.getParameter(this.methodParam); // 判断_method值非空 if (StringUtils.hasLength(paramValue)) { // 将_method值替换到HttpServletRequest中,实现修改http请求方式的效果 requestToUse = new HttpMethodRequestWrapper(request, paramValue); } } // 过滤器继续执行,HttpServletRequest替换成了包装过的requestToUse filterChain.doFilter(requestToUse, response); } private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper { private final String method; public HttpMethodRequestWrapper(HttpServletRequest request, String method) { super(request); // 初始化将外部传入的_method值给到HttpServletRequest#method this.method = method.toUpperCase(Locale.ENGLISH); } // HttpMethodRequestWrapper父类实现了HttpServletRequest,重写getMethod方法, 最终映射handler读取的HTTP请求方式就是转换包装后的_method @Override public String getMethod() { return this.method; } }}public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest { // ... private HttpServletRequest _getHttpServletRequest() { return (HttpServletRequest) super.getRequest(); } @Override public String getMethod() { return this._getHttpServletRequest().getMethod(); } }
9. 错误处理机制
9.1 SpringBoot默认的错误处理机制
浏览器效果, 返回一个默认的错误页面
浏览器端发送请求的请求头中Accept中有text/html
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*errorpublic String getServletPrefix() { String result = this.servletPath; if (result.contains("*")) { result = result.substring(0, result.indexOf("*")); } if (result.endsWith("/")) { result = result.substring(0, result.length() - 1); } return result;}// ErrorProperties#getPath 获取系统出现异常后,错误处理的url: /errorpublic class ErrorProperties { @Value("${error.path:/error}") private String path = "/error"; public String getPath() { return this.path; }}
9.2.3 BasicErrorController
当系统出现错误时, springboot会将去寻找处理错误的url (/error), 然后使用/error去找到mvc中映射的handler处理错误.
// 处理错误的handler,默认路由到 /error 来处理@Controller@RequestMapping("${server.error.path:${error.path:/error}}")public class BasicErrorController extends AbstractErrorController { private final ErrorProperties errorProperties; // AbstractErrorController#errorAttributes private final ErrorAttributes errorAttributes; // Create a new {@link BasicErrorController} instance. public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, ListerrorViewResolvers) { super(errorAttributes, errorViewResolvers); Assert.notNull(errorProperties, "ErrorProperties must not be null"); this.errorProperties = errorProperties; } @Override public String getErrorPath() { return this.errorProperties.getPath(); } // 如果请求头中的Accept=text/html, 会在该方法中处理, 一般处理来自浏览器端的请求 @RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { // 获取http-status HttpStatus status = getStatus(request); // 将异常信息封装到model中 Map model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); // 设置http请求状态 response.setStatus(status.value()); // 解析model数据并返回view ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView == null ? new ModelAndView("error", model) : modelAndView); } // 其他客户端发送的请求,出现异常时使用该方法处理并产生Json类型的响应数据 @RequestMapping @ResponseBody // response json public ResponseEntity
9.2.4 DefaultErrorViewResolver
BasicErrorController#errorHtml中使用到了resolveErrorView进行错误处理的视图解析.
// 继承自AbstractErrorController的处理异常的视图解析器private final ListerrorViewResolvers;// BasicErrorController#resolveErrorViewprotected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map model) { for (ErrorViewResolver resolver : this.errorViewResolvers) { // ErrorViewResolver#resolveErrorView 视图解析 ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null;}
DefaultErrorViewResolver#resolveErrorView
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { private static final MapSERIES_VIEWS; static { Map views = new HashMap (); // 按照请求状态码进行匹配 /error/4xx.html, /error/5xx.html 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 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 model) { // viewName是请求状态码, 拼接后/error/400.html String errorViewName = "error/" + viewName; // 模板引擎可以解析这个页面地址的话, 就用这个页面地址封装视图 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders // TemplateAvailabilityProviders#getProvider .getProvider(errorViewName, this.applicationContext); if (provider != null) { return new ModelAndView(errorViewName, model); } // 模板引擎找不到对应的页面,就去类路径下去找 return resolveResource(errorViewName, model); } private ModelAndView resolveResource(String viewName, Map model) { for (String location : this.resourceProperties.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { // 如果找到自定义的错误处理的页面,封装View return new ModelAndView(new HtmlResourceView(resource), model); } } catch (Exception ex) { } } return null; } @Override public int getOrder() { return this.order; } public void setOrder(int order) { this.order = order; } private static class HtmlResourceView implements View { private Resource resource; HtmlResourceView(Resource resource) { this.resource = resource; } @Override public String getContentType() { return MediaType.TEXT_HTML_VALUE; } // 视图渲染 @Override public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { response.setContentType(getContentType()); FileCopyUtils.copy(this.resource.getInputStream(), response.getOutputStream()); } }}
TemplateAvailabilityProviders#getProvider
public class TemplateAvailabilityProviders { // 配置的可用模板 private final Listproviders; public TemplateAvailabilityProviders(ClassLoader classLoader) { Assert.notNull(classLoader, "ClassLoader must not be null"); // 从spring.factories中加载Template availability providers //FreeMarkerTemplateAvailabilityProvider,ThymeleafTemplateAvailabilityProvider... this.providers = SpringFactoriesLoader .loadFactories(TemplateAvailabilityProvider.class, classLoader); } public TemplateAvailabilityProvider getProvider(String view, Environment environment, ClassLoader classLoader, ResourceLoader resourceLoader) { // .... RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver( environment, "spring.template.provider."); if (!propertyResolver.getProperty("cache", Boolean.class, true)) { // findProvider 匹配模板 return findProvider(view, environment, classLoader, resourceLoader); } TemplateAvailabilityProvider provider = this.resolved.get(view); if (provider == null) { synchronized (this.cache) { provider = findProvider(view, environment, classLoader, resourceLoader); provider = (provider == null ? NONE : provider); this.resolved.put(view, provider); this.cache.put(view, provider); } } return (provider == NONE ? null : provider); } private TemplateAvailabilityProvider findProvider(String view, Environment environment, ClassLoader classLoader, ResourceLoader resourceLoader) { for (TemplateAvailabilityProvider candidate : this.providers) { // 从候选模板中匹配可用的模板页面 if (candidate.isTemplateAvailable(view, environment, classLoader, resourceLoader)) { return candidate; } } return null; }}
ThymeleafTemplateAvailabilityProvider#isTemplateAvailable
public class ThymeleafTemplateAvailabilityProvider implements TemplateAvailabilityProvider { @Override public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader, ResourceLoader resourceLoader) { if (ClassUtils.isPresent("org.thymeleaf.spring4.SpringTemplateEngine", classLoader)) { PropertyResolver resolver = new RelaxedPropertyResolver(environment, "spring.thymeleaf."); String prefix = resolver.getProperty("prefix", ThymeleafProperties.DEFAULT_PREFIX); // classpath:/templates/ String suffix = resolver.getProperty("suffix", ThymeleafProperties.DEFAULT_SUFFIX); // .html // classpath:/templates/404.html return resourceLoader.getResource(prefix + view + suffix).exists(); } return false; }}
9.2.5 DefaultErrorAttributes
BasicErrorController解析浏览器端的请求或其他客户端请求的errorHtml和error的处理中都有获取异常相关信息的操作, 是从DefaultErrorAttributes中获取.
@Order(Ordered.HIGHEST_PRECEDENCE)public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { // .... // 获取异常信息 @Override public MapgetErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map errorAttributes = new LinkedHashMap (); // 存入时间戳 errorAttributes.put("timestamp", new Date()); // 存入响应状态码 addStatus(errorAttributes, requestAttributes); // 存入异常信息 addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); // 存入请求uri addPath(errorAttributes, requestAttributes); return errorAttributes; } private void addStatus(Map errorAttributes, RequestAttributes requestAttributes) { // 获取异常的响应码javax.servlet.error.status_code的值 Integer status = getAttribute(requestAttributes, "javax.servlet.error.status_code"); if (status == null) { errorAttributes.put("status", 999); errorAttributes.put("error", "None"); return; } errorAttributes.put("status", status); try { errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase()); } catch (Exception ex) { // Unable to obtain a reason errorAttributes.put("error", "Http Status " + status); } } private void addErrorDetails(Map errorAttributes, RequestAttributes requestAttributes, boolean includeStackTrace) { Throwable error = getError(requestAttributes); if (error != null) { while (error instanceof ServletException && error.getCause() != null) { error = ((ServletException) error).getCause(); } errorAttributes.put("exception", error.getClass().getName()); addErrorMessage(errorAttributes, error); if (includeStackTrace) { addStackTrace(errorAttributes, error); } } // 获取异常信息javax.servlet.error.message的值 Object message = getAttribute(requestAttributes, "javax.servlet.error.message"); if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null) && !(error instanceof BindingResult)) { // 存入异常信息message errorAttributes.put("message", StringUtils.isEmpty(message) ? "No message available" : message); } } private void addErrorMessage(Map errorAttributes, Throwable error) { BindingResult result = extractBindingResult(error); if (result == null) { errorAttributes.put("message", error.getMessage()); return; } if (result.getErrorCount() > 0) { errorAttributes.put("errors", result.getAllErrors()); errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. Error count: " + result.getErrorCount()); } else { errorAttributes.put("message", "No errors"); } } private void addPath(Map errorAttributes, RequestAttributes requestAttributes) { // 获取请求uri String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri"); if (path != null) { errorAttributes.put("path", path); } } @Override public Throwable getError(RequestAttributes requestAttributes) { Throwable exception = getAttribute(requestAttributes, ERROR_ATTRIBUTE); if (exception == null) { exception = getAttribute(requestAttributes, "javax.servlet.error.exception"); } return exception; } @SuppressWarnings("unchecked") private T getAttribute(RequestAttributes requestAttributes, String name) { return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST); }}
9.3 定制异常响应页面
9.3.1 有模板引擎的场景
将错误页面命名为 错误状态码.html放在模板引擎下的error文件夹下,发生此状态码的错误就会来到对应的页面。error/状态码.html;
我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确匹配优先(优先寻找精确的errorCode.html页面)。
// DefaultErrorViewResolver#SERIES_VIEWS, 保存请求出现错误时的默认处理页面前缀4xx 5xxprivate static final MapSERIES_VIEWS;static { Map views = new HashMap (); views.put(Series.CLIENT_ERROR, "4xx"); views.put(Series.SERVER_ERROR, "5xx"); SERIES_VIEWS = Collections.unmodifiableMap(views);}// DefaultErrorViewResolver#resolveErrorView@Overridepublic ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map model) { // 优先精确匹配, 错误响应状态码 404 ,500 ModelAndView modelAndView = resolve(String.valueOf(status), model); // 如果精确匹配失败,就按照默认规则找4xx, 5xx if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { // status.series()就是用错误响应状态码/100,返回匹配上的枚举, 查看HttpStatus.Series#valueOf // 然后再从SERIES_VIEWS中获取是4xx还是5xx页面前缀 modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView;}private ModelAndView resolve(String viewName, Map model) { // viewName就是精确匹配的错误响应状态码或默认匹配的4xx,5xx String errorViewName = "error/" + viewName; // error/4xx TemplateAvailabilityProvider provider = this.templateAvailabilityProviders .getProvider(errorViewName, this.applicationContext); if (provider != null) { // 模板解析完成后, 得到完整的异常处理页面路径 error/4xx.html return new ModelAndView(errorViewName, model); } // 如果模板引擎找不到, 就去"classpath:/META-INF/resources/", "classpath:/resources/", // "classpath:/static/", "classpath:/public/" 去找 error/4xx.html return resolveResource(errorViewName, model);}
DefaultErrorAttributes使得页面能获取的信息:
-
timestamp 时间戳
-
status 状态码
-
error 错误提示
-
exception 异常
-
message 异常消息
-
path 请求url
/error/4xx.html页面
4xx异常 系统异常
time:[[(${#dates.format(timestamp,'yyyy-MM-dd HH:mm:ss')})]]
status:[[(${status})]]
error:[[(${error})]]
exception:[[(${exception})]]
message:[[(${message})]]
errors:[[(${errors})]]
trace:[[(${trace})]]
path:[[(${path})]]
启动应用后, 访问一个不存在的请求url, 如果没有提供精确匹配的404.html页面, 会继续匹配系统默认的4xx.html界面渲染异常信息.
如果提供了404.html页面, 就会优先精确匹配的404.html页面来渲染异常信息.
9.3.2 无模板引擎的场景
模板引擎找不到这个错误页面,就去静态资源文件夹下找。
"classpath:/META-INF/resources/""classpath:/resources/""classpath:/static/""classpath:/public/" "/" 当前项目的根路径
以上都没有错误页面,就来到SpringBoot默认的错误页面渲染异常信息。
具体源码在ErrorMvcAutoConfiguration中:
@Configuration@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)@Conditional(ErrorTemplateMissingCondition.class)protected static class WhitelabelErrorViewConfiguration { // 创建默认的异常渲染界面 private final SpelView defaultErrorView = new SpelView( "Whitelabel Error Page
" + "This application has no explicit mapping for /error, so you are seeing this as a fallback.
" + "${timestamp}" + "There was an unexpected error (type=${error}, status=${status})." + "${message}"); // 异常渲染的viewBean @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } // If the user adds @EnableWebMvc then the bean name view resolver from // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment. @Bean @ConditionalOnMissingBean(BeanNameViewResolver.class) public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; }}
9.4 定制异常响应Json
9.4.1 自定义异常处理
自定义用户不存在的异常类
public class UserNotExistException extends Exception { public UserNotExistException(String message) { super(message); }}
模拟触发异常的请求, 只要访问/exception/username?username=aaa就会触发.
@RestController@RequestMapping(value = {"/exception"})public class ExceptionTestController { @RequestMapping("/username") @ResponseBody public String user(@RequestParam("username") String username) throws UserNotExistException { if ("aaa".equals(username)) { throw new UserNotExistException("用户不存在!"); } return username; }}
使用全局异常处理类来捕获异常及异常处理.
package com.aiguigu.springboot02config.exception.handler;import com.aiguigu.springboot02config.exception.UserNotExistException;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;import java.util.HashMap;import java.util.Map;@ControllerAdvice(basePackages = {"com.aiguigu.springboot02config.controller"})public class MyExceptionHandler { // 自定义异常处理 // 浏览器客户端都返回的json数据,无法实现自适应效果(浏览器返回页面,其他客户端返回json数据) @ResponseBody @ExceptionHandler(UserNotExistException.class) public MaphandleException(Exception e) { Map map = new HashMap<>(); map.put("code", "user.not_exist"); map.put("message", e.getMessage()); return map; }}
请求 http://localhost:8082/boot1/user?user=aaa ,浏览器和postman请求都返回自定义的json数据, 没有实现自适应响应效果。
{"code":"user.not_exist","message":"用户不存在!"}
9.4.2 自适应响应
可以将异常请求转发到/error进行自适应响应 , 在全局异常处理类中加入自定义异常信息并转发到/error. SpringBoot默认的异常处理请求就是 /error. (在BasicErrorController源码中查看.)
@ControllerAdvice(basePackages = {"com.aiguigu.springboot02config.controller"})public class MyExceptionHandler { // 对UserNotExistException进行拦截处理 @ExceptionHandler(UserNotExistException.class) public String handleException2(Exception e, HttpServletRequest request) { Mapmap = new HashMap<>(); map.put("code", "user.not_exist"); map.put("message", "用户出错了. " + e.getMessage()); // 传入我们自己的错误状态码, 进入我们指定的错误页面 request.setAttribute("javax.servlet.error.status_code", 500); // 转发到 /error 实现自适应效果(浏览器返回页面,其他客户端返回json数据) ErrorMvcAutoConfiguration return "forward:/error"; }}
测试浏览器效果:
其他客户端请求效果:
{ "timestamp": 1682520309898, "status": 500, "error": "Internal Server Error", "exception": "com.aiguigu.springboot02config.exception.UserNotExistException", "message": "用户不存在!", "path": "/boot1/exception/username"}
9.4.3 传递定制数据
请求出现异常后,会来到/error请求,会被BasicErrorController处理,响应数据是由getErrorAttributes得到的(父类AbstractErrorController的方法);
-
编写一个ErrorController的实现类或AbstractErrorController的子类,放在容器中。
- 异常响应数据是通过errorAttributes.getErrorAttributes方法得到的;容器中的DefaultErrorAttributes#getErrorAttributes()方法,默认进行数据处理。
- 在全局异常处理类中将需要响应的定制异常信息存入到requst域中.
在全局异常处理类中将需要传递的信息保存到自定义字段ext中.
// 对UserNotExistException进行拦截处理@ExceptionHandler(UserNotExistException.class)public String handleException2(Exception e, HttpServletRequest request) { Mapmap = new HashMap<>(); map.put("code", "user.not_exist"); map.put("message", "用户出错了. " + e.getMessage()); // 传入我们自己的错误状态码, 进入我们指定的错误页面 request.setAttribute("javax.servlet.error.status_code", 500); // 将map信息存入自定义的ext字段 request.setAttribute("ext", map); // 转发到 /error 实现自适应效果(浏览器返回页面,其他客户端返回json数据) ErrorMvcAutoConfiguration return "forward:/error";}
自定义MyErrorAttributes,继承DefaultErrorAttributes,重写getErrorAttributes方法,响应数据map可以存入我们自定义的信息, 还能从request域对象中取出之前存入的信息ext。
// 给容器加入自定义的 ErrorAttributes@Componentpublic class MyErrorAttributes extends DefaultErrorAttributes { // 返回值map就是页面和json能获取的所有字段 @Override public MapgetErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map map = super.getErrorAttributes(requestAttributes, includeStackTrace); map.put("company", "atguigu"); // 我们的异常处理器携带的数据 Map ext = (Map ) requestAttributes.getAttribute("ext", RequestAttributes.SCOPE_REQUEST); map.put("ext", ext); return map; }}
浏览器测试效果
其他客户端测试效果, 返回了company, ext自定义字段信息.
{ "timestamp": 1682519269726, "status": 500, "error": "Internal Server Error", "exception": "com.aiguigu.springboot02config.exception.UserNotExistException", "message": "用户不存在!", "path": "/boot1/exception/username", "company": "atguigu", "ext": { "code": "user.not_exist", "message": "用户出错了. 用户不存在!" }}