Chapter4: SpringBoot与Web开发1

尚硅谷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/**")) {
                // 所有/webjars/**,都去classpath:/META-INF/resources/webjars/下面找资源
				customizeResourceHandlerRegistration(
						registry.addResourceHandler("/webjars/**")
								.addResourceLocations(
										"classpath:/META-INF/resources/webjars/")
						.setCachePeriod(cachePeriod));
			}
            // "/**"访问当前项目的任何静态资源,从当前项目静态资源文件夹下查找。
			String staticPathPattern = this.mvcProperties.getStaticPathPattern();
			if (!registry.hasMappingForPattern(staticPathPattern)) {
				customizeResourceHandlerRegistration(
						registry.addResourceHandler(staticPathPattern)
								.addResourceLocations(
							// 当前项目类路径下的静态资源目录 
                               /*"classpath:/META-INF/resources/", 
                                *"classpath:/resources/",
                                *"classpath:/static/", 
                                *"classpath:/public/" 
                                */
                                    this.resourceProperties.getStaticLocations())
						.setCachePeriod(cachePeriod));
			}
		}
	}

}

2.1 访问/webjars/**

经过查看WebMvcAutoConfigurationAdapter#addResourceHandlers源码, 访问所有的/webjars/**,都会去类路径下的classpath:/META-INF/resources/webjars/下面找资源;
https://www.webjars.org 提供了webjars的库, 我们来引入jQuery的依赖.

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.3.1</version>
</dependency>

引入之后的依赖效果:
在这里插入图片描述
启动应用后, 访问 http://localhost:8082/boot1/webjars/jquery/3.3.1/jquery.js, 可以读取到jquery.js文件内容.
在这里插入图片描述

2.2 访问当前项目下的静态资源

一样从上面的源码中发现, 访问/** 会从当前项目静态资源目录下查找需要的静态资源文件, 项目的静态资源目录如下:

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/

WebMvcAutoConfigurationAdapter#addResourceHandlers 追踪源码分析

静态资源访问url匹配 staticPathPattern=/**

// 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/**")) {
			// 所有/webjars/**,都去classpath:/META-INF/resources/webjars/下面找资源
			customizeResourceHandlerRegistration(
					registry.addResourceHandler("/webjars/**")
							.addResourceLocations(
									"classpath:/META-INF/resources/webjars/")
					.setCachePeriod(cachePeriod));
		}
		// "/**"访问当前项目的任何静态资源,从当前项目静态资源文件夹下查找。
		String staticPathPattern = this.mvcProperties.getStaticPathPattern();
		if (!registry.hasMappingForPattern(staticPathPattern)) {
			customizeResourceHandlerRegistration(
					registry.addResourceHandler(staticPathPattern)
							.addResourceLocations(
						// 当前项目类路径下的静态资源目录 
						   /*"classpath:/META-INF/resources/", 
							*"classpath:/resources/",
							*"classpath:/static/", 
							*"classpath:/public/" 
							*/
								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);
	}

	/**
	 * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
	 * /resources/, /static/, /public/] plus context:/ (the root of the servlet context).
	 */
	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()=/**
				this.mvcProperties.getStaticPathPattern());
	}
}
// WebMvcAutoConfiguration#WelcomePageHandlerMapping
static final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping {
    // ...
    // 如果访问路径匹配/**, 系统自动生产handler(Controller)并设置view为index.html,并转发到首页index.html
    private WelcomePageHandlerMapping(Resource welcomePage,
				String staticPathPattern) {
			if (welcomePage != null && "/**".equals(staticPathPattern)) {
				logger.info("Adding welcome page: " + welcomePage);
				ParameterizableViewController controller = new ParameterizableViewController();
				controller.setViewName("forward:index.html");
				setRootHandler(controller);
				setOrder(0);
			}
		}
}

ResourceProperties#getWelcomePage

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware {
    // ...
    // getWelcomePage -> getStaticWelcomePageLocations 将/index.html封装到Resource对象
    public Resource getWelcomePage() {
		for (String location : getStaticWelcomePageLocations()) {
			Resource resource = this.resourceLoader.getResource(location);
			try {
				if (resource.exists()) {
					resource.getURL();
					return resource;
				}
			}
			catch (Exception ex) {
				// Ignore
			}
		}
		return null;
	}

    // 查找项目路径下的index.html, 查找链  /** -> resources/static,public,resources目录下的index.html
	private String[] getStaticWelcomePageLocations() {
		String[] result = new String[this.staticLocations.length];
		for (int i = 0; i < result.length; i++) {
			String location = this.staticLocations[i];
			if (!location.endsWith("/")) {
				location = location + "/";
			}
			result[i] = location + "index.html";
		}
		return result;
	}
}

访问 http://localhost:8082/boot1/,也会去静态资源目录下面去找index.html,先匹配上哪个展示哪个目录下的index页面。
在这里插入图片描述

2.4 图标映射

所有的**/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);
	}

	/**
	 * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
	 * /resources/, /static/, /public/] plus context:/ (the root of the servlet context).
	 */
	private String[] staticLocations = RESOURCE_LOCATIONS;
    
    // ...
    // favicon.ico也是去项目类路径的静态资源默认目录下去查找
    List<Resource> getFaviconLocations() {
		List<Resource> locations = new ArrayList<Resource>(
				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的依赖.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

切换thymeleaf版本

<properties>
	<java.version>1.8</java.version>
	<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
	<!--布局功能的支持程序 thymeleaf3主程序 layout2以上版本-->
	<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
</properties>

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";
    // ....
    /**
	 * 可以在全局配置文件中指定模板文件存放的路径和模板文件后缀
	 * spring.thymeleaf.prefix=...
	 * spring.thymeleaf.suffix=...
	 */
	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属性;来替换原生属性的值。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
	<head>
		<meta charset="UTF-8">
		<title>hello</title>
	</head>
	<body>
		<h1>success</h1><br/>
        <!--th:text 设置div里面的文本内容-->
        <div id="div01" class="myDiv" th:id="div1" th:class="mydiv" th:utext="${hello}">
            这里是欢迎信息
        </div>
        <hr/>
        <div th:text="${hello}"></div>
        <div th:utext="${hello}"></div>
	</body>
</html>

编写controller映射方法

@Controller
public class HelloController {
    /**
     * 查出一些数据在页面展示
     * 映射到 /template/success.html
     *
     * @return
     */
    @RequestMapping("/success")
    public String success(Map<String, Object> map) {
        //classpath:/templates/success.html
        map.put("hello", "<h1>你好</h1>");
        //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配置, 使用自定义的.

@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        super.addViewControllers(registry);
        // 浏览器发送/atguigu请求来到success页面
        registry.addViewController("/atguigu").setViewName("success");
    }
}

上面的配置对应到xml是这样的:

<mvc:view-controller path="/atguigu" view-name="success"/>

controller编写

@Controller
public class HelloController {
    /**
     * 查出一些数据在页面展示
     * 映射到 /template/success.html
     *
     * @return
     */
    @RequestMapping("/success")
    public String success(Map<String, Object> map) {
        //classpath:/templates/success.html
        map.put("hello", "<h1>你好</h1>");
        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(List<WebMvcConfigurer> configurers) {
            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的自动配置
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
    // ...
}

为什么添加@EnableWebMvc注解后,会导致所有的SpringMVC的自动配置都失效呢 ?

从源码分析EnableWebMvc导入了DelegatingWebMvcConfiguration

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

DelegatingWebMvcConfiguration对WebMvc各个场景的基本功能配置提供了支持。

@Configuration
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);
		}
	}
  // ....   
}    

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。

@Controller
public class HelloController {
    /**
     * 映射到/template/login.html
     *
     * @return
     */
    @RequestMapping(path = {"/", "/index", "/index.html"})
    public String index() {
        return "login";
    }
}

浏览器访问 http://localhost:8082/boot1/, http://localhost:8082/boot1/indexhttp://localhost:8082/boot1/index.html , 映射器都会去静态资源目录下匹配指定的login.html. 这是SpringMvc中映射器的实现效果.
在这里插入图片描述
我们也可以不使用默认的映射器实现上面的效果, 我们首先注释掉 HelloController#index(...)

@Controller
public class HelloController {
    /**
     * 映射到/template/login.html
     *
     * @return
     */
   /* @RequestMapping(path = {"/", "/index", "/index.html"})
    public String index() {
        return "login";
    }*/
}

而是使用自定义的映射器去给url指定访问的资源, 可以通过实现WebMvcConfigurerAdapter来扩展SpringMvc的功能.

/**
 * 使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
 */
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
     /**
     * 所有的WebMvcConfigurerAdapter组件都会一起生效
     *
     * @return
     */
    // 添加自定义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模板引擎.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title th:text="#{login.tip}">登录页面</title>
    </head>
    <body>
        <h1 th:text="#{login.tip}">登录页面</h1>
        <div>
            <form action="dashboard.html" th:action="@{/user/login}" method="post">
                <span style="color: red" th:utext="${msg}"></span><br/>
                <input type="text" name="username" placeholder="请输入用户名" th:placeholder="#{login.username}"/><br/>
                <input type="password" name="password" placeholder="请输入密码" th:placeholder="#{login.password}"/><br/>
                <input type="checkbox" value="记住密码"/>[[#{login.remember}]]<br/>
                <input type="submit" value="登录" th:value="#{login.button}"/>
            </form>
        </div>
        <div>
            <a href="#">中文</a>
            <a href="#">English</a>
        </div>
    </body>
</html>

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

<div>
    <a th:href="@{/index.html(l='zh_CN')}">中文</a>
    <a th:href="@{/index.html(l='en_US')}">English</a>
</div>

WebMvcConfigurerAdapter中扩展自定义 LocaleResolver

/**
 * 使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
 */
@Configuration
public 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的功能, 增加拦截器.

@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
    /**
     * 所有的WebMvcConfigurerAdapter组件都会一起生效
     *
     * @return
     */
    // 添加自定义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) {
                // /** 拦截所有请求进行登录验证,但是排除第一次进入登录页面的请求
                registry.addInterceptor(new LoginHandlerIntercepter()).addPathPatterns("/**").excludePathPatterns("/index.html", "/user/login", "/");
               
            }
        };
        return adapter;
    }
}

login.html界面, 如果接口返回了错误msg, 就展示错误信息.

<div>
    <form action="dashboard.html" th:action="@{/user/login}" method="post">
        <span style="color: red" th:utext="${msg}" th:if="${not #strings.isEmpty(msg)}"></span><br/>
        <input type="text" name="username" placeholder="请输入用户名" th:placeholder="#{login.username}"/><br/>
        <input type="password" name="password" placeholder="请输入密码" th:placeholder="#{login.password}"/><br/>
        <input type="checkbox" value="记住密码"/>[[#{login.remember}]]<br/>
        <input type="submit" value="登录" th:value="#{login.button}"/>
    </form>
</div>

编写登录提交的接口controller

// 登录api
@Controller
public class LoginController {

    @PostMapping(value = "/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Map<String, Object> map, HttpSession session) {
        if (!StringUtils.isEmpty(username) && Objects.equals(CONSTANT.PASSWORD.getValue(), password)) {
            // 登录成功,设置session
            session.setAttribute("loginUser", username);
            //登录成功 防止表单重复提交,可以重定向到主页
            return "redirect:/main.html";
        } else {
            // 登录失败
            map.put("msg", "用户名或密码错误");
            return "login";
        }
    }

    private enum CONSTANT {
        PASSWORD("123456", "登录密码");

        private String value;
        private String msg;

        CONSTANT(String value, String msg) {
            this.value = value;
            this.msg = msg;
        }

        public String getValue() {
            return value;
        }

        public String getMsg() {
            return msg;
        }
    }
}

重启应用, 浏览器访问http://localhost:8082/boot1/user/login, 点击"登录"按钮提交, 拦截器中的登录校验生效, 出现接口返回的提示信息.
在这里插入图片描述
输入正确的用户,密码后, 重定向到 http://localhost:8082/boot1/main.html, 映射到dashboard.html界面.
在这里插入图片描述

dashboard.html源码

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>dashboard</title>
    </head>
    <body>
        <h1>dashboard</h1><br/>
        <h3>登录成功</h3>
        <!--th:fragment抽取公共片段, 片段名tolist, id选择器tolist-->
        <a th:href="@{/emps}" th:fragment="link" id="tolist">去到员工管理页面</a>
    </body>
</html>

8.2 CRUD员工

8.2.1 实验要求

(1) RestfulCRUD: CRUD满足Rest风格

URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作.

function普通CRUD(uri来区分操作)Restful CRUD
查询getEmpemp—GET
添加addEmp?xxxemp—POST
修改updateEmp?id=1&name=xxx&…emp/{id}—PUT
删除deleteEmp?id=1emp/{id}—DELETE

(2) 实验的请求架构

实验功能请求URI请求方式
查询所有员工empsGET
查询某个员工(来到修改页面)emp/1GET
来到添加页面empGET
添加员工empPOST
来到修改页面(查出员工信息回显)emp/1GET
修改员工empPUT
删除员工emp/1DELETE
8.2.2 员工列表

thymeleaf公共页面元素抽取

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>dashboard</title>
    </head>
    <body>
        <h1>dashboard</h1><br/>
        <h3>登录成功</h3>
        <!--th:fragment抽取公共片段, 片段名tolist, id选择器tolist-->
        <a th:href="@{/emps}" th:fragment="link" id="tolist">去到员工管理页面</a>
    </body>
</html>

thymeleaf页面引入公共片段

<!--dashboard页面的link公共片段引入-->
<!--片段名方式引入 link-->
<div th:insert="~{dashboard::link}"></div>
<!--选择器方式引入 tolist-->
<div th:replace="~{dashboard::#tolist}"></div>

三种引入功能片段的属性
th:insert 将公共片段整个插入到声明引入的元素中
th:replace 将引入的元素替换为公共判断
th:include 将被引入的片段的内容包含进声明的标签中

员工列表页面 list.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title th:text="员工信息列表">页面管理</title>
</head>
<body>
<span style="color: red" th:utext="${msg}"></span><br/>
<h1 th:text="员工列表"></h1>
<button><a th:href="@{/emp}">员工添加</a></button>
<table border="1">
    <tr>
        <td>id</td>
        <td>lastName</td>
        <td>email</td>
        <td>gender</td>
        <td>department</td>
        <td>birthday</td>
        <td>操作</td>
    </tr>
    <tr th:each="emp : ${emps}">
        <td th:text="${emp.id}"></td>
        <td th:text="${emp.lastName}"></td>
        <td th:text="${emp.email}"></td>
        <td>[[(${emp.gender}==0?'女':'男')]]</td>
        <td>[[(${emp.department.departmentName})]]</td>
        <td th:text="${#dates.format(emp.birthday,'yyyy/MM/dd HH:mm:ssSSS')}"></td>
        <td>
            <button><a th:href="@{/emp/}+${emp.id}">修改</a></button>
            <!--<form th:action="@{/emp/}+${emp.id}" method="post">
                <input type="hidden" name="_method" value="delete"/>
                <button type="submit" style="color: blueviolet">删除</button>
            </form>-->
            <!--上面删除的方式,每行都会产生一个表单,我们用js事件改进, 自定义属性del_uri-->
            <button style="color: blueviolet" class="deleteBtn" th:attr="del_uri=@{/emp/}+${emp.id}">删除</button>
        </td>
    </tr>
</table>
<!--dashboard页面的link公共片段引入-->
<!--片段名方式引入 link-->
<div th:insert="~{dashboard::link}"></div>
<!--选择器方式引入 tolist-->
<div th:replace="~{dashboard::#tolist}"></div>

<!--引入jQuery包-->
<script type="text/javascript" th:src="@{/webjars/jquery/3.3.1/jquery.js}"></script>
<!--创建删除按钮提交的form表单-->
<form id="deleteEmpForm" method="post">
    <input type="hidden" name="_method" value="delete"/>
</form>
<!--给“删除”按钮绑定点击事件-->
<script>
    $(".deleteBtn").click(function () {
        // 删除当前员工
        $("#deleteEmpForm").attr("action", $(this).attr("del_uri")).submit();
        return false;
    });
</script>
</body>
</html>

dashboard.html页面点击【去到员工管理页面】链接,访问/emps去到员工管理页面展示员工列表信息。

@Controller
public class EmployeeController {
     @Autowired
    EmployeeDao employeeDao;

    // 查询所有员工列表
    @RequestMapping(value = "/emps", method = RequestMethod.GET)
    public String list(Model model) {
        Collection<Employee> employees = employeeDao.getAll();
        // 放在请求域中进行共享
        model.addAttribute("emps", employees);
        // thymeleaf默认就会拼串儿,会拼接到类路径下  classpath:/templates/emp/list.html
        return "emp/list";
    } 
}

准备访问数据。

// 准备数据
@Repository
public class EmployeeDao {
    private static Map<Integer, Employee> employees = null;

    @Autowired
    private DepartmentDao departmentDao;

    static {
        employees = new HashMap<>();
        employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1, new Department(101, "D-AA"), new Date()));
        employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1, new Department(102, "D-BB"), new Date()));
        employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0, new Department(103, "D-CC"), new Date()));
        employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0, new Department(104, "D-DD"), new Date()));
        employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1, new Department(105, "D-EE"), new Date()));
    }

    private static Integer initId = 1006;

    // 添加or修改
    public void save(Employee employee) {
        if (employee.getId() == null) {
            employee.setId(initId++);
        }
        employee.setDepartment(departmentDao.getDepartment(employee.getDepartment().getId()));
        employees.put(employee.getId(), employee);
    }

    public Collection<Employee> getAll() {
        return employees.values();
    }

    public Employee get(Integer id) {
        return employees.get(id);
    }

    public void delete(Integer id) {
        employees.remove(id);
    }
}

@Repository
public class DepartmentDao {

    private static Map<Integer, Department> departments = null;

    static {
        departments = new HashMap<>();
        departments.put(101, new Department(101, "D-AA"));
        departments.put(102, new Department(102, "D-BB"));
        departments.put(103, new Department(103, "D-CC"));
        departments.put(104, new Department(104, "D-DD"));
        departments.put(105, new Department(105, "D-EE"));
    }

    public Collection<Department> getDepartments() {
        return departments.values();
    }

    public Department getDepartment(Integer id) {
        return departments.get(id);
    }
}

测试效果
在这里插入图片描述

8.2.3 添加员工
<button><a th:href="@{/emp}">员工添加</a></button>

点击【员工添加】按钮, 访问路径/emp,get请求,跳转到员工添加的录入界面 add.html

@Controller
public class EmployeeController {
    @Autowired
    EmployeeDao employeeDao;
    @Autowired
    DepartmentDao departmentDao;
    
    // ...
    // 来到员工添加页面
    @GetMapping("/emp")
    public String toAddPage(Model model) {
        // 来到添加页面,查出所有的部门,在页面显示
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments", departments);
        // thymeleaf默认就会拼串儿,会拼接到类路径下  classpath:/templates/emp/add.html
        return "emp/add";
    }
}    

add.html页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>员工添加页面</title>
</head>
<body>
<h1>员工添加</h1>
<form action="#" method="post" th:action="@{/emp}">
    <table border="1">
        <tr>
            <td>lastName:</td>
            <td><input type="text" name="lastName" placeholder="zhangsan"/></td>
        </tr>
        <tr>
            <td>email:</td>
            <td><input type="text" name="email" placeholder="zhangsan@aiguigu.com"/></td>
        </tr>
        <tr>
            <td>gender:</td>
            <td><input type="radio" name="gender" value="1"/><input type="radio" name="gender" value="0"/>
            </td>
        </tr>
        <tr>
            <td>department:</td>
            <td>
                <select name="department.id">
                    <option th:each="department:${departments}"
                            th:text="${department.id} +' '+ ${department.departmentName}"
                            th:value="${department.id}">
                        
                    </option>
                </select>
            </td>
        </tr>
        <tr>
            <td>birthday:</td>
            <td><input type="text" name="birthday"/></td>
        </tr>
        <tfoot>
        <tr>
            <td colspan="2" align="center"><input type="submit" value="添加"/></td>
        </tr>
        </tfoot>
    </table>
</form>

<div th:replace="~{dashboard::#tolist}"></div>
</body>
</html>

录入添加的员工信息
在这里插入图片描述
信息录入完后提交post请求到/emp

@Controller
public class EmployeeController {
    @Autowired
    EmployeeDao employeeDao;
    @Autowired
    DepartmentDao departmentDao;
    
    // ...
    // 员工添加 SpringMVC自动将请求参数和pojo对象映射,只要参数名和pojo属性名称一致即可
    @PostMapping("/emp")
    public String addEmp(Employee employee) {
        employeeDao.save(employee);
        System.out.println("add emp finished");
        return "redirect:/emps";
    }
}  

提交发现报错
在这里插入图片描述

原因:界面birthday映射到后台employee对象的birthday字段是日期类型,SpringBoot对SpringMVC自动配置的日期类型是yyyy/MM/dd,而我们填写的birthday是yyyyMMdd,可以修改映射配置即可。

# 日期映射格式
spring.mvc.date-format=yyyyMMdd

重新启动添加员工成功, 重定向到员工列表页面,可以看到刚添加的那条记录。
在这里插入图片描述

8.2.4 修改员工

员工列表页面新增“修改”按钮。

<td>
	<button><a th:href="@{/emp/}+${emp.id}">修改</a></button>
</td>

点击【修改】按钮,提交get请求到/emp/{id}, 查询要修改的员工信息回显到修改页面edit.html

@Controller
public class EmployeeController {
    @Autowired
    EmployeeDao employeeDao;
    @Autowired
    DepartmentDao departmentDao;
    
    // ...
    // 来到修改页面,查出当前员工,在修改页面回显
    @GetMapping("/emp/{id}")
    public String toEditPage(@PathVariable("id") Integer id, Model model) {
        Employee employee = employeeDao.get(id);
        model.addAttribute("employee", employee);
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments", departments);
        return "emp/edit";
    }
} 

修改界面edit.html, SpringMVC中配置 HiddenHttpMethodFilter通过_method属性将post请求转换为put请求。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>员工修改界面</title>
</head>
<body>
<h1>员工修改</h1>
<form action="#" method="post" th:action="@{/emp}">
    <!-- 发送put请求修改员工数据
     1、SpringMVC中配置 HiddenHttpMethodFilter (SpringBoot自动配置好了)
     2、页面创建一个post表单
     3、创建一个input项,name="_method" ;值就是我们指定的请求方式
     -->
    <input type="hidden" name="_method" value="put" th:if="${employee!=null}"/>
    <input type="hidden" name="id" th:value="${employee.id}" th:if="${employee!=null}"/>
    <table border="1">
        <tr>
            <td>lastName:</td>
            <td><input type="text" name="lastName" th:value="${employee.lastName}"/></td>
        </tr>
        <tr>
            <td>email:</td>
            <td><input type="text" name="email" th:value="${employee.email}"/></td>
        </tr>
        <tr>
            <td>gender:</td>
            <td><input type="radio" name="gender" value="1" th:checked="${employee.gender==1}"/><input type="radio" name="gender" value="0" th:checked="${employee.gender==0}"/>
            </td>
        </tr>
        <tr>
            <td>department:</td>
            <td>
                <select name="department.id">
                    <option th:each="department:${departments}"
                            th:text="${department.id} +' '+ ${department.departmentName}"
                            th:value="${department.id}"
                            th:selected="${department.id==employee.department.id}"/>
                </select>
            </td>
        </tr>
        <tr>
            <td>birthday:</td>
            <td><input type="text" name="birthday"
                       th:value="${#dates.format(employee.birthday,'yyyy-MM-dd')}"/></td>
        </tr>
        <tfoot>
        <tr>
            <td colspan="2" align="center"><input type="submit" value="修改"/></td>
        </tr>
        </tfoot>
    </table>
</form>
<div th:replace="~{dashboard::#tolist}"></div>
</body>
</html>

修改员工信息后,提到put请求到/emp进行修改操作。
在这里插入图片描述

@Controller
public class EmployeeController {
    @Autowired
    EmployeeDao employeeDao;
    @Autowired
    DepartmentDao departmentDao;
    
    // ...
    // 修改员工信息
    @PutMapping("/emp")
    public String edit(Employee employee, @RequestParam("_method") String method) {
        employeeDao.save(employee);
        return "redirect:/emps";
    }
} 

修改员工信息后, 重定向到员工列表界面,验证刚刚修改的记录。

8.2.5 删除员工

员工列表页面新增“删除”按钮。

删除提交的是delete请求,form表单支持的是get或post请求,需要使用SpringMVC的过滤器HiddenHttpMethodFilter配置转换成delete请求。

<td>
	<button><a th:href="@{/emp/}+${emp.id}">修改</a></button>
	<form th:action="@{/emp/}+${emp.id}" method="post">
		<input type="hidden" name="_method" value="delete"/>
		<button type="submit" style="color: blueviolet">删除</button>
	</form>
</td>

点击“删除”按钮,提交delete请求到/emp/{id}

@Controller
public class EmployeeController {
    @Autowired
    EmployeeDao employeeDao;
    @Autowired
    DepartmentDao departmentDao;
    
    // ...
    // 删除员工
    @DeleteMapping("/emp/{id}")
    public String delete(@PathVariable("id") Integer id) {
        employeeDao.delete(id);
        return "redirect:/emps";
    }
} 

上面删除按钮的提交实现性能不好,列表的每一行员工信息的删除操作都会产生一个form表单。可以使用js事件优化。给“删除”按钮添加类选择器 class=“deleteBtn”,并使用thymeleaf设置自定义属性(提交的uri).

  • 使用jquery的api,先引入webjars的jquery包。

  • 创建删除按钮提交的form表单,form表单创建在列表外面且只有此一个。

  • 给“删除”按钮绑定点击事件。

<td>
	<button><a th:href="@{/emp/}+${emp.id}">修改</a></button>
	<!--<form th:action="@{/emp/}+${emp.id}" method="post">
		<input type="hidden" name="_method" value="delete"/>
		<button type="submit" style="color: blueviolet">删除</button>
	</form>-->
	<!--上面删除的方式,每行都会产生一个表单,我们用js事件改进, 自定义属性del_uri-->
	<button style="color: blueviolet" class="deleteBtn" th:attr="del_uri=@{/emp/}+${emp.id}">删除</button>
</td>

<!--引入jQuery包-->
<script type="text/javascript" th:src="@{/webjars/jquery/3.3.1/jquery.js}"></script>
<!--创建删除按钮提交的form表单-->
<form id="deleteEmpForm" method="post">
    <input type="hidden" name="_method" value="delete"/>
</form>
<!--给“删除”按钮绑定点击事件-->
<script>
    $(".deleteBtn").click(function () {
        // 删除当前员工
        $("#deleteEmpForm").attr("action", $(this).attr("del_uri")).submit();
        return false;
    });
</script>
8.2.6 HiddenHttpMethodFilter

上面的修改和删除中,使用HiddenHttpMethodFilter读取_method参数都将form表单的post请求方式成功转换到put和delete请求方式。源码分析如下:

public class HiddenHttpMethodFilter extends OncePerRequestFilter {

	/** Default method parameter: {@code _method} */
	public static final String DEFAULT_METHOD_PARAM = "_method";

	private String methodParam = DEFAULT_METHOD_PARAM;


	/**
	 * Set the parameter name to look for HTTP methods.
	 * @see #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);
	}


	/**
	 * Simple {@link HttpServletRequest} wrapper that returns the supplied method for
	 * {@link HttpServletRequest#getMethod()}.
	 */
	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();
    }
    /**
     * The default behavior of this method is to return getMethod() on the
     * wrapped request object.
     */
    @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,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7

其他客户端效果(postman等), 默认响应一个json数据.

{
  "timestamp": 1682344395720,
  "status": 500,
  "error": "Internal Server Error",
  "exception": "org.thymeleaf.exceptions.TemplateInputException",
  "message": "An error happened during template parsing (template: \"class path resource [templates/emp/edit.html]\")",
  "path": "/boot1/emp/1"
}

9.2 原理

可以参考ErrorMvcAutoConfiguration,错误处理的自动配置类。它给容器中添加了以下组件.
ErrorPageCustomizer, BasicErrorController ,DefaultErrorViewResolver , DefaultErrorAttributes

9.2.1 异常解析过程

一旦系统出现4xx或5xx的错误码,ErrorPageCustomizer就会生效, 使用ErrorPageRegistryErrorPage注册到容器中, 映射到/error请求;/error请求会被BasicErrorController处理,其提供了htmljson响应类型; 如果响应类型是响应页面,去到哪个页面是由DefaultErrorViewResolver解析得到的。

ErrorMvcAutoConfiguration源码, 查看注册的异常处理核心组件.

@Configuration
@ConditionalOnWebApplication // web应用才会生效,也就是要读到mvc环境
// Servlet和前端控制器存在, 该自动配置类才会生效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) 
// Load before the main WebMvcAutoConfiguration so that the error View is available
// 在WebMvcAutoConfiguration加载之前加载
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
// 绑定配置属性类ResourceProperties 
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {

	private final ServerProperties serverProperties;

	private final List<ErrorViewResolver> errorViewResolvers;

	public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
			ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
		this.serverProperties = serverProperties;
		this.errorViewResolvers = errorViewResolversProvider.getIfAvailable();
	}
    
    // DefaultErrorAttributes
    @Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes();
	}

    // BasicErrorController
	@Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
				this.errorViewResolvers);
	}

    // ErrorPageCustomizer
	@Bean
	public ErrorPageCustomizer errorPageCustomizer() {
		return new ErrorPageCustomizer(this.serverProperties);
	}
    
    @Configuration
	static class DefaultErrorViewResolverConfiguration {

		private final ApplicationContext applicationContext;

		private final ResourceProperties resourceProperties;

		DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
				ResourceProperties resourceProperties) {
			this.applicationContext = applicationContext;
			this.resourceProperties = resourceProperties;
		}

        // DefaultErrorViewResolver
		@Bean
		@ConditionalOnBean(DispatcherServlet.class)
		@ConditionalOnMissingBean
		public DefaultErrorViewResolver conventionErrorViewResolver() {
			return new DefaultErrorViewResolver(this.applicationContext,
					this.resourceProperties);
		}

	}
}	
9.2.2 ErrorPageCustomizer

系统出现错误以后来到error请求进行处理, 类似web.xml注册的错误页面规则, 将配置的error/xxx.html异常处理界面注册到容器中.

private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
	
	private final ServerProperties properties;

	protected ErrorPageCustomizer(ServerProperties properties) {
		this.properties = properties;
	}

	@Override
	public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
        // getServletPrefix 获取servlet匹配的前缀规则
		ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
				+ this.properties.getError().getPath());
		errorPageRegistry.addErrorPages(errorPage);
	}

	@Override
	public int getOrder() {
		return 0;
	}
}

// ServerProperties#getServletPrefix 获取servlet匹配的前缀规则  /*/error
public 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: /error
public class ErrorProperties {
	/**
	 * Path of the error controller.
	 */
	@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, List<ErrorViewResolver> errorViewResolvers) {
		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<String, Object> 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<Map<String, Object>> error(HttpServletRequest request) 
        // 获取异常信息
		Map<String, Object> body = getErrorAttributes(request,
				isIncludeStackTrace(request, MediaType.ALL));
		HttpStatus status = getStatus(request);
		return new ResponseEntity<Map<String, Object>>(body, status);
	}

	// Provide access to the error properties.
	protected ErrorProperties getErrorProperties() {
		return this.errorProperties;
	}
    
    // 从请求参数中获取请求状态 javax.servlet.error.status_code
    protected HttpStatus getStatus(HttpServletRequest request) {
		Integer statusCode = (Integer) request
				.getAttribute("javax.servlet.error.status_code");
		if (statusCode == null) {
            // 500
			return HttpStatus.INTERNAL_SERVER_ERROR;
		}
		try {
			return HttpStatus.valueOf(statusCode);
		}
		catch (Exception ex) {
            // 500
			return HttpStatus.INTERNAL_SERVER_ERROR;
		}
	}
    
    // 获取request中的请求参数
    protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
			boolean includeStackTrace) {
		RequestAttributes requestAttributes = new ServletRequestAttributes(request);
        // DefaultErrorAttributes#getErrorAttributes
		return this.errorAttributes.getErrorAttributes(requestAttributes,
				includeStackTrace);
	}
}
9.2.4 DefaultErrorViewResolver

BasicErrorController#errorHtml中使用到了resolveErrorView进行错误处理的视图解析.

// 继承自AbstractErrorController的处理异常的视图解析器
private final List<ErrorViewResolver> errorViewResolvers;
// BasicErrorController#resolveErrorView
protected ModelAndView resolveErrorView(HttpServletRequest request,
		HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
	for (ErrorViewResolver resolver : this.errorViewResolvers) {
        // ErrorViewResolver#resolveErrorView 视图解析
		ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
		if (modelAndView != null) {
			return modelAndView;
		}
	}
	return null;
}

DefaultErrorViewResolver#resolveErrorView

/**
 * Default {@link ErrorViewResolver} implementation that attempts to resolve error views
 * using well known conventions. Will search for templates and static assets under
 * {@code '/error'} using the {@link HttpStatus status code} and the
 * {@link HttpStatus#series() status series}.
 * <p>
 * For example, an {@code HTTP 404} will search (in the specific order):
 * <ul>
 * <li>{@code '/<templates>/error/404.<ext>'}</li>
 * <li>{@code '/<static>/error/404.html'}</li>
 * <li>{@code '/<templates>/error/4xx.<ext>'}</li>
 * <li>{@code '/<static>/error/4xx.html'}</li>
 * </ul>
 */
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	private static final Map<Series, String> SERIES_VIEWS;

	static {
		Map<Series, String> views = new HashMap<Series, String>();
        // 按照请求状态码进行匹配 /error/4xx.html, /error/5xx.html
		views.put(Series.CLIENT_ERROR, "4xx");
		views.put(Series.SERVER_ERROR, "5xx");
		SERIES_VIEWS = Collections.unmodifiableMap(views);
	}

	// ...

	/**
	 * Create a new {@link DefaultErrorViewResolver} instance.
	 * @param applicationContext the source application context
	 * @param resourceProperties resource properties
	 */

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

	private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //  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<String, Object> model) {
        /**
        * /templates/error/404.html,4xx.html
		* /static/error/404.html,4xx.html
		* /public/error/404.html,4xx.html
		* /resources/error/404.html,4xx.html
        */
		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;
	}

	/**
	 * {@link View} backed by an HTML resource.
	 */
	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<String, ?> model, HttpServletRequest request,
				HttpServletResponse response) throws Exception {
			response.setContentType(getContentType());
			FileCopyUtils.copy(this.resource.getInputStream(),
					response.getOutputStream());
		}
	}
}

TemplateAvailabilityProviders#getProvider

public class TemplateAvailabilityProviders {
	// 配置的可用模板
	private final List<TemplateAvailabilityProvider> providers;

	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解析浏览器端的请求或其他客户端请求的errorHtmlerror的处理中都有获取异常相关信息的操作, 是从DefaultErrorAttributes中获取.

@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes
		implements ErrorAttributes, HandlerExceptionResolver, Ordered {
	// ....

    // 获取异常信息
	@Override
	public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
			boolean includeStackTrace) {
		Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
        // 存入时间戳
		errorAttributes.put("timestamp", new Date());
        // 存入响应状态码
		addStatus(errorAttributes, requestAttributes);
        // 存入异常信息
		addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
        // 存入请求uri
		addPath(errorAttributes, requestAttributes);
		return errorAttributes;
	}

	private void addStatus(Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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> 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 5xx
private static final Map<Series, String> SERIES_VIEWS;

static {
    Map<Series, String> views = new HashMap<Series, String>();
    views.put(Series.CLIENT_ERROR, "4xx");
    views.put(Series.SERVER_ERROR, "5xx");
    SERIES_VIEWS = Collections.unmodifiableMap(views);
}
// DefaultErrorViewResolver#resolveErrorView
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
		Map<String, Object> 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<String, Object> 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页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>4xx异常</title>
    </head>
    <body>
        <h5 th:text="4xx异常">系统异常</h5>
        <h5>time:[[(${#dates.format(timestamp,'yyyy-MM-dd HH:mm:ss')})]]</h5>
        <h5>status:[[(${status})]]</h5>
        <h5>error:[[(${error})]]</h5>
        <h5>exception:[[(${exception})]]</h5>
        <h5>message:[[(${message})]]</h5>
        <h5>errors:[[(${errors})]]</h5>
        <h5>trace:[[(${trace})]]</h5>
        <h5>path:[[(${path})]]</h5>
    </body>
</html>

启动应用后, 访问一个不存在的请求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(
			"<html><body><h1>Whitelabel Error Page</h1>"
					+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
					+ "<div id='created'>${timestamp}</div>"
					+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
					+ "<div>${message}</div></body></html>");

    // 异常渲染的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 {

    /**
     * http://localhost:8082/boot1/exception/username?username=aaa
     */
    @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会扫描当前类所在包的所有处理器handler方法,执行通知增强的逻辑
 * 属性 basePackages 可以指定扫描范围
 */
@ControllerAdvice(basePackages = {"com.aiguigu.springboot02config.controller"})
public class MyExceptionHandler {

    // 自定义异常处理
    // 浏览器客户端都返回的json数据,无法实现自适应效果(浏览器返回页面,其他客户端返回json数据)
    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String, Object> handleException(Exception e) {
        Map<String, Object> 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) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", "user.not_exist");
        map.put("message", "用户出错了. " + e.getMessage());

        /**
         * DefaultErrorAttributes#addStatus
         * Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code");
         */
        // 传入我们自己的错误状态码, 进入我们指定的错误页面
        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) {
	Map<String, Object> map = new HashMap<>();
	map.put("code", "user.not_exist");
	map.put("message", "用户出错了. " + e.getMessage());

	/**
	 * DefaultErrorAttributes
	 * Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code");
	 */
	// 传入我们自己的错误状态码, 进入我们指定的错误页面
	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
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    // 返回值map就是页面和json能获取的所有字段
    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
        map.put("company", "atguigu");
        // 我们的异常处理器携带的数据
        Map<String, Object> ext = (Map<String, Object>) 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": "用户出错了. 用户不存在!"
  }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值