员工添加
# | lastName | gender | department | birth | 操作 | |
---|---|---|---|---|---|---|
使用Spring Boot进行Web应用的开发:
自动配置原理:
打开WebMVCAutoConfiguration类,找到它的内部类WebMVCAutoConfigurationAdapter里的addResourceHandlers()方法。
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
}
说到这段代码,不得不说一个东西:webjars,我们访问官方网站:https://www.webjars.org/。webjars的目的是以jar包的形式引入一些静态资源。比如我需要引入jquery,就可以将jquery的坐标放到pom.xml中即可。
所有的/webjars/**,都去classpath:/META-INF/resources/webjars/下寻找资源,引入jquery的jar包后,看一下里面目录结构,如果和我的不一致,可以将Compact Middle Packages的勾选去掉。
此时,如果我希望访问jquery.js,可以通过浏览器请求localhost:8080/webjars/jquery/3.4.1/jquery.js即可。
“/**”访问当前项目的任何资源,会去静态资源的文件夹查找("/"表示当前项目的根路径)。
静态资源文件夹:"classpath:/META‐INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"。
发送一个请求,如果没有controller处理,就会请求到静态资源文件夹中,去里面查看有没有。
在static目录下,添加一个静态文件,比方说我添加了一个test.html,通过浏览器访问:localhost:8080/test.html。因为test.html没有controller来处理,此时,请求就发送到了静态资源文件夹里,在静态资源文件夹里,找到了test.html,就给我们展示了出来。
当浏览器访问localhost:8080/的时候,因为/也符合/**,所以请求到了静态资源目录,会去静态资源目录查找index页面。
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
return welcomePageHandlerMapping;
}
private Optional getWelcomePage() {
String[] locations = WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations());
return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}
找到WebMVCAutoConfiguration.class中的welcomePageHandlerMapping()方法,这个方法是用来处理欢迎页请求的。观察new WelcomePageHandlerMapping()方法里倒数第二个参数,会调用getWelcomePage()方法,这里也贴出来了。再看locations变量的值,它是从resourceProperties.getStaticLocations()获取的,点进去一看究竟,方法体直接返回了this.staticLocations,再找staticLocations的赋值,发现ResourceProperties()构造函数中,将CLASSPATH_RESOURCES_LOCATIONS的值赋给了它,而CLASSPATH_RESOURCES_LOCATIONS的值就是上面说的静态资源文件夹了。
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Optional welcomePage, String staticPathPattern) {
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage.get());
this.setRootViewName("forward:index.html");
} else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
this.setRootViewName("index");
}
}
于是,当访问/的时候,就会去静态资源文件夹下查找index.html,也就是上面的这些代码。
因为使用的Spring Boot版本问题,在WebMvcAutoConfigruation中找不到关于请求favicon的请求处理了,同样它也会去静态资源文件夹查找favicon.ico的文件。如果可以找到,就显示这个图标。
于是,可以将一个icon文件放到静态资源文件下查看效果。如果图标不出现,可以使用Ctrl+F5对浏览器进行强制刷新,就可以看到效果了。
最后再说一句,如果手动设置静态资源的文件目录,可以在配置文件中指定,value如果有多值,使用英文逗号分开即可。当手动配置上了静态资源,就不会从默认的静态资源查找了。
spring.resources.static-locations=classpath:/hello/,classpath:/world/
Spring Boot使用的是内置的Tomcat,不支持JSP,如果纯粹些静态页面的话,给开发带来很大麻烦。于是Spring Boot推出了Thymeleaf模板帮助我们解决这个问题,Thymeleaf语法更简单,功能更强大。
org.springframework.boot
spring-boot-starter-thymeleaf
通过ThymeleafProperties可以知道,如果我们将HTML代码放在classpath:/templates/下,Thymeleaf引擎就会识别到并进行渲染。
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
添加一个处理“/success”请求的方法,返回“success”,并在template里加入success.html代码。
@RequestMapping("/success")
public String success() {
// 因为没有带@ResponseBody注解,所以返回的是页面,因为这里使用了Thymeleaf
// 于是Thymeleaf会去classpath:/templates里查找,也就是找classpath:/template/success.html
return "success";
}
Thymeleaf文档地址:https://www.thymeleaf.org/documentation.html,根据自己的版本选择文档的版本,这里我选择3.0版本:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html。
新写一个处理“/showParam”的方法,返回“showParam”,同样,在template里加入showParam.html文件。
@RequestMapping("/showParam")
public String showParam(Map map) {
map.put("key", "value");
map.put("users", Arrays.asList("zhangsan", "lisi", "wangwu"));
return "showParam";
}
1.导入Thymeleaf的名称空间,也就是在html标签上加上一小段代码,用于Thymeleaf的代码提示功能。
2.使用Thymeleaf语法,在html页面取到map中的值并展示。
Order | Feature | Attributes |
1 | Fragment inclusion(片段包含) | th:insert th:replace |
2 | Fragment iteration(片段迭代) | th:each |
3 | Conditional evaluation(条件判断) | th:if th:unless th:switch th:case |
4 | Local variable definition(局部变量定义) | th:object th:with |
5 | General attribute modification(常规属性修改) | th:attr th:attrprepend th:attrappend |
6 | Specific attribute modification(特定属性修改) | th:value th:href th:src …… |
7 | Text (tag body modification)(修改标签体内容) | th:text th:utext |
8 | Fragment specification(声明片段) | th:fragment |
9 | Fragment removal(移除片段) | th:remove |
这段内容有点多,具体的可以参考官方文档中标准表达式语法这一节:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standard-expression-syntax。
官网文档地址:https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications。
Spring Boot为Spring MVC提供了自动配置,自动配置在Spring的默认值之上添加了以下功能:
@Bean
public ViewResolver getMyViewResolver() {
return new MyViewResolver();
}
// 定义一个视图解析器,在项目启动的时候,会自动加载进来
private static class MyViewResolver implements ViewResolver {
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
return null;
}
}
下面这句话和视频里讲解的有点不一样,因为我使用的版本是2.x的,视频里用的1.x的。
如果希望保持Spring Boot对Spring MVC自动配置的功能,并且只是想添加一些MVC的功能(Interceptor、Formatter、ViewController等),可以自己添加一个带有@Configuration注解的类。这个类的类型是WebMvcConfigurer,并且不能带有@EnableWebMvc注解。如果想要完全接管Spring MVC,可以添加@Configuration和@EnableWebMvc注解,也就是全面接管Spring MVC了。
创建自己的MVC配置类,实现WebMvcConfigurer接口,带上@Configuration注解,重写addViewControllers()方法,这种方式既保留了所有的自动配置,也扩展了我们的配置。
package com.atguigu.springboot.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送/atguigu请求,返回success页面
registry.addViewController("/atguigu").setViewName("success");
}
}
原理:
1.WebMVCAutoConfiguration是Spring MVC的自动配置类。
2.找到WebMvcAutoConfigurationAdapter类,它也是实现了WebMvcConfigurer接口,类名上面有@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})的注解,点开EnableWebMvcConfiguration类 ,这个类继承了DelegatingWebMvcConfiguration类,点开这个类,可以发现下面这段代码,这段代码的意思是从容器中获取所有的WebMvcConfigurer类。找到这个类里的addViewControllers()方法,它调用的是configuers的addViewControllers()方法。点进去查看,可以发现它把所有的WebMvcConfigurer的配置都调用了一遍,也就是所有的WebMvcConfigurer都起作用的意思。
// DelegatingWebMvcConfiguration类
@Autowired(required = false)
public void setConfigurers(List configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
// DelegatingWebMvcConfiguration类的addViewControllers()方法具体实现
public void addViewControllers(ViewControllerRegistry registry) {
Iterator var2 = this.delegates.iterator();
while(var2.hasNext()) {
WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
delegate.addViewControllers(registry);
}
}
3.容器中所有的WebMvcConfigurer都会起作用。
实现的效果就是:Spring MVC的自动配置和我们的扩展配置都会起作用。
只需要在配置类上添加@EnableWebMvc注解,Spring Boot对Spring MVC的自动配置都不会执行了,所有的都是我们自己来配置,所有的Spring MVC的自动配置都失效了。
原理:
1.@EnableWebMvc的核心
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
2. 点开DelegatingWebMvcConfiguration类
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
}
3.找到WebMvcAutoConfiguration类
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
}
根据注解@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})的意思,当容器中没有WebMvcConfigurationSupport.class的时候,WebMvcAutoConfiguration才会生效。但是@EnableWebMvc注解上有一个@Import({DelegatingWebMvcConfiguration.class}),并且DelegatingWebMvcConfiguration是WebMvcConfigurationSupport的子类,于是WebMvcConfigurationSupport也被导入了,所以@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})的值就是false了,因此原有的自动配置都失效了。
4.@EnableWebMvc将WebMvcConfigurationSupport导入到容器中了。
5.导入的WebMvcConfigurationSupport是Spring MVC最基本的功能,因此其他自动配置失效了。
在实际开发中,通常不建议这么干,因为全面接管Spring MVC后,自己需要编写的代码会大大增加,更常用的应该是扩展Spring MVC。
模式:
将dao和entity导入,将静态资源导入项目。
启动项目,访问localhost:8080,发现访问不到内容,因为MyMvcConfig类里配置了@EnableWebMvc,去掉之后,再次访问,发现访问到了static目录下的index.html,这是默认的首页。怎么才能访问到templates下的index.html呢?需要走Controller来访问。
前面讲过,Spring Boot访问静态资源的时候,会去这些地方查找资源:classpath:/META‐INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/。它们的优先级是怎么样的呢?
优先级顺序/META-INF/resources>resources>static>public。
这里介绍两种方法:
1.在Controller中加入映射,返回index,从而访问到templates里面的页面。
@RequestMapping({"/", "/index.html"})
public String index() {
return "index";
}
2.在MyMvcConfig中添加视图映射,可以在MyMvcConfig里原有的addViewControllers()里继续添加,也可以写另外一个方法,返回WebMvcConfigurer,带上@Bean将WebMvcConfigurer注入到容器中,在这个WebMvcConfigurer里做视图映射。
// 所有的WebMvcConfigurer组件都会一起起作用
// 将WebMvcConfigurer注入容器
@Bean
public WebMvcConfigurer webMvcConfigurer() {
WebMvcConfigurer webMvcConfigurer = new WebMvcConfigurer() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}
};
return webMvcConfigurer;
}
找到下面的标签,进行修改即可,最前面的/不要忘了,否则是拿不到资源的。
步骤:
在resources下创建名称为i18n的文件夹,加入login.properties,login_zh_CN.properties后,此时IDEA识别到这个文件夹是国际化文件夹,会切换为国际化模式,右键继续添加,点击“Add Properties Files to Resource Bundle”,在弹窗中点击右侧加号,输入en_US,添加即可。点击Resource Bundle 'login',点击左上角的“+”号,输入属性的键,会发现页面右侧多出了几个方格,这里就是方便进行国家化的地方,可以方便的设置这个键在不同配置文件下的值是什么。
打开MessageSourceAutoConfiguration类,它通过messageSourceProperties()方法注入了MessageSourceProperties类,查看MessageSourceProperties类,它里面有一个basename属性,再结合messageSourceProperties()方法上的@ConfigurationProperties(prefix = "spring.messages")可知,通过spring.messages.basename来指定国际化文件夹。于是,我们在配置文件中配置上。看弹幕里有说使用/不能用.,我试了下都可以,在MessageSourceAutoConfiguration类里有getResources()方法,有一个replace()方法,将.换成/,所以.和/都是可以的。
spring.messages.basename=i18n.login
使用Thymeleaf标签取国际化配置文件中的值,注意提前修改properties的编码,否则会中文乱码。
原理:
找到WebMvcAutoConfiguration类中的localeResolver()方法,如果能从配置文件中读取到spring.mvc.locale配置的值,就按照这个值进行国际化,如果读取不到就按照Http请求头中的信息确定国际化。
默认情况下,是没有配置spring.mvc.locale,也就是根据Http请求头中的Accept-Language属性确定的。
既然知道了原理,我们可以在点击按钮的时候,发送请求,根据请求头带的参数,修改请求头中的信息,将Locale替换掉即可。
前端页面需要做修改,再编写一个MyLocaleResolver类来处理,并将MyLocaleResolver注入到容器中。
中文
English
package com.atguigu.springboot.component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;
public class MyLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest httpServletRequest) {
String language = httpServletRequest.getParameter("language");
Locale locale = Locale.getDefault();
if (!StringUtils.isEmpty(language)) {
// 因为参数值是en_US或zh_CN的,这里使用带有两个参数的构造器构造Locale
String[] split = language.split("_");
locale = new Locale(split[0], split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
}
}
在MyMvcConfig中注入自己编写的MyLocaleResolver,需要注意的是,这里的方法名必须是localeResolver(),否则无效。
@Bean
public LocaleResolver localeResolver() {
return new MyLocaleResolver();
}
给form表单添加action和method,给username和password加上name属性,编写对应的controller。
package com.atguigu.springboot.controller;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
@Controller
public class LoginController {
// @RequestMapping(value = "/user/login", method = RequestMethod.POST)
// @PostMapping和@RequestMapping(value = "/user/login", method = RequestMethod.POST)是一样的
// 除了@PostMapping还有@GetMapping、@PutMapping、@DeleteMapping
@PostMapping(value = "/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Map map) {
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
return "dashboard";
} else {
map.put("msg", "用户名密码错误");
return "login";
}
}
}
当登陆失败后,返回登陆页面,将错误信息展示出来,在登陆上面加入p标签,并使用Thymeleaf的th:if标签进行判断。
登陆成功后,执行的是转发,因此登陆成功页面的url还是以localhost:8080/user/login,此时再刷新就会重新提交表单,为了避免重复提交表单,登陆成功后,需要做一个重定向跳转。在视图解析器里添加一个映射,修改登陆成功后的代码。
// 视图解析器添加一个映射
registry.addViewController("main.html").setViewName("dashboard");
// 登陆成功后,修改返回值,因为配置了main.html的映射,所以会跳到dashboard.html页面,而url继续保持main.html不变
return "redirect:/main.html";
现在直接访问localhost:8080/main.html可以绕过登陆,这是不行的,需要编写拦截器进行控制,并将拦截器配置到MyMvcConfig中,才能生效。
给login方法添加HttpSession参数,登陆成功后,写session用于后续拦截器判断权限。
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Map map,
HttpSession httpSession) {
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
httpSession.setAttribute("loginUser", username);
return "redirect:/main.html";
} else {
map.put("msg", "用户名密码错误");
return "login";
}
}
package com.atguigu.springboot.component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginHandlerInterceptor implements HandlerInterceptor {
// 目标方法执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("loginUser");
if (user == null) {// 未登录,进行拦截,并跳转到登录页
request.setAttribute("msg", "没有权限,请先登录");
request.getRequestDispatcher("/index.html").forward(request, response);
return false;
} else {// 已经登录,放行请求
return true;
}
}
}
在MyMvcConfig类里加入addInterceptor()方法,放在new WebMvcConfigurer(){}里面,需要把静态资源放行。
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 这个地方,老师在讲的时候说Spring Boot已经帮我们处理好了静态资源的映射,老师使用的是Spring Boot 1.x版本,可能这是1.x版本里的特性
// 在Spring Boot 2.x版本里,还是需要把静态资源加到放行里面的,否则浏览器拿不到静态资源
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/**")// 拦截任意路径下的任意请求
.excludePathPatterns("/", "/index.html", "/user/login", "/webjars/**", "/asserts/**");// 放行的请求
}
URI:/资源名称/资源标识,使用HTTP请求方式区分对资源的CRUD操作。
功能 | 普通CRUD(uri区分操作) | RESTful CRUD |
查询 | getEmp | emp:GET请求 |
添加 | addEmp | emp:POST请求 |
修改 | updateEmp | emp:PUT请求 |
删除 | deleteEmp | emp:DELETE请求 |
功能 | 请求URI | 请求方式 |
查询所有员工 | emp/list | GET |
查询某个员工 | emp/{id} | GET |
来到添加页面 | emp | GET |
添加员工 | emp | POST |
来到修改页面 | emp/{id} | GET |
修改员工 | emp | PUT |
删除员工 | emp/{id} | DELETE |
修改dashborad.html页面中的a标签请求地址。
编写EmployeeController类来处理员工相关的请求。
package com.atguigu.springboot.controller;
import com.atguigu.springboot.dao.EmployeeDao;
import com.atguigu.springboot.entities.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Collection;
import java.util.Map;
@Controller
@RequestMapping("/emp")
public class EmpolyeeController {
@Autowired
EmployeeDao employeeDao;
@GetMapping("/list")
public String list(Map map) {
Collection employees = employeeDao.getAll();
map.put("emps", employees);
return "list";
}
}
dashboard.html和list.html中有公共的部分,可以将其抽取出来,在其他页面进行引用。
引入公共片段的方式有两个:
在templates下创建commons文件夹,添加bar.html文件,给topbar和sidebar添加属性“th:fragment="片段名称”,用于标识这个片段,将topbar和sidebar放到bar.html中。
Title
在dashboard.html和list.html中使用如下代码引入代码片段。
Thymeleaf支持3种引入公共片段的th属性:th:insert、th:replace、th:include,具体看示例。
© 2011 The Good Thymes Virtual Grocery
高亮是通过class来加上的,所以可以使用Thymeleaf模板里的判断,根据当前请求的uri来设置class的值。Thymeleaf的代码包含支持传递参数,在做代码包含的时候,传一个参数过去,在bar.html页面,根据传过来的这个值,判断哪个选项高亮即可。
将main部分换成下面的代码,主要修改就是从请求带过来的map中读取数据并展示, 在上面合适位置加上一个添加按钮。
员工添加
#
lastName
email
gender
department
birth
操作
给上面的“员工添加”加上href标签:th:href="@{/emp}",在EmployeeController里加上处理请求,让它返回add页面并把部门信息带过来,在templates里加入add.html。add.html从list.html复制过来,将main里面的东西替换掉。
// 在class上有@RequestMapping("/emp")的注解,所以这里是空
@GetMapping("")
public String toAddPage(Map map) {
Collection departments = departmentDao.getDepartments();
map.put("depts", departments);
// 返回添加页面
return "add";
}
编写处理添加请求的Controller,执行添加操作后重定向到/emp/list。
// 在class上有@RequestMapping("/emp")的注解,所以这里是空
@PostMapping("")
// Spring MVC自动将参数和对象进行绑定,要求参数名和JavaBean的属性名一致
public String addEmp(Employee employee) {
employeeDao.save(employee);
// 重定向返回员工列表页面
return "redirect:/emp/list";
}
在测试添加的格式化,可能碰到一个坑,birth属性必须是yyyy/MM/dd的形式,否则就报错了。可以通过spring.mvc.date-format=yyyy-MM-dd在配置文件中来指定格式,此时yyyy/MM/dd就不能用了,只能是yyyy-MM-dd的格式。因此,这里可以考虑使用日期插件来解决。
给list.html页面的修改按钮为a标签,加上跳转地址:th:href="@{/emp/}+${emp.id}",编写controller接收请求,查询员工信息和部门信息,用于回显,并跳转到edit页面,edit页面是从add页面复制过来的,做了下修改。
@GetMapping("{id}")
public String toEditPage(@PathVariable("id") int id,
Map map) {
Employee employee = employeeDao.get(id);
map.put("emp", employee);
Collection departments = departmentDao.getDepartments();
map.put("depts", departments);
// 回到修改页面
return "edit";
}
在修改信息的时候,需要发送PUT请求,但是form是没有PUT请求的,还是需要发送POST表单,另外加一个input项,具体看上面的html页面,这是通过HiddenHttpMethodFilter来处理的。
找到HiddenHttpMethodFilter类中的doFilterInternal()方法,获取“_method”的值,判断“_method”的值是否在ALLOWED_METHODS里面,如果在,就以“_method”的值的方式重新发送请求。
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter((ServletRequest)requestToUse, response);
}
编写修改处理的Controller。
@PutMapping("{id}")
public String editEmp(Employee employee) {
// PathVariable中的{id}会自动封装到employee,给employee的id进行赋值
employeeDao.save(employee);
return "redirect:/emp/list";
}
删除需要发送DELETE请求,所以需要用一个表单包裹delete按钮,以form的post形式把请求发出去了,同样,需要查看当前Spring Boot是否自动注入了HiddenHttpMethodFilter,如果没有,需要配置上spring.mvc.hiddenmethod.filter.enabled=true。
编写删除处理的Controller。
@DeleteMapping("{id}")
public String deleteEmp(@PathVariable("id") int id) {
employeeDao.delete(id);
return "redirect:/emp/list";
}
此时会发现页面的样式变了,因为我们使用了from的缘故,把页面的元素挤下来了。可以考虑把from表单扔到外面,点击按钮,通过js的方式把表单发送出去。
原理:参考ErrorMVCAutoConfiguration类:错误处理的自动配置。
DefaultErrorAttribute类的getErrorAttributes()方法,获取错误页面的信息:
public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
通过BasicErrorController的注解可以看到,这个类就是一个Controller,而且RequestMapping对应的值是${server.error.path:${error.path:/error}},这里冒号是用来判断的,当冒号前面的值为空时,整个表达式的值就是后面的值。在BasicErrorController中,观察errorHtml()方法和error()方法,一个用于返回html页面,一个用于返回ResponseEntity,也就是json格式。
// 返回HTML
@RequestMapping(produces = {"text/html"})
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// 通过resolveErrorView()方法拿到返回的页面和数据
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
// 返回json数据
@RequestMapping
public ResponseEntity
浏览器发送请求的时候,请求头里有一个Accept属性,表示希望接收的值的格式是text/html。
再看其他客户端,比如用postman发送一个请求,它的请求头里Accept属性是*/*,所以给它返回了json数据。
在ErrorMvcAutoConfiguration类找到ErrorPageCustomizer内部类,找到registerErrorPages()方法,这里会new一个ErrorPage,通过this.properties.getError()找到ErrorProperties类,这个是ServerProperties类的一个属性,进入ErrorProperties类,可以看到path的值来自配置文件的error.path的值。默认值是/error,此时就用到BasicErrorController类了。
找到DefaultErrorViewResolver类的resolve()方法,其中viewName来自resolveErrorView()方法的参数,status.series()方法返回Http请求错误码的第一位,SERIES_VIEWS是一个map结构,并在类的静态代码块中做了初始化,放进去了两个对象,分别是{4:4xx}和{5:5xx},通过SERIES_VIEWS的get,于是viewName的值就是4xx或5xx了。
尝试获取模板引擎,如果能获取到,就把用errorViewName构造一个modelAndView扔给模板引擎处理。
如果不能获取到,继续调用resolveReource()方法。在resolveReource()方法中,会去静态资源文件夹中查找错误页面。
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map model) {
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map model) {
String[] var3 = this.resourceProperties.getStaticLocations();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String location = var3[var5];
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception var8) {
}
}
return null;
}
步骤:
程序发生错误(4xx或5xx)的时候,ErrorPageCustomizer类就会生效(定制错误响应规则),发送/error请求,由BasicErrorController来处理。响应页面是由DefaultErrorViewResolver解析得到的。
1.有模板引擎的情况下,会查找templates/error/状态码.html。所以,可以把自定义的4xx和5xx放在对应文件夹下。这里可以使用4xx.html代替所有4开头的错误状态码,使用5xx.html代替所有5开头的错误状态码。并且遵循精确匹配优先,模糊匹配在后的原则。
在模板里可以获取到DefaultErrorAttitudes的值并显示。
status:[[${status}]]
timestamp:[[${timestamp}]]
error:[[${error}]]
exception:[[${exception}]]
message:[[${message}]]
errors:[[${errors}]]
不过,exception的值没有显示出来,原因是:Spring Boot 2.x版本里includeException默认是false的,具体可以查看DefaultErrorAttributes类的初始化方法,给includeException赋值为false,那么,我们可以在配置文件中开启includeException:
# 自定义错误页面,显示exception的内容
server.error.include-exception=true
2.在没有模板引擎的情况下,会从静态资源文件夹下查找错误页面,即查找static/error/状态码.html。因为static文件夹不会经过Thymeleaf模板渲染,所以里面的Thymeleaf语法都无效,如果静态资源使用的是Thymeleaf语法获取的,需要改成普通html语法,另外,它也取不到DefaultErrorAttitudes的值。
3.以上都没有错误页面,就来到Spring Boot的默认错误提示页面,即返回new ModelAndView("error", model);,在ErrorMvcAutoConfiguration类里有一个defaultErrorView()方法,返回默认的error视图。也就是Spring Boot默认的错误提示页面。
编写一个UserNotExistException类,并写一个controller请求/testException,直接throw这个异常。使用postman访问/testException,会返回一个json数据,现在需要定制这个json数据。
package com.atguigu.springboot.exception;
public class UserNotExistException extends RuntimeException {
public UserNotExistException() {
super("用户不存在");
}
}
// 写一个Controller方法,直接抛出这个异常
@RequestMapping("/testException")
public void testException() {
// 直接抛出异常
throw new UserNotExistException();
}
编写MyExceptionHandler来处理UserNotExistException,请求/testException。
第一种方式,不管是浏览器,还是其他客户端,都返回了json数据,这种方式不太好,我们希望自适应。
第二种方式,将请求进行转发,转发给/error,因为/error后面会做自适应,但是发现跳转到了Spring Boot默认的错误页面,观察此时的状态码是200,因为没有200对应的错误页面,所以返回了默认的错误页面,因此就需要手动设置状态码。
package com.atguigu.springboot.controller;
import com.atguigu.springboot.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注解
@ControllerAdvice
public class MyExceptionHandler {
// 1.浏览器和postman获取的都是json数据
@ResponseBody
@ExceptionHandler(UserNotExistException.class)
public Map handleException(Exception exception) {
Map map = new HashMap<>();
map.put("code", "user.notexist");
map.put("message", exception.getMessage());
return map;
}
// 2.因为/error请求支持自适应处理,我们将错误请求转发到/error
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception exception,
HttpServletRequest httpServletRequest) {
Map map = new HashMap<>();
// 需要自己传入错误状态码,否则就是200,可是200没有对应的error页面,又回到了Spring Boot默认的错误页面了
// 详情:BasicErrorController中的errorHtml()方法,查看getStatus()方法发现,状态码是request.getAttribute("javax.servlet.error.status_code")
// 所以我们要自己把状态码改掉
httpServletRequest.setAttribute("javax.servlet.error.status_code", 400);
map.put("code", "user.notexist");
map.put("message", exception.getMessage());
// 转发到/error完成自适应
return "forward:/error";
}
}
上面的第二个方式可以做到自适应,可是不能将数据带出去,观察/error请求处理的两个方法,在返回数据的时候,都调用了一个getErrorAttributes()方法,会获取到一个ErrorAttributes。ErrorAttributes是一个接口,它的实现类是DefaultErrorAttributes,再看ErrorMVCAutoConfiguration类的errorAttributes()方法,它上面有一个注解,@ConditionalOnMissingBean(value = {ErrorAttributes.class}, search = SearchStrategy.CURRENT),也就是ErrorAttributes在容器中不存在的时候,创建一个DefaultErrorAttributes放到容器中,这个DefaultErrorAttributes用于放置attribute。于是,我们可以自己写一个ErrorAttributes,此时DefaultErrorAttributes就不会被注入容器了,在后面执行getErrorAttributes()的时候,我们可以自己控制里面的attribute。
package com.atguigu.springboot.component;
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 MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
// 获取errorAttributes
Map errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
// 向errorAttributes中放入自定义信息
errorAttributes.put("myExceptionInfo", "自定义的用户不存在异常");
return errorAttributes;
}
}
再次发送一个/testException请求,发现返回结果中带上了自定义添加的myExceptionInfo。
{"timestamp":"2020-04-12T10:13:58.823+0000","status":400,"error":"Bad Request","message":"用户不存在","path":"/testException","myExceptionInfo":"自定义的用户不存在异常"}
上面这种方式,需要在MyErrorAttributes添加自定义信息,将异常信息都在MyErrorAttributes里拼装显然不合适,应该交给各自的ExceptionHandler,ExceptionHandler组装好之后,通过request传递出去,MyErrorAttributes根据需要,就从request里取出来一并放到attributes里。
// MyExceptionHandler中的第二种方法修改,将自定义json通过request传递出去
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception exception,
HttpServletRequest httpServletRequest) {
Map map = new HashMap<>();
// 需要自己传入错误状态码,否则就是200,可是200没有对应的error页面,又回到了Spring Boot默认的错误页面了
// 详情:BasicErrorController中的errorHtml()方法,查看getStatus()方法发现,状态码是request.getAttribute("javax.servlet.error.status_code")
// 所以我们要自己把状态码改掉
httpServletRequest.setAttribute("javax.servlet.error.status_code", 400);
map.put("code", "user.notexist");
map.put("message", exception.getMessage());
// 将map放到request中,在MyErrorAttributes中调用
httpServletRequest.setAttribute("ext", map);
// 转发到/error完成自适应
return "forward:/error";
}
// MyErrorAttributes中getErrorAttributes()方法修改,添加了从request域获取自定义信息
public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
errorAttributes.put("myExceptionInfo", "自定义的用户不存在异常");
// 获取MyExceptionHandler中传过来的异常信息
Map map = (Map) webRequest.getAttribute("ext", RequestAttributes.SCOPE_REQUEST);
errorAttributes.put("ext", map);
return errorAttributes;
}
再次发送一个/testException请求,发现返回结果中带上了MyExceptionHandler中自定义添加的信息。
{"timestamp":"2020-04-12T10:23:33.345+0000","status":400,"error":"Bad Request","message":"用户不存在","path":"/testException","myExceptionInfo":"自定义的用户不存在异常","ext":{"code":"user.notexist","message":"用户不存在"}}
Spring Boot默认情况下使用内置Tomcat作为嵌入式Servlet容器。
通用Servlet容器设置
server.xxx=xxx
Tomcat的设置
server.tomcat.xxx=xxx
package com.atguigu.springboot.config;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyServerConfig {
@Bean
public WebServerFactoryCustomizer webServerFactoryCustomizer() {
return new WebServerFactoryCustomizer() {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.setPort(8081);
}
};
}
}
Spring Boot默认以jar包形式启动嵌入式Servlet容器,来启动Spring Boot应用,没有web.xml配置文件。
创建MyServlet,MyFilter,MyListener类。
package com.atguigu.springboot.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 输出一句话
resp.getWriter().write("Hello MyServlet");
}
}
package com.atguigu.springboot.filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpFilter;
import java.io.IOException;
public class MyFilter extends HttpFilter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("MyFilter process");
chain.doFilter(request, response);
}
}
package com.atguigu.springboot.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("初始化");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("销毁");
}
}
Servlet使用ServletRegistrationBean注入,Filter使用FilterRegistrationBean注入,Listener使用ServletListenerRegistrationBean注入。
@Bean
public ServletRegistrationBean servletRegistrationBean() {
return new ServletRegistrationBean(new MyServlet(), "/myServlet");
}
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new MyFilter());
filterRegistrationBean.setUrlPatterns(Arrays.asList("/hello", "/myServlet"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
return new ServletListenerRegistrationBean(new MyListener());
}
Spring Boot帮我们注册了前端控制器DispatcherServlet,在类DispatcherServletAutoConfiguration类中。
@Bean(name = {"dispatcherServletRegistration"})
@ConditionalOnBean(value = {DispatcherServlet.class}, name = {"dispatcherServlet"})
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
// 默认拦截为:/:所有请求,包括静态资源,不拦截jsp请求
// /*:拦截所有请求,也拦截jsp请求
// 可以通过spring.mvc.servlet.path来控制前端控制器拦截的请求路径
registration.setName("dispatcherServlet");
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
打开pom.xml,右键选择Diagrams-Show Dependencies,找到spring-boot-starter-tomcat,右键选择Exclude,在pom中添加上spring-boot-starter-jetty的依赖。启动项目,此时项目就运行在Jetty中了。同理,Undertow也是类似的方法。
视频中讲解的是Spring Boot 1.x版本,现在用的大多数是Spring Boot 2.x版本,所以,这块内容没有办法按照视频上的记录了,因为在Spring Boot 2.x中,有些代码重构了,导致有的类找不到了,于是找了几篇博客看看,学习一下。
查看ServletWebServerFactoryAutoConfiguration类,通过注解,我们可以知道,它在Web环境下运行,当注解条件满足时候,会注入4个类:BeanPostProcessorsRegistrar、EmbeddedTomcat、EmbeddedJetty、EmbeddedUndertow。开启了配置属性的注解,并把ServerProperties作为组件导入,ServerProperties对应的就是配置文件中server开头的一些配置,在里面就包含server.tomcat开头的配置,其中BeanPostProcessorsRegistrar是ServletWebServerFactoryAutoConfiguration的静态内部类,用于注册WebServerFactoryCustomizerBeanPostProcessor和ErrorPageRegistrarBeanPostProcessor。
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(-2147483648)
@ConditionalOnClass({ServletRequest.class})
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties({ServerProperties.class})
@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {
}
另外3个类属于ServletWebServerFactoryConfiguration的嵌套配置类,根据注解,只有当它们的条件满足时,才会将这个类注入进来作为Web Server,Spring Boot默认使用的Tomcat作为Web Server。
以EmbeddedTomcat为例,查看它上面的注解,@ConditionalOnMissingBean(value = {ServletWebServerFactory.class}, search = SearchStrategy.CURRENT),当ServletWebServerFactory不存在的时候,EmbeddedTomcat生效,ServletWebServerFactory有一个方法getWebServer(),当EmbeddedTomcat生效后,会调用getWebServer()方法,也就是TomcatServletWebServerFactory类中的getWebServer()方法。
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, this.getPort() >= 0);
}
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
this.monitor = new Object();
this.serviceConnectors = new HashMap();
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.initialize();
}
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + this.getPortsDescription(false));
synchronized(this.monitor) {
try {
this.addInstanceIdToEngineName();
Context context = this.findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && "start".equals(event.getType())) {
this.removeServiceConnectors();
}
});
this.tomcat.start();
this.rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), this.getClass().getClassLoader());
} catch (NamingException var5) {
}
this.startDaemonAwaitThread();
} catch (Exception var6) {
this.stopSilently();
this.destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", var6);
}
}
}
在这个方法里,首先会创建一个Tomcat对象,设置连接器等配置,最终调用getTomcatWebServer()方法,当Tomcat获取端口号的值是正数时,创建TomcatWebServer,在TomcatWebServer里,执行initialize()方法,在此方法中调用tomcat的start()方法。
找到ServletWebServerFactoryConfiguration类的tomcatServletWebServerFactory()方法,这个方法用来返回一个嵌入式Servlet容器工厂,在方法里面打上断点,找到TomcatServletWebServerFactory类的getWebServer()方法,在方法里面打上断点。Debug模式运行Spring Boot项目。
最下面的方法是SpringApplication.run(SpringBoot04WebRestfulcrudApplication.class, args);依次向上代表内层的方法调用,此时方法栈的顶端是我们打断点的tomcatServletWebServerFactory()方法。于是从下往上看就是代码的运行流程。
传统Web应用使用org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext作为容器,响应式Web应用使用org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext作为容器,其他情况使用org.springframework.context.annotation.AnnotationConfigApplicationContext作为容器。
public ConfigurableApplicationContext run(String... args) {
……
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
context = this.createApplicationContext();
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
……
}
protected ConfigurableApplicationContext createApplicationContext() {
Class> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch(this.webApplicationType) {
case SERVLET:
contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext");
break;
case REACTIVE:
contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext");
break;
default:
contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext");
}
} catch (ClassNotFoundException var3) {
throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
}
}
return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
}
调用onRefresh();方法。
ServletWebServerApplicationContext类的onRefresh()方法。
protected void onRefresh() {
super.onRefresh();
try {
this.createWebServer();
} catch (Throwable var2) {
throw new ApplicationContextException("Unable to start web server", var2);
}
}
ServletWebServerApplicationContext类的createWebServer()方法。
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = this.getServletContext();
if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = this.getWebServerFactory();
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
} else if (servletContext != null) {
try {
this.getSelfInitializer().onStartup(servletContext);
} catch (ServletException var4) {
throw new ApplicationContextException("Cannot initialize servlet context", var4);
}
}
this.initPropertySources();
}
ServletWebServerApplicationContext类的getWebServerFactory()方法。
protected ServletWebServerFactory getWebServerFactory() {
String[] beanNames = this.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.");
} else if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
} else {
return (ServletWebServerFactory)this.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
}
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator();
while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
}
this.prepareContext(tomcat.getHost(), initializers);
return this.getTomcatWebServer(tomcat);
}
嵌入式Servlet容器:应用打包成jar包。优点:简单、便捷。缺点:默认不支持JSP页面,优化定制比较复杂。
外置Servlet容器:支持JSP,应用打包成war包。
org.springframework.boot
spring-boot-starter-tomcat
provided
表示在编译和测试时使用。
package com.atguigu.springboot04;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringBoot04WebJspApplication.class);
}
}
点开Project Structure,左侧找到Modules,点击Web,找到“Web Resource Directory”,里面已经给出了一个提示的目录,双击这个目录,确定即可创建。再找到上面的Deployment Descriptors右边的加号,点击添加web.xml,注意这里需要修改web.xml的位置。
如果要从Controller返回WEB-INF里的页面,需要在配置文件里写上如下内容:
spring.mvc.view.prefix=/WEB-INF/
spring.mvc.view.suffix=.jsp
jar包:执行Spring Boot主类main()的时候,启动ioc容器,创建嵌入式Servlet容器。
war包:启动服务器,服务器再启动Spring Boot应用,启动ioc容器,关键点在SpringBootServletInitializer类。
1.服务器启动,会创建当前web应用里的ServletContainerInitializer实例。
2.ServletContainerInitializer实现类放在jar包的META-INF/service文件夹下,有一个名为
javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名。
3.还可以使用@HandlesType注解,在应用启动的时候,加载我们感兴趣的类。
1.启动Tomcat。
2.org\springframework\spring-web\5.2.5.RELEASE\spring-web-5.2.5.RELEASE.jar!\META-INF\services\javax.servlet.ServletContainerInitializer
在Spring的Web模块里,有这个文件:org.springframework.web.SpringServletContainerInitializer。
3.SpringServletContainerInitializer将带有@HandlesTypes(WebApplicationInitializer.class)注解的这个类型的类都传入到onStartup方法的Set中,为这些WebApplicationInitializer创建实例。
4.对每一个WebApplicationInitializer,调用自身的onStartup()方法。
5.SpringBootServletInitializer是WebApplicationInitializer的实现类,因此也会被创建并执行onStartup()方法。
6.SpringBootServletInitializer实例执行onstartup()的时候,会创建容器,即调用createRootApplicationContext()方法。
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
// 创建Spring应用构建器
SpringApplicationBuilder builder = this.createSpringApplicationBuilder();
builder.main(this.getClass());
ApplicationContext parent = this.getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, (Object)null);
builder.initializers(new ApplicationContextInitializer[]{new ParentContextApplicationContextInitializer(parent)});
}
builder.initializers(new ApplicationContextInitializer[]{new ServletContextApplicationContextInitializer(servletContext)});
builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
// 调用configure()方法,这里的configure()方法就是ServletInitializer类中重写的configure方法,传入了我们自己的SpringBoot应用
builder = this.configure(builder);
builder.listeners(new ApplicationListener[]{new SpringBootServletInitializer.WebEnvironmentPropertySourceInitializer(servletContext)});
// 构建起创建Spring应用
SpringApplication application = builder.build();
if (application.getAllSources().isEmpty() && MergedAnnotations.from(this.getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
application.addPrimarySources(Collections.singleton(this.getClass()));
}
Assert.state(!application.getAllSources().isEmpty(), "No SpringApplication sources have been defined. Either override the configure method or add an @Configuration annotation");
if (this.registerErrorPageFilter) {
application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
}
// 启动这个Spring应用
return this.run(application);
}