回顾:
错误处理机制
1. SpringBoot默认的错误处理机制
① pc端访问
浏览器发送请求的请求头:
② 客户端访问(默认响应一个json格式的数据)
客户端发送请求的请求头:
原理: 参照错误处理的自动配置类(ErrorMvcAutoConfiguration)
ErrorMvcAutoConfiguration给容器中添加了以下组件:
1) DefaultErrorAttributes
页面共享信息:
2) BasicErrorController(处理默认的/error请求)
3) ErrorPageCustomizer(定制错误的响应规则)
系统出现错误以后来到error请求进行处理
4) DefaultErrorViewResolver
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = this.resolve(String.valueOf(status), 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<String, Object> model) {
//默认SpringBoot可以去找到一个页面 如: error/404
String errorViewName = "error/" + viewName;
//如果模板引擎可以解析这个页面地址就用模板引擎解析, 返回一个ModelAndView
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
//如果模板引擎不可解析, 则在静态资源文件夹下找errorViewName对应的页面 如error/404.html
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
① 一旦系统出现4xx或5xx之类的错误, ErrorPageCustomizer就会生效.
② 此时就会被BasicErrorController处理, 根据请求头做区分, 判断是返回html页面的数据还是json格式的数据.
@RequestMapping(
produces = {"text/html"} //将会产生html类型的数据, 浏览器发送的请求来到这个方法处理
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//生成具体的错误页面, 包含页面地址和页面内容
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView == null ? new ModelAndView("error", model) : modelAndView;
}
@RequestMapping
@ResponseBody //产生json格式数据, 其他客户端发送的请求来到这个方法处理
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = this.getStatus(request);
return new ResponseEntity(body, status);
}
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
Iterator var5 = this.errorViewResolvers.iterator();
ModelAndView modelAndView;
do {
if (!var5.hasNext()) {
return null;
}
//根据所有的ErrorViewResolver(DefaultErrorViewResolver的子类)得到ModelAndView
ErrorViewResolver resolver = (ErrorViewResolver)var5.next();
modelAndView = resolver.resolveErrorView(request, status, model);
} while(modelAndView == null);
return modelAndView;
}
2. 自定义响应
① 定制错误的页面(pc端)
1) 有模板引擎的情况下, SpringBoot是在模板文件夹下找对应状态码的文件, 如error/404.html, 即只需在templates文件夹下新建error/404.html, 即可在返回状态码是404的情况下跳转到该自定义的页面.
也可以通过4xx.html非精确匹配, 当精确匹配不到具体的页面时, 则如果是4开头的状态码, 则匹配至4xx.html页面
页面能获取的信息:
● timestamp: 时间戳
● status: 状态码
● error: 错误提示
● exception: 异常对象
● message: 异常的消息
● errors: JSR303数据校验的错误信息
2) 在模板引擎无法解析的情况下, 则在静态资源文件夹下找
3) 以上都不满足的情况下, 则跳转SpringBoot默认的错误页面.
② 定制json格式数据(客户端)
自定义异常类:
1) 自定义异常处理, 返回定制的json数据
2) 转发到/error请求进行自适应响应效果处理
3) 将定制的数据携带出去(以上两种方式是无法携带定制的数据的)
出现错误以后, 会转发至/error请求, 被BasicErrorController处理, 响应出去可以获取的数据是由getErrorAttributes得到的, 该方法是父类(AbstractErrorController(ErrorController))规定的方法, SpringBoot规定, 当容器中没有ErrorController的bean时, 则使用BasicErrorController, 那么解决办法
● 自定义一个ErrorController的实现类(或者是编写AbstractErrorController的子类), 放入容器中即可.
● 页面上能用的数据或者json返回的数据都是通过errorAttributes.getErrorAttributes得到的, 是由容器中 DefaultErrorAttributes.getErrorAttributes()默认进行数据处理的;
最终的效果: 响应是自适应的, 可以通过ErrorAttributes改变需要返回的内容.
页面:
json数据:
配置嵌入式Servlet容器
SpringBoot默认使用的是嵌入式的Servlet容器(Tomcat)
1) 配置内嵌的Servlet容器(Tomcat)
① 在配置文件中直接修改和server有关的配置(application.properties / application.yml)
server.port=8081
server.context-path=/crud
server.tomcat.uri-encoding=UTF-8
② EmbeddedServletContainerCustomizer(嵌入式的Servlet容器的定制器, 用来修改Servlet容器的配置)
//配置嵌入式的Servlet容器
@Bean
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
return new EmbeddedServletContainerCustomizer() {
//定制嵌入式的Servlet容器相关的规则
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.setPort(8083); //修改容器的端口
}
};
}
2) 注册三大组件(Servlet / Filter / Listener)
利用ServletRegistrationBean / FilterRegistrationBean / ServletListenerRegistrationBean对三大组件进行注册.
① ServletRegistrationBean(注册Servlet)
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req,resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("Hello MyServlet");
}
}
@Bean
public ServletRegistrationBean myServlet(){
ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),"/myServlet");
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
② FilterRegistrationBean(注册Filter)
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("MyFilter process...");
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new MyFilter());
registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));
return registrationBean;
}
③ ServletListenerRegistrationBean(注册Listener)
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("contextInitialized...web应用启动");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("contextDestroyed...当前web项目销毁");
}
}
@Bean
public ServletListenerRegistrationBean myListener(){
ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener());
return registrationBean;
}
3) 切换其他的Servlet容器
SpringBoot默认支持在三种容器之间切换
① Tomcat(默认使用)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
② Jetty(适合开发长连接应用)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
③ Undertow(不支持jsp, 但并发性能好)
<dependency>
<artifactId>spring-boot-starter-undertow</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
4) 嵌入式Servlet容器的自动配置原理
① EmbeddedServletContainerAutoConfiguration(嵌入式的Servlet容器自动配置)
@AutoConfigureOrder(-2147483648)
@Configuration
@ConditionalOnWebApplication
@Import({EmbeddedServletContainerAutoConfiguration.BeanPostProcessorsRegistrar.class})
public class EmbeddedServletContainerAutoConfiguration {
@Configuration
@ConditionalOnClass({Servlet.class, Tomcat.class}) //两个类: 原生Servlet和Tomcat, 判断是否引入了Tomcat的依赖
@ConditionalOnMissingBean(
value = {EmbeddedServletContainerFactory.class}, //判断当前容器中有没有用户自己定义的嵌入式的Servlet容器工厂
//该工厂的作用是创建嵌入式的Servlet容器
search = SearchStrategy.CURRENT
)
public static class EmbeddedTomcat {
public EmbeddedTomcat() {
}
@Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory();
}
}
}
EmbeddedServletContainerFactory(获取嵌入式的Servlet容器 ==> EmbeddedServletContainer)
该工厂的继承树正式三个内嵌的Servlet容器(Tomcat / Jetty / Undertow)
EmbeddedServletContainer(容器)
以TomcatEmbeddedServletContainerFactory为例
public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
Tomcat tomcat = new Tomcat(); //创建一个Tomcat实例
//配置Tomcat的基本环节
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
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.getTomcatEmbeddedServletContainer(tomcat); //将配置好的Tomcat传入进去, 返回一个嵌入式的Tomcat容器
} //该方法执行完成之后则启动Tomcat容器
步骤小结:
① SpringBoot根据导入的依赖情况, 给容器中添加相应的
② 容器中某个组件要创建对象就会惊动后置处理器, 只要是嵌入式的Servlet容器工厂, 后置处理器就工作.
③ 后置处理器, 从容器中获取所有的定制器, 调用定制器的定制方法.
5) 嵌入式Servlet容器的启动原理
步骤:
① SpringBoot应用启动运行run方法
② 创建IOC容器对象,并初始化容器,创建容器中的每一个组件, 如果是web应用, 则创建 AnnotationConfigEmbeddedWebApplicationContext, 如果不是, 则创建AnnotationConfigApplicationContext
③ 刷新创建好的ioc容器
④ webIOC容器会创建嵌入式的Servlet容器
⑤ 获取嵌入式的Servlet容器工厂
EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
从ioc容器中获取EmbeddedServletContainerFactory 组件, TomcatEmbeddedServletContainerFactory创建对象,后置 处理器一看是这个对象,就获取所有的定制器来先定制Servlet容器的相关配置;
⑥ 使用容器工厂获取嵌入式的Servlet容器: this.embeddedServletContainer = containerFactory .getEmbeddedServletContainer(getSelfInitializer());
⑦ 嵌入式的Servlet容器创建对象并启动Servlet容器,先启动嵌入式的Servlet容器,再将ioc容器中剩下没有创建出的对象获取出来, 也就是IOC容器启动并创建嵌入式的Servlet容器
配置外置的Servlet容器
优点: 简单 / 便捷
缺点: 不支持jsp / 优化和定制比较复杂
1) 安装外部Tomcat环境并配置web项目
2) 小结
① 必须创建一个war项目
② 将嵌入式的Tomcat指定为provided
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
③ 必须编写一个SpringBootInitializer的实现类, 并重写configure方法
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { //SpringBoot应用程序的构建器
return application.sources(SpringBoot04WebJspApplication.class); //传入SpringBoot应用的主程序
}
}
④ 启动服务器即可使用外部Tomcat
外部Tomcat启动SpringBoot的原理
区别:
jar包: 执行SpringBoot的主方法, 启动ioc容器, 创建嵌入式的Servlet容器
war包: 启动服务器, 服务器启动SpringBoot应用, 再启动ioc容器
servlet3.0(Spring注解版):
8.2.4 Shared libraries / runtimes pluggability:
规则:
① 服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例:
② ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下,有一个名为javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名
③ 还可以使用@HandlesTypes,在应用启动的时候加载我们感兴趣的类;
流程:
① 启动Tomcat
② org\springframework\spring-web\4.3.14.RELEASE\spring-web-4.3.14.RELEASE.jar!\META-INF\services\javax.servlet.ServletContainerInitializer:
Spring的web模块里面有这个文件:org.springframework.web.SpringServletContainerInitializer
③ SpringServletContainerInitializer将@HandlesTypes(WebApplicationInitializer.class)标注的所有这个类型的类都传入到onStartup方法的Set<Class<?>>;为这些WebApplicationInitializer类型的类创建实例;
④ 每一个WebApplicationInitializer都调用自己的onStartup;
⑤ 相当于我们的SpringBootServletInitializer的类会被创建对象,并执行onStartup方法
⑥ SpringBootServletInitializer实例执行onStartup的时候会createRootApplicationContext;创建容器
protected WebApplicationContext createRootApplicationContext(
ServletContext servletContext) {
//创建SpringApplicationBuilder
SpringApplicationBuilder builder = createSpringApplicationBuilder();
StandardServletEnvironment environment = new StandardServletEnvironment();
environment.initPropertySources(servletContext, null);
builder.environment(environment);
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
builder.initializers(
new ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
//调用configure方法,子类重写了这个方法,将SpringBoot的主程序类传入了进来
builder = configure(builder);
//使用builder创建一个Spring应用
SpringApplication application = builder.build();
if (application.getSources().isEmpty() && AnnotationUtils
.findAnnotation(getClass(), Configuration.class) != null) {
application.getSources().add(getClass());
}
Assert.state(!application.getSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
if (this.registerErrorPageFilter) {
application.getSources().add(ErrorPageFilterConfiguration.class);
}
//启动Spring应用
return run(application);
}
7)、Spring的应用就启动并且创建IOC容器
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
analyzers = new FailureAnalyzers(context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
//刷新IOC容器
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
return context;
}
catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}