第三章 SpringBoot的Web开发
一 解决静态资源
在SpringBoot的项目结构中没有webapp文件夹,由此静态资源放在哪个文件夹就是个问题,只能从源码分析。
搜索WebMvcAutoConfiguration
类,找到方法addResourceHandlers()
方法,然后分代码块分析:
public class WebMvcAutoConfiguration{
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
}
1 第一个代码块:
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
该if判断是指:关于静态资源的配置已经自定义了,就读取自定义配置文件的内容并结束该方法。何为自定义配置文件?我们通过点击WebMvcProperties.class
发现:
public class WebMvcProperties{
//省略代码
private String staticPathPattern = "/**";
//省略代码
}
由此我们看出,可以在yaml文件中配置staticPathPattern
属性来指定静态资源的存放位置。但是只要自定义了静态资源的存放位置,SpringBoot的默认存放位置就失效了(因为第一个代码块中有一个return)。
2 第二个代码块:
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
在一个叫webjars的文件夹下找静态资源,webjars的位置位于**/META-INF/resources/
下。何为webjars?百度搜索webjars,进入官网;
在项目中引入maven依赖,然后我们可以在外部库中看见:
刚好这个目录结构完全符合源码中的目录结构,要使用的话指需要在运行程序后,输入相应的URL就可以访问该webjars文件夹下的资源!在浏览器中输入:http://localhost:8080/webjars/jquery/3.5.1/
:
3 第三个代码块:
其中getStaticPathPattern()
是获得staticPathPattern
,其中staticPathPattern = "/**"
,该变量表示请求URL是:http://localhost:8080/**
的时候,会从ResourceProperties
类找到的目录中找资源!
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
getResourceLocations()方法会返回一个“/”的路径,然后getStaticLocations()方法是属于ResourceProperties类的,点击这个方法进去会发现:
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
//省略代码!
public String[] getStaticLocations() {
return this.staticLocations;
}
}
所以一共有四个路径:
classpath:/META-INF/resources/
。这个就是指webjars的那个目录!classpath:/resources/**
classpath:/static/**
classpath:/public/**
/
优先级是resources>static>public!
二 首页的定制
通过快捷键alt+7来查看当前类的结构,从而寻找关于首页图标定制的源码。
找到了如下代码:
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
private Optional<Resource> getWelcomePage() {
String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}
private Resource getIndexHtml(String location) {
return this.resourceLoader.getResource(location + "index.html");
}
getWelcomePage()
方法中调用getResourceLocations()
方法,其中getStaticLocations()
是获得staticLocations
的值,而它的值为:
staticLocations={ "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/" }
然后再调用getIndexHtml()方法。这几段代码是表示:程序会从/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
这几个目录下寻找index.html作为主页。
三 Thymeleaf
Thymeleaf其实就是一个模板引擎,在原来写SSM所使用的JSP也就是一个模板引擎。模板引擎的意义在于:通过解析我们写的表达式将我们在后台封装的数据展现到页面的指定位置。
1. 环境准备
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2. 源码分析
从自动配置源码我们可以看出,每个Starter都有一个自己的xxxProperties类,搜索ThymeleafProperties进入查看:
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
//相当于前缀
public static final String DEFAULT_PREFIX = "classpath:/templates/";
//相当于后缀
public static final String DEFAULT_SUFFIX = ".html";
}
通过这几个属性可知,springBoot会去类路径下的templates文件夹中寻找html文件:
2.1 验证:
编写Controller类
@Controller
public class ThemController {
@RequestMapping("/testThymeleaf")
public String ThymeleafHello(){
return "Thymeleaf";
}
}
在templates文件夹下创建html
<head>
<meta charset="UTF-8">
<title>测试Thymeleaf</title>
</head>
<body>
你好!Thymeleaf!
</body>
验证结果:
3. Thymeleaf的语法
具体见官网:https://www.thymeleaf.org/
四 装配SpringMVC
引用SpringBoot文档的介绍,自动配置在Spring的默认值之上添加了以下功能:
1.包含ContentNegotiatingViewResolver和BeanNameViewResolver;
2.支持提供静态资源,包括对WebJars的支持;自动注册Converter,GenericConverter和Formatter类;
3.支持HttpMessageConverters; 自动注册MessageCodesResolve,静态index.html支持;
4.定制Favicon支持;
5.自动使用ConfigurableWebBindingInitializerbean。
如果要保留这些SpringBoot对于MVC配置并进行更多的MVC配置(拦截器,格式化程序,视图控制器和其他功能),可以自己创建配置类并且实现WebMvcConfigurer接口,在该类上加上@Configuration注解,但不能添加@EnableWebMvc注解。
如果你想自己自定义
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
或者ExceptionHandlerExceptionResolver
,可以自己创建类并且实现WebMvcRegistrations
接口。
1. 举例说明如何扩展SpringMVC配置
比如SpringBoot帮我们自动装配的视图解析器无法满足需求,此时我们需要扩展它的功能或者是完全重写它的功能。笔者在进行测试的时候进行了三种测试。首先创建了一个配置类MvcConfigurer
实现了WebMvcConfigurer
接口。
1.1 测试一:
创建MyViewResolver类,在pojo包下,无MvcConfigurer类。
public class MyViewResolver implements ViewResolver {
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
return null;
}
@Bean
public ViewResolver myViewResolver(){
return new MyViewResolver();
}
}
类结构如下图所示:
在DispatcherServlet
类的doDispatch()
方法打断点测试:
运行主程序后,输入http://localhost:8080/后,进入断点测试发现:
1.2 测试二:
创建MyViewResolver类,在pojo包下,有MvcConfigurer类但是无内容。
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {
}
依旧在相同的地方打断点,输入http://localhost:8080/
后,进入断点测试发现:
1.3测试三:
创建MyViewResolver类,在pojo包下,有MvcConfigurer类并且myViewResolver()方法(标有@Bean注解的方法)
public class MyViewResolver implements ViewResolver {
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
return null;
}
}
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {
@Bean
public ViewResolver myViewResolver(){
return new MyViewResolver();
}
}
依旧在相同的地方打断点,运行主程序后,输入http://localhost:8080/后,进入断点测试发现:
得出结论:
- 如果我们要扩展SpringMVC,必须需要一个配置类并且它实现了WebMvcConfigurer接口并且有@Configuration注解;
- 我们扩展SpringMVC某些功能的时候如自定义视图,自定义视图解析器,自定义类型转换器等,加入IOC容器的方法(就是标注了@Bean的方法)必须在配置类中!
2. WebMvcConfigurer接口的方法介绍(重点)
public interface WebMvcConfigurer {
//该方法是用于定制URL匹配规则的
//构建web应用程序时,并不是所有的URL请求都遵循默认的规则。有时,我们希望RESTful URL匹配的时候含定界符“.”;
//这种情况在Spring中可以称之为“定界符定义的格式”;有时,我们希望识别斜杠的存在。
default void configurePathMatch(PathMatchConfigurer configurer) {
}
//用于内容协商机制。根据对方是浏览器还是客户端,服务端返回不同数据格式
void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
}
//处理异步请求的。只能设置两个值,一个超时时间(毫秒,Tomcat下默认是10000毫秒,即10秒);
//还有一个是AsyncTaskExecutor,异步任务执行器。
default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
}
//*默认静态资源处理器
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
//*增加转化器或者格式化器
default void addFormatters(FormatterRegistry registry) {
}
//*自定义拦截器
default void addInterceptors(InterceptorRegistry registry) {
}
//*自定义资源映射
default void addResourceHandlers(ResourceHandlerRegistry registry) {
}
//*处理跨域
default void addCorsMappings(CorsRegistry registry) {
}
//*处理页面跳转
default void addViewControllers(ViewControllerRegistry registry) {
}
//*配置视图解析器
default void configureViewResolvers(ViewResolverRegistry registry) {
}
//注册自定义控制器(controller)方法参数类型
default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
}
//注册自定义控制器(controller)方法返回类型
default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
}
//信息转换器
default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
//仅添加一个自定义的HttpMessageConverter,不覆盖默认注册的HttpMessageConverter
default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
//注册异常处理
default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
//多个异常处理,可以重写次方法指定处理顺序等
default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
}
default Validator getValidator() {
return null;
}
default MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
2.1 addFormatters
主要作用是用于增加转化器或者格式化器,其实对于数据转换和格式化我们一般也就是用日期转字符串,字符串转日期功能,在SpringBoot中已经配置好了一个内部类Format(在WebMvcProperties类中)。在和SpringMVC时候一样依旧需要一个实现了 Converter接口的类。通过addFormatters方法我们可以省略在SpringMVC中类似于以下的配置:
<bean id="converterService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<!-- 给工厂注入一个新的类型转换器 -->
<property name="converters">
<array>
<!-- 配置自定义类型转换器 -->
<bean class="com.itachi.util.StringAndDateConverter"></bean>
</array>
</property>
</bean>
具体案例:
2.1.1 HTML文件中
<a href="/web/deleteAccount?date=2018/01/01">根据日期删除账户,测试自定义转换器</a>
2.1.2 Controller方法中:
@RequestMapping("/deleteAccount")
public String deleteAccount(Date date) {
System.out.println("删除了账户。。。。"+date);
return "success";
}
2.1.3 自定义类型转换器
public class StringAndDateConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
DateFormat format = null;
try {
if(StringUtils.isEmpty(source)) {
throw new NullPointerException("请输入要转换的日期");
}
format = new SimpleDateFormat("yyyy-MM-dd");
Date date = format.parse(source);
return date;
}catch (Exception e) {
throw new RuntimeException("输入日期有误");
}
}
}
2.1.4 MvcConfigurer类中
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringAndDateConverter());
}
}
//我们通过第1点的实例也可以这样写代码:
// @Bean
// public Converter<String, Date> myFormatter(){
// return new StringAndDateConverter();
// }
点击主程序,输入http://localhost:8080/test.html进行测试:
2.2 addInterceptors
主要作用是用于配置拦截器。在和SpringMVC时候一样依旧需要一个实现了HandlerInterceptor接口的类。通过addInterceptors()方法我们可以省略原来在SpringMVC中类似于以下的配置:
<mvc:interceptors>
<mvc:interceptor>
<!-- 配置拦截器作用的路径,/**表示拦截所有路径 -->
<mvc:mapping path="/**"/>
<!-- 配置不需要拦截器作用的路径 /admin表示放行所有以/admin结尾的请求路径 -->
<mvc:exclude-mapping path="/admin"/>
<!-- 定义在<mvc:interceptor>下面的Interceptor,表示对匹配路径的请求才进行拦截 -->
<bean class="com.itachi.pojo.MyInterceptorOne"/>
</mvc:interceptor>
</mvc:interceptors>
具体示例:
2.2.1 拦截器类
public class MyInterceptorOne implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("拦截器中的preHandle方法执行了");
return true;
}
}
2.2.2 Controller类
@RestController
@RequestMapping("/web")
public class webController {
@RequestMapping("/hello")
public String sayHello(){
System.out.println("Controller类中Hello!");
return "sayHello运行成功!";
}
@RequestMapping("/nohello")
public String noSayHello(){
System.out.println("Controller类中的NoHello!");
return "noSayHello运行成功!";
}
}
2.2.3 MvcConfigurer类中
MvcConfigurer类,也就是那个实现了WebMvcConfigurer接口并且标注了@Configuration的类:
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptorOne())
.addPathPatterns("/**")
.excludePathPatterns("/web/nohello");
/*
假如有多个拦截器,就可以像这样配置
registry.addInterceptor(new MyInterceptorTwo())
.addPathPatterns("/**")
.excludePathPatterns("/web/hello");
*/
}
}
- addInterceptor():表示添加一个拦截器类,该类必须要实现HandlerInterceptor接口。
- addPathPatterns():用于设置拦截器的过滤路径规则。addPathPatterns("/**")对所有请求都拦截。
- excludePathPatterns():用于设置不需要拦截的过滤规则。
点击主程序运行,输入http://localhost:8080/web/nohello。测试结果:
输入http://localhost:8080/web/hello。测试结果:
2.3 addResourceHandlers
该方法是用于自定义静态资源的映射。其实这个方法我们不是第一次见到,在分析Thymeleaf源码的时候我们就已经见到了该方法:
现在我们来说明它如何使用,首先还是定义一个WebConfig类实现WebMvcConfigurer接口,重写addResourceHandlers方法即可:
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/picture/**").addResourceLocations("file:D:/picture/");
}
- addResourceHandler:指的是对外暴露的访问路径也就是前端要访问的资源路径。
- addResourceLocations:指的是内部文件放置的目录。
- 该段代码就表示:用户要访问类路径picture文件夹下的资源的时候,都会从本地D盘下的/picture文件夹找相应的资源!
2.4 addCorsMappings
解决跨域问题。
2.4.1 何为跨域?
Url的一般格式:协议 + 域名(子域名 + 主域名) + 端口号 + 资源地址。当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。对于前后端分离的项目来说,如果前端项目与后端项目部署在两个不同的域下,那么势必会引起跨域问题的出现。
一般来说:当前页面是无法处理跨域页面的请求的,这是由于浏览器的同源策略限制。解决跨域问题就需要通过以下三种方式处理:
2.4.2 方式一:@CrossOrigin注解
直接在Controller的类上加上@CrossOrigin注解,注解的值就是能处理的跨域页面请求,默认就是可以处理所有跨域页面请求。
@Controller
@CrossOrigin("https://www.baidu.com/")
@RequestMapping("/web")
public class webController {
@RequestMapping("/hello")
public String sayHello(){
System.out.println("Controller类中Hello!");
return "sayHello运行成功!";
}
}
@CrossOrigin(“https://www.baidu.com/”):表示webController类下的所有方法都可以处理百度所发来的请求。其实他还有很多属性,点击该注解进入源码:
public @interface CrossOrigin {
/** @deprecated */
@Deprecated
String[] DEFAULT_ORIGINS = new String[]{"*"};
/** @deprecated */
@Deprecated
String[] DEFAULT_ALLOWED_HEADERS = new String[]{"*"};
/** @deprecated */
@Deprecated
boolean DEFAULT_ALLOW_CREDENTIALS = false;
/** @deprecated */
@Deprecated
long DEFAULT_MAX_AGE = 1800L;
//同origins属性一样
@AliasFor("origins")
String[] value() default {};
//所有可以处理的页面的集合,例如"https://www.baidu.com/"。如果没有定义,所有页面都支持
@AliasFor("value")
String[] origins() default {};
//允许请求头重的header,默认都支持
String[] allowedHeaders() default {};
//响应头中允许访问的header,默认为空
String[] exposedHeaders() default {};
//请求支持的方法,例如"{RequestMethod.GET, RequestMethod.POST}"}。默认支持RequestMapping中设置的方法
RequestMethod[] methods() default {};
//是否允许cookie随请求发送,使用时必须指定具体的域
String allowCredentials() default "";
//预请求的结果的有效期,默认30分钟
long maxAge() default -1L;
}
以上注解均来自:SpringBoot解决CORS跨域(@CrossOrigin)
2.4.2 方式二:addCorsMappings(不推荐)
重写WebMvcConfigurer接口中的addCorsMappings()方法属于全局配置。
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
- addMapping():设置为/**就表示所有URL路径都可以被跨域。可以任意配置,可以具体到请求路径;
- allowedOrigins():设置为*就表示所有外部页面的请求都可以处理,假如是这样配置allowedOrigins(“https://www.baidu.com/”)就表示只处理百度页面发送的请求!
- allowCredentials():true值就代表带上cookie信息。
- allowedMethods():允许请求的方式,如GET", “HEAD”, “POST”, “PUT”, “DELETE”, "OPTIONS"等
- maxAge():设置跨域允许时间
不推荐的原因在于:使用该中配置后,若又自定义了拦截器就会导致跨域配置失效。原因是因为请求会先进入拦截器而不是Mapping映射中。这样导致返回的head信息中没有信息,与我们配置的跨域信息不符合,就会失效。解决办法为第三种方式。
2.4.3 方式三:拦截器实现
这种方式不需要webConfig配置类。直接定义一个CorsFilter实现Filter接口,并标上@Component注解即可。Filter接口是servlet包下的
@Component
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) servletResponse;
res.addHeader("Access-Control-Allow-Credentials", "true");
res.addHeader("Access-Control-Allow-Origin", "*");
res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
res.addHeader("Access-Control-Allow-Headers", "Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN");
if (((HttpServletRequest) servletRequest).getMethod().equals("OPTIONS")) {
servletResponse.getWriter().println("ok");
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
方法res.addHeader()添加的内容含义与方式二是相同的。
随便在某个页面(最好是空白页面,不要百度什么的,似乎有什么https和http互相访问的限制),按F12,打开【开发者工具】,在里面的【Console】可以直接输入以下js代码按下回车即可。
var token= "LtSFVqKxvpS1nPARxS2lpUs2Q2IpGstidMrS8zMhNV3rT7RKnhLN6d2FFirkVEzVIeexgEHgI/PtnynGqjZlyGkJa4+zYIXxtDMoK/N+AB6wtsskYXereH3AR8kWErwIRvx+UOFveH3dgmdw1347SYjbL/ilGKX5xkoZCbfb1f0=,LZkg22zbNsUoHAgAUapeBn541X5OHUK7rLVNHsHWDM/BA4DCIP1f/3Bnu4GAElQU6cds/0fg9Li5cSPHe8pyhr1Ii/TNcUYxqHMf9bHyD6ugwOFTfvlmtp6RDopVrpG24RSjJbWy2kUOOjjk5uv6FUTmbrSTVoBEzAXYKZMM2m4=,R4QeD2psvrTr8tkBTjnnfUBw+YR4di+GToGjWYeR7qZk9hldUVLlZUsEEPWjtBpz+UURVmplIn5WM9Ge29ft5aS4oKDdPlIH8kWNIs9Y3r9TgH3MnSUTGrgayaNniY9Ji5wNZiZ9cE2CFzlxoyuZxOcSVfOxUw70ty0ukLVM/78=";
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://192.168.2.156:8080/web/hello');
xhr.setRequestHeader("x-access-token",token);
xhr.send(null);
xhr.onload = function(e) {
var xhr = e.target;
console.log(xhr.responseText);
}
测试结果:
2.5 addViewControllers
以前写SpringMVC的时候,如果需要访问一个页面,必须要写Controller类,然后再写一个方法跳转到页面。现在只需要重写 addViewControllers方法即可。
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/test").setViewName("account");
registry.addViewController("/").setViewName("test");
}
}
第一行代码表示:用户输入http://localhost:8080/test的时候会自动跳转到account.html页面;
第二行代码表示:用户输入http://localhost:8080的时候会自动跳转到test.html页面。
注意:
- 不会覆盖WebMvcAutoConfiguration(Springboot自动配置)中的addViewControllers(在此方法中,SpringBoot将“/”映射至index.html),这也就意味着自己的配置和Spring Boot的自动配置同时有效。
- 注意的是该操作是转发,地址栏是不会变化的!
2.6 configureViewResolvers
这个方法是用来配置视图解析器的,该方法的参数ViewResolverRegistry 是一个注册器,用来注册你想自定义的视图解析器等.。重写该方法的目的就是用来代替SpringMVC中的以下配置:
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!—3.3.1前缀(目录) -->
<property name="prefix" value="/WEB-INF/pages/"/>
<!—3.3.2后缀 -->
<property name="suffix" value=".jsp"/>
</bean>
具体代码为:
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {
@Bean
public InternalResourceViewResolver resourceViewResolver()
{
InternalResourceViewResolver internalResourceViewResolver = new InternalResourceViewResolver();
//请求视图文件的前缀地址
internalResourceViewResolver.setPrefix("/WEB-INF/pages/");
//请求视图文件的后缀
internalResourceViewResolver.setSuffix(".html");
return internalResourceViewResolver;
}
/**
* 视图配置
* @param registry
*/
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.viewResolver(resourceViewResolver());
/*registry.jsp("/WEB-INF/jsp/",".jsp");*/
}
}
3. 配置SpringMVC的原理
3.1 探究Formatter数据格式化
继续探究SpringBoot对于SpringMVC的扩展,官方文档中还有这句话:自动注册Converter,GenericConverter和Formatter类;
我们是试着探究是如何自动注册的,并且是否能够进行扩展按照我们的意愿进行修改。点开WebMvcAutoConfiguration类找到mvcConversionService()方法。
public FormattingConversionService mvcConversionService() {
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()
.dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
addFormatters(conversionService);
return conversionService;
}
Format format = this.mvcProperties.getFormat();这行代码就表示SpringBoot会优先读取我们的配置文件来寻找我们是否对数据格式化器进行配置!
点击mvcProperties会发现它是WebMvcProperties类型变量,点击WebMvcProperties会发现:
public class WebMvcProperties {
//省略代码
private final Format format = new Format();
//省略代码
public static class Format {
/**
* Date format to use, for example `dd/MM/yyyy`.
* 该注解是举例:说我们可以定义日期格式为2020/09/08这种格式!
*/
private String date;
/**
* Time format to use, for example `HH:mm:ss`.
*/
private String time;
/**
* Date-time format to use, for example `yyyy-MM-dd HH:mm:ss`.
*/
private String dateTime;
public String getDate() {
return this.date;
}
public void setDate(String date) {
this.date = date;
}
public String getTime() {
return this.time;
}
public void setTime(String time) {
this.time = time;
}
public String getDateTime() {
return this.dateTime;
}
public void setDateTime(String dateTime) {
this.dateTime = dateTime;
}
}
//省略代码
}
前文我们说过xxxProperties类都可以在yaml文件中进行自定义配置来覆盖SpringBoot的默认配置或者与SpringBoot的默认配置共存!我们尝试在Yaml文件中进行配置,会发现:
依旧能根据我们的配置文件来修改默认配置,达到扩展SpringMVC功能的需求!
3.2 举例说明Formatter数据格式化。
首先先创建实体类,带有Date类型的属性。
public class Student {
private String name;
private int age;
private Date birth;
//省略get、set、toString()方法
}
然后创建Thymeleaf.html文件和success.html文件放在templates目录下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试Thymeleaf</title>
</head>
<body>
你好!Thymeleaf!
<form action="/save" method="post">
学生名称:<input type="text" name="name" ><br/>
学生年龄:<input type="text" name="age" ><br/>
学生生日:<input type="text" name="birth" ><br/>
<input type="submit" value=" 保存 ">
</form>
</body>
</html>
然后创建Controller类:
@Controller
public class ThemController {
@RequestMapping("/testThymeleaf")
public String ThymeleafHello(){
return "Thymeleaf";
}
@RequestMapping("/save")
public String saveStudent(Student student){
System.out.println(student);
return "success";
}
}
最后配置yaml文件:
spring:
mvc:
format:
date: yyyy.MM.dd
点击主程序运行,输入http://localhost:8080/testThymeleaf。
尝试输入错误的时间格式:
3.3 探究@EnableWebMvc注解
官方文档:如果要保留这些SpringBoot对于MVC配置并进行更多的MVC配置(拦截器,格式化程序,视图控制器和其他功能),可以自己创建配置类并且实现WebMvcConfigurer接口,在该类上加上@Configuration注解,但不能添加@EnableWebMvc注解。
为什么不能加@EnableWebMvc注解?官方还说如果要全面接管SpringMVC就可以加上@EnableWebMvc注解。我们在WebMvcAutoConfiguration有个注解值得注意:
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class):当容器中不存在这个类的时候自动配置才会生效!
而我们点击@EnableWebMvc会发现:
只要是使用了@EnableWebMvc注解,它就会想容器中导入一个DelegatingWebMvcConfiguration类从而导致自动配置类失效!因此不能加上@EnableWebMvc!
4. 装配SpringMVC总结
- 在SpingBoot中对于SpringMVC开发:主要关注WebMvcAutoConfiguration类,WebMvcProperties类,WebMvcConfigurer接口。
- WebMvcAutoConfiguration类和WebMvcProperties类在我们没需求的情况下已经帮我们自动装配了很多组件,直接用即可。
- 如果我们想改变SpringBoot帮我们自动装配的组件属性值,可以在WebMvcProperties类中找到我们想要改变的组件属性值,然后在Yaml文件进行配置修改即可。
- 如果自动配置的组件中没有我们想要的组件,就需要一个扩展类来实现WebMvcConfigurer接口并标记上@Configuration注解。自定义我们想要的组件过后,将加入IOC容器的方法放在扩展类中即可。
- WebMvcConfigurer接口还有很多方法,实现如跳转,拦截器等等功能达到简化代码的作用。
五 SpringBoot的错误页面和异常处理
第五大点pringBoot的错误页面和异常处理的内容均来自:
SpringBoot - 自定义错误页1;
SpringBoot - 自定义错误页2;
SpringBoot - 自定义错误页3。
在页面用户输入错误URL或者请求存在页面的时候会报错,对于浏览器而言,SpringBoot会提供一个默认的错误页面:
这样的默认错误页面对于用户体验而言是及其不友好的,SpringBoot支持我们自定义错误页面,有使用静态页面和使用模板两种方式。除此之外,我们还可以自定义错误提示消息等等。
1.SpringBoot错误页面原理
SpringBoot所有关于错误页面自动配置的类都在ErrorMvcAutoConfiguration中,而自动配置的属性类在ErrorProperties类中! 重要的一共有五个组件:DefaultErrorAttributes、BasicErrorController、DefaultErrorViewResolver、ErrorPageCustomizer
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
//ServerProperties是web容器的配置参数类
private final ServerProperties serverProperties;
//获取错误信息用
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
//默认的错误处理。
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
//当发生错误的时候,该组件根据ErrorProperties类中的path属性的值,将请求转发至path对应的URL。
//默认为/error
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
默认错误视图处理器。根据错误类型状态码等数据来决定返回的页面
@Configuration(proxyBeanMethods = false)
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
}
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
}
}
1.1 ServerProperties
Springboot配置文件中以server开头的项表示服务器的配置参数,这一点从字面意义即可直观理解,这些参数,包括端口,路径设置,SSL配置参数等等。我们可以参考类ServerProperties的定义知道能配置具哪些参数。我们可以从下图看到,SpringBoot自己集成了包括Tomcat,Jetty,Netty等服务器还有其他可配置的属性。我们举例说明:
# 项目contextPath,一般在正式发布版本中,我们不配置
server.context-path=/myspringboot
# 错误页,指定发生错误时,跳转的URL。请查看BasicErrorController源码便知
server.error.path=/error
# 服务端口
server.port=9090
# session最大超时时间(分钟),默认为30
server.session-timeout=60
# 该服务绑定IP地址,启动服务器时如本机不是该IP地址则抛出异常启动失败,只有特殊需求的情况下才配置
# server.address=192.168.16.11
Tomcat为Spring Boot的默认容器,下面是几个常用配置:
# tomcat最大线程数,默认为200
server.tomcat.max-threads=800
# tomcat的URI编码
server.tomcat.uri-encoding=UTF-8
# 存放Tomcat的日志、Dump等文件的临时文件夹,默认为系统的tmp文件夹(如:C:\Users\Shanhy\AppData\Local\Temp)
server.tomcat.basedir=H:/springboot-tomcat-tmp
# 打开Tomcat的Access日志,并可以设置日志格式的方法:
#server.tomcat.access-log-enabled=true
#server.tomcat.access-log-pattern=
# accesslog目录,默认在basedir/logs
#server.tomcat.accesslog.directory=
# 日志文件目录
logging.path=H:/springboot-tomcat-tmp
# 日志文件名称,默认为spring.log
logging.file=myapp.log
1.2 ErrorPageCustomizer
该类属于ErrorMvcAutoConfiguration类的内部类。这个类的作用是定制错误的响应规则,通过getPath方法获得ErrorProperties类的path属性(默认为/error),发生错误时会默认发送该请求(即/error请求)。我们可以通过在application.properties文件中配置该path属性:server.error.path=/error。
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
//获取配置的路径,并且errorpage注册到了servlet容器。如果没有配置就默认将请求转发至/error
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
1.3 DefaultErrorAttributes
DefaultErrorAttributes类的作用就是收集错误信息后生成Model信息,用于页面显示或者以Jason形式返回给客户端。如果开发者没有自己提供一个 ErrorAttributes 的实例的话,那么 Spring Boot 将自动提供一个ErrorAttributes 的实例,也就是 DefaultErrorAttributes。一个DefaultErrorAttributes尽量提供以下信息:
①时间戳记-提取错误的时间 ②status-状态码 ③错误-错误原因
④exception-根异常的类名(如果已配置)
⑤消息-异常消息(如果已配置)
⑥错误-异常中的任何ObjectErrors BindingResult(如果已配置)
⑦trace-异常堆栈跟踪(如果已配置)
⑧path-引发异常时的URL路径。
发生异常的时候,会调用getErrorAttributes()方法获得错误信息。
public class DefaultErrorAttributes{
//该方法有多个重载形式,针对于不同情况向errorAttributes集合中添加不同的错误信息
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
//添加错误时间
errorAttributes.put("timestamp", new Date());
//添加状态码
addStatus(errorAttributes, webRequest);
//添加错误信息
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
//添加引起错误的URL路径
addPath(errorAttributes, webRequest);
return errorAttributes;
}
}
DefaultErrorAttributes类在收集了错误信息后,就会将这些信息设为默认值封装进model中,我们当然可以通过继承DefaultErrorAttributes类来自定义错误信息!
所以我们如果要自定义错误信息的时候,
1.4 BasicErrorController
SpringBoot内置了一个BasicErrorController对异常进行统一的处理,当在页面发生异常的时候会自动把请求转发到/error(Spring Boot提供的一个默认的映射,这个转发动作是ErrorPageCustomizer来完成的)。BasicErrorController类主要起到组合的作用,将其他类解析好的错误数据、错误页面、错误URL组合在一起返回给浏览器或者其他客户端。我们可以自定义页面内容,只需在classpath路径下新建error页面即可,当然我们也可以自定义error页面的路径。
BasicErrorController提供两种返回错误:当用户是浏览器的时候,就会返回html页面;当用户采用的是其他客户端的时候(如postman测试),就会返回JSON数据。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//浏览器客户端时返回html页面。一般情况下浏览器默认发送的请求头中Accept: text/html
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
//获取状态码,判断是4xx还是5xx等错误
HttpStatus status = getStatus(request);
//getErrorAttributes获取错误信息并且封装成model数据,用于页面显示
//此处可以看成就是调用了DefaultErrorAttributes的getErrorAttributes()方法
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//根据状态码去寻找对应的html页面(该方法属于DefaultErrorViewResolver类)
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
//如果没有找到对应的 /error/xxx.html视图,就会返回一个名为error的ModelAndView
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//其他客户端时返回jason数据
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
}
有一个注解我们需要特别注意一下:
//如果在配置文件中配置了server.error.path值,则使用指定的值作为错误请求;
//如果未配置,则查看是否配置了error.path;如果还是没有,则该控制器默认处理/error请求。
@RequestMapping("${server.error.path:${error.path:/error}}")
1.5 DefaultErrorViewResolver
默认错误视图处理器,该类的作用就是:当发生错误的时候,根据错误信息来选择错误页面展示给用户。
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
//省略代码
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//先是看能不能找到以状态码为名字的错误页面
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
//如果没有,就去找以4xx,5xx为名字的错误页面
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
//省略代码
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//以错误状态码作为错误页面名
String errorViewName = "error/" + viewName;
//如果有模板引擎Templates,就以模板引擎来寻找以errorViewName为名字的错误页面
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
//如果找到了就直接返回该错误页面
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
//模版引擎不能够解析到页面的情况下,就在静态资源文件夹下查找errorViewName对应的页面
return resolveResource(errorViewName, model);
}
}
2. 默认错误页面
通过DefaultErrorViewResolver的源码(String errorViewName = “error/” + viewName)分析可知,SpringBoot会在/error文件夹下寻找以状态码或者以4xx、5xx为名字的页面作为错误展示页,找不到的时候会展现默认页面给用户。
2.1 使用静态页面
如果不需要向用户展示详细的错误信息,可以把错误信息定义成静态页面。即在 resource/static 目录下创建 error 目录,然后在 error 目录中创建错误展示页面。错误展示页面命名规则有如下两种:
- 一种是直接使用具体的响应码命名文件,比如:404.html、405.html、500.html
- 另一种可以使用 4xx.html、5xx.html 这样的命名
注意:响应码.html 的优先级高于 4xx.html、5xx.html 页面的优先级。比如:发生一个 404 错误,则优先展示 404.html 而不是 4xx.html
例子:
html文件中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
发生了4xx错误。
</body>
</html>
输入错误的URL测试:
2.2 使用动态模版templates
在 resource/templates 目录下创建 error 目录,然后在 error 目录中创建错误展示页面。但是用之前必须引入相关的templates 依赖。
404.html页面代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table border="1">
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
</table>
</body>
</html>
测试结果:
3. 自定义 Error 数据、Error 视图
3.1 简单的自定义错误数据
通过DefaultErrorAttributes源码可知,页面上默认的错误数据包括timestamp、status、error、message、path都是该类生成的,我们可以根据自己的需求对错误信息进行增加,我们只需要自定义一个类继承DefaultErrorAttributes类,并重写getErrorAttributes()方法。
下面我们来举例说明一下,首先创建自定义类继承 DefaultErrorAttributes类,通过父类的getErrorAttributes()方法获得父类默认的错误信息集合,然后通过添加 @Component 注解,该类将被注册到 Spring 容器中。
package com.itachi.config;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.Map;
@Component
public class MyDefaultErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
// 获取 Spring Boot 默认提供的错误信息
Map<String,Object> myErrorAttributes=super.getErrorAttributes(webRequest, options);
// 添加一个自定义的错误信息
myErrorAttributes.put("message", "出错了!出错了!");
// 移除一个默认的错误信息
myErrorAttributes.remove("error");
return myErrorAttributes;
}
}
创建404.html页面,启动后访问一个不存在的URL路径:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table border="1">
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
</table>
</body>
</html>
记住,这种方式只对返回页面时有效,当返回的是JSON数据的时候还是返回默认数据!
3.2 自定义错误视图
通过DefaultErrorViewResolver的源码分析可知,虽然会在error文件夹下寻找已状态码为名字的页面作为错误页面,但是这种方式不灵活。我们在实际开发中定义的错误页面可能不是以状态码来命名的,甚至说我们可能对于不同的状态码,我们使用一个错误页面来展示。这时候我们就需要自定义错误视图!
自定义错误视图也很简单,定义一个类实现ErrorViewResolver接口即可。
@Component
public class MyErrorViewResolver implements ErrorViewResolver {
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
// 自定义一个 ModelAndView,并且在其中设置 Error 视图和 Error 数据
ModelAndView mv = new ModelAndView("errorPage");
// 加入Spring Boot提供的默认5条错误信息
mv.addAllObjects(model);
return mv;
}
}
在resource/templates文件夹下创建errorPage.html文件。注意该文件的位置,我们给自定义视图设置名字的时候并没有加前缀error/。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>啦啦啦啦啦</title>
</head>
<body>
我是简单的自定义错误视图页面
<table border="1">
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
</table>
</body>
</html>
点击主程序运行,输入错误的URL地址进行测试,测试结果如下图所示,访问到了自定义的错误视图。
3.3 完全自定义
完全自定义的意思就是:完全自定义错误页面,完全自定义错误页面显示的内容,完全自定义返回的JSON内容!
这样的话我们就不能直接实现ErrorViewResolver接口了,而是继承BasicErrorController类,并且至少重写errorHtml()、error()这两个方法!
@Component
public class MyErrorViewResolver2 extends BasicErrorController {
@Autowired
public MyErrorViewResolver2(ErrorAttributes errorAttributes, ServerProperties serverProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, serverProperties.getError(), errorViewResolvers);
}
@Override
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
//获取错误状态码
HttpStatus status = getStatus(request);
//获取 Spring Boot 默认提供的错误信息
Map<String, Object> errorAttributes = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML));
//向默认错误信息中再添加一条
errorAttributes.put("message", "完全自定义的错误!!!");
//自定义的视图,用于客户端是浏览器时返回
ModelAndView modelAndView=new ModelAndView("errorPage",errorAttributes,status);
return modelAndView;
}
@Override
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
// 获取 Spring Boot 默认提供的错误信息,然后添加一个自定义的错误信息
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML));
//向默认错误信息中再添加一条
body.put("message", "完全自定义的错误!!!JASON时候用!");
//当客户端不是浏览器时,返回JSON数据!
return new ResponseEntity<>(body, status);
}
}
第四章 SpringBoot与持久层
对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。
一 整合JDBC
可以使用模板进行创建,勾选JDBC API和MySQl Driver。会发现引入以下的依赖:
<!-- JDBC相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySql相关的-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 自己添加的依赖,方便写例子。该依赖对于整合持久层来说不是必要的!-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
创建yaml文件对连接数据库进行必要的配置!
spring:
datasource:
data-username: root
data-password: root
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
特别注意:这个配置是通过idea的自动提示进行配置的,会发生错误!具体见第1小点(xxxTemplate)的例子!
在配置的时候有3点注意的地方:
红色箭头指向的配置是适用于mysql5.0版本以上的,黄色箭头指向的部分是mysql8.0版本以上的(笔者使用的是mysql5.7.11)- 笔者刚开始的时候,依赖其实是:
这样配置过后,在书写yaml文件的时候报错了,通过网上查阅资料将scope属性改为5.7.11后才恢复正常! - serverTimezone=UTC以前没配置过,这是用于配置时区。springboot会自动配置最新版本的mysql(笔者在使用的时候,mysql版本为8.0版本以上),最新版本的mysql需要配置这个时区!
- 在yaml文件中配置数据库的时候,用户名和密码是全数字的时候需要加双引号!
点击主程序运行:
xxxTemplate
xxxTemplate是springBoot为我们创建好的模板bean,我们可以拿来直接使用。对于JDBC来说即使没有mybatis,Spring 本身也对原生的JDBC 做了轻量级的封装也就是JdbcTemplate!JdbcTemplate 的自动配置是JdbcTemplateConfiguration 类。搜索JdbcTemplate类,并使用alt+7来查看它的方法:
JdbcTemplate主要提供以下几类方法:
- execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
- update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
- query方法及queryForXXX方法:用于执行查询相关语句;
- call方法:用于执行存储过程、函数相关语句。
首先来简单使用它吧,首先数据库中的数据为:
创建sqlController类:
@RestController
@RequestMapping("/jdbc")
public class sqlController {
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("/list")
public List<Map<String, Object>> userList(){
String sql = "select * from number";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
return maps;
}
}
直接进入测试发现居然运行失败:
提示说密码不可用,但是我们通过控制台进入的时候,该数据库可用,查阅资料得知属性名称如果用-隔开的,就会抛出Access denied for user ‘’@‘localhost’ (using password: NO)这个错误。于是我们得修改配置文件为:
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
driverClassName: com.mysql.jdbc.Driver
再次运行成功:
二 整合Druid
值得注意得是对于SpringBoot2.0我们在使用数据源的时候它会自动给你配置Hikari 数据源。如果我们一定要SpringBoot中使用Druid,需要引入它的依赖,自定义一个关于Druid的配置类(类似于SpringBoot中的xxxProperties类)用于与yaml文件中关于Druid的属性绑定起来!(注意:新版本中的SpringBoot已经有了Druid的starter,具体见SpringBoot进阶教程 | 第三篇:整合Druid连接池以及Druid监控)
1.Druid的基本使用
1.1 引入依赖
引入Druid和log4j的依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
1. 2 yaml文件
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
driverClassName: com.mysql.jdbc.Driver
#表示采用Druid作为数据源
type: com.alibaba.druid.pool.DruidDataSource
#当type属性中启动了Druid就可以对Druid的属性进行配置了。
#能进行哪些配置的话得看DruidDataSource的源码。该处是配置数据源初始化大小和最大连接数!
initialSize: 5
maxActive: 20
1.3 建立DruidConfig类
package com.itachi.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
}
1.4 建立sqlController类
@RestController
@RequestMapping("/jdbc")
public class sqlController {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
DataSource dataSource;
@GetMapping("/list")
public List<Map<String, Object>> userList() throws SQLException {
String sql = "select * from number";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
System.out.println(dataSource.getClass());
Connection connection = dataSource.getConnection();
System.out.println(connection);
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
return maps;
}
}
进入测试:
1.5个人疑问:
- @ConfigurationProperties注解使用了过后,在yaml文件中会自动提示。但是这个案例却无法自动提示。后来经过测试可知:只有@ConfigurationProperties标注在类上的时候并且该类有属性对应的get、set方法的时候才能自动提示!。我们这个整合Druid例子注解是标注在方法上面的,所以没有自动提示!
- 我们在将配置文件和实体类属性进行绑定的时候,往往直接在类上加上@Component和@ConfigurationProperties注解,然后在Yaml文件中对该类的属性进行配置。但是若是别人写好的类,无法再上面加上这两个注解怎么办?
这个整合案例给了我们另外一种方式:利用@Bean注解将别人的类加入IOC容器中,然后再加上@ConfigurationProperties注解,这样也能在在Yaml文件中对该类的属性进行配置。只是没有提示而已!
2. Druid的深入使用
2.1 yaml配置文件能配置的属性
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
2.2 监视
Druid的最强大的地方在于它具有监控的功能,监视包括友好SQL语句、SQL攻击、防止SQL注入等等。它也提供了一个默认的Web页面。要使用也比较简单,直接在config中配置即可。
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
//配置 Druid 监控管理后台的Servlet;
//内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式
@Bean
public ServletRegistrationBean statViewServlet() {
//固定写法
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
// 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
// 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
//固定写法
Map<String, String> initParams = new HashMap<>();
//后台管理界面的登录账号
initParams.put("loginUsername", "admin");
//后台管理界面的登录密码
initParams.put("loginPassword", "123456");
//后台允许谁可以访问
//initParams.put("allow", "localhost"):表示只有本机可以访问
//initParams.put("allow", ""):为空或者为null时,表示允许所有访问
initParams.put("allow", "");
//deny:Druid 后台拒绝谁访问
//initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问
//设置初始化参数
bean.setInitParameters(initParams);
return bean;
}
}
配置完毕后,我们可以选择访问 :http://localhost:8080/druid/login.html。访问结果如下:
输入我们配置的用户名和密码,点击登录会发现:
三 整合Mybatis
1. 数据库中的内容
2. 引入依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
可以看到,该starter不是SpringBoot的starter.
3. 创建实体类Number
public class Number {
private Integer id;
private Integer numberage;
private String numberName;
private String numberSex;
//以下省略get、set、toString方法
}
4.创建对应的Mapper类
//该接口就相当于SSM中的Dao层
@Mapper
@Repository
public interface NumberMapper {
List<Number> getNumbers();
Number getById(Integer id);
}
@Mapper注解:表示该类是个mapper类,所谓的mapper类就是在原来SSM框架中的Dao接口。
5.创建mapper对应的xml文件
该xml文件是为了编写sql语句。路径为:resources/mybatis/mapper/NumberMapper.xml。该路径不固定,可以自定义。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itachi.mapper.NumberMapper">
<select id="getNumbers" resultType="Number">
select * from number;
</select>
<select id="getById" resultType="Number" parameterType="int">
select * from number where id = #{id};
</select>
</mapper>
其中mapper namespace很重要,它的值是对应的mapper类所在路径。
6. 在yaml文件中进行配置
#配置数据库连接信息
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
driverClassName: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
initialSize: 5
maxActive: 20
# type-aliases-package指向对应的实体类
# mapper-locations指向mapper对应的xml文件的位置。
mybatis:
type-aliases-package: com.itachi.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
7.创建Controller类
@RestController
@RequestMapping("/mybatis")
public class NumberController {
@Autowired
private NumberMapper numberMapper;
@RequestMapping("/get")
public List<Number> getEmployees(){
return numberMapper.getNumbers();
}
}
点击主程序进行测试: