java框架之SpringBoot(7)-异常处理
前言
在 SpringBoot 项目中,默认情况下,使用浏览器访问一个不存在的地址会返回如下错误页面:
而当客户端未非浏览器时,错误信息则会以 json 数据返回,如下:
会出现如上效果的原因是 SpringBoot 针对错误消息做了自动配置,对应自动配置类为 org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration 。
自定义错误页
查看错误自动配置类会发现在该类中注册了如下组件:
ErrorPageCustomizer
@Bean public ErrorPageCustomizer errorPageCustomizer() { return new ErrorPageCustomizer(this.serverProperties); }
查看该组件类:
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { private final ServerProperties properties; protected ErrorPageCustomizer(ServerProperties properties) { this.properties = properties; } @Override public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + this.properties.getError().getPath()); errorPageRegistry.addErrorPages(errorPage); } @Override public int getOrder() { ; } }
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration.ErrorPageCustomizer
在第 10 行的 registerErrorPages 方法中,注册了一个错误页,错误页路径为 this.properties.getError().getPath() ,该值为
@Value("${error.path:/error}") private String path = "/error";
即,一旦出现了 4xx 或 5xx 错误,该组件就会生效,可用其定制系统发生错误时的转发路径,默认情况下当前请求会转发到 /error 路径。
BasicErrorController
@Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers); }
查看该组件类:
package org.springframework.boot.autoconfigure.web; import java.util.Collections; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.autoconfigure.web.ErrorProperties.IncludeStacktrace; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.ModelAndView; @Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { private final ErrorProperties errorProperties; public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) { this(errorAttributes, errorProperties, Collections.<ErrorViewResolver>emptyList()); } public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) { super(errorAttributes, errorViewResolvers); Assert.notNull(errorProperties, "ErrorProperties must not be null"); this.errorProperties = errorProperties; } @Override public String getErrorPath() { return this.errorProperties.getPath(); } @RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); } @RequestMapping @ResponseBody public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<Map<String, Object>>(body, status); } protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) { IncludeStacktrace include = getErrorProperties().getIncludeStacktrace(); if (include == IncludeStacktrace.ALWAYS) { return true; } if (include == IncludeStacktrace.ON_TRACE_PARAM) { return getTraceParameter(request); } return false; } protected ErrorProperties getErrorProperties() { return this.errorProperties; } }
org.springframework.boot.autoconfigure.web.BasicErrorController
可以看到该组件实际上是一个控制器,用来处理路径为配置中定义的 "${server.error.path:${error.path:/error}}" 请求,如果 server.error 和 error.path 都没有配置,则默认处理路径为 /error 的请求。
控制器中有两个响应方法,分别为第 46 行的 errorHtml 方法和第 58 行的 error 方法,它们都是用来处理路径为 /error 的请求,但 errorHtml 方法返回的错误消息是一个 html 页面,而 error 方法是返回的错误消息是一个 json 数据。通过 @RequestMapping 注解中的 produces 属性来区分客户端需要的错误消息类型,即根据客户端的 accept 请求头区分。具体以哪个页面作为错误页则可看到在第 52 行的 resolveErrorView 方法:
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { for (ErrorViewResolver resolver : this.errorViewResolvers) { ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; }
org.springframework.boot.autoconfigure.web.AbstractErrorController#resolveErrorView
可以看到,该方法时遍历容器中所有的错误视图解析器,如果解析器解析当前请求返回的 modelAndView 不为空,则以该 modelAndView 作为错误页的响应。即:以哪个页面作为错误页是由错误视图解析器的 resolveErrorView 方法的返回值决定。
DefaultErrorViewResolver
@Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean public DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties); }
这是默认配置的错误视图解析器,查看它的 resolveErrorView 方法:
@Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { // 传入字符串形式的状态码 ModelAndView modelAndView = resolve(String.valueOf(status), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve(String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; // 如:error/404 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders .getProvider(errorViewName, this.applicationContext); if (provider != null) { // 如果模板引擎解析器可解析则返回模板视图 return new ModelAndView(errorViewName, model); } // 模板引擎不可解析时 return resolveResource(errorViewName, model); } private ModelAndView resolveResource(String viewName, Map<String, Object> model) { for (String location : this.resourceProperties.getStaticLocations()) { // 遍历静态资源文件夹 try { Resource resource = this.applicationContext.getResource(location); // 获取静态资源 resource = resource.createRelative(viewName + ".html"); // 如:error/404.html if (resource.exists()) { // 判断对应资源是否存在 return new ModelAndView(new HtmlResourceView(resource), model); // 如果存在则返回对应 html 视图 } } catch (Exception ex) { } } return null; }
org.springframework.boot.autoconfigure.web.DefaultErrorViewResolver#resolveErrorView
通过上述代码可以看到,当请求出现错误时,错误视图解析器会在模板路径及静态文件夹路径下寻找以该错误对应状态码命名的 html 页面作为错误响应视图。比如错误代码为 404,那么默认情况下将会寻找在 templates 和 static 等静态资源文件夹下的 error/404.html 页面作为响应页。我们还可以通过使用 4xx.html 和 5xx.html 作为模板页或静态页分别来匹配以 4 开头和 5 开头的错误让其作为该错误的响应页。从 org.springframework.boot.autoconfigure.web.BasicErrorController#errorHtml 方法的返回值可以看到,如果在模板文件夹和静态文件夹下都没有找到对应的错误页,那么将会返回 new ModelAndView("error", model) 对象,而这个 error 视图在错误自动配置类中中已经配置好了:
@Configuration @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { private final SpelView defaultErrorView = new SpelView( "<html><body><h1>Whitelabel Error Page</h1>" + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>" + "<div id='created'>${timestamp}</div>" + "<div>There was an unexpected error (type=${error}, status=${status}).</div>" + "<div>${message}</div></body></html>"); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } @Bean @ConditionalOnMissingBean(BeanNameViewResolver.class) public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - ); return resolver; } }
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration.WhitelabelErrorViewConfiguration
DefaultErrorAttributes
@Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); }
如上我们只说明了错误页的显示规则,那错误页的消息又是从何而来呢?回头看到 org.springframework.boot.autoconfigure.web.BasicErrorController#errorHtml 方法:
@RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); }
org.springframework.boot.autoconfigure.web.BasicErrorController#errorHtml
可以看到返回的 model 的数据为 getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)) 方法的返回值,查看该方法:
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) { RequestAttributes requestAttributes = new ServletRequestAttributes(request); return this.errorAttributes.getErrorAttributes(requestAttributes, includeStackTrace); }
org.springframework.boot.autoconfigure.web.AbstractErrorController#getErrorAttributes
继续查看 this.errorAttributes.getErrorAttributes(requestAttributes, includeStackTrace) 方法:
@Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>(); errorAttributes.put("timestamp", new Date()); addStatus(errorAttributes, requestAttributes); addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); addPath(errorAttributes, requestAttributes); return errorAttributes; } private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) { Integer status = getAttribute(requestAttributes, "javax.servlet.error.status_code"); if (status == null) { errorAttributes.put(); errorAttributes.put("error", "None"); return; } errorAttributes.put("status", status); try { errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase()); } catch (Exception ex) { errorAttributes.put("error", "Http Status " + status); } } private void addErrorDetails(Map<String, Object> errorAttributes, RequestAttributes requestAttributes, boolean includeStackTrace) { Throwable error = getError(requestAttributes); if (error != null) { while (error instanceof ServletException && error.getCause() != null) { error = ((ServletException) error).getCause(); } errorAttributes.put("exception", error.getClass().getName()); addErrorMessage(errorAttributes, error); if (includeStackTrace) { addStackTrace(errorAttributes, error); } } Object message = getAttribute(requestAttributes, "javax.servlet.error.message"); if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null) && !(error instanceof BindingResult)) { errorAttributes.put("message", StringUtils.isEmpty(message) ? "No message available" : message); } } private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) { BindingResult result = extractBindingResult(error); if (result == null) { errorAttributes.put("message", error.getMessage()); return; } ) { errorAttributes.put("errors", result.getAllErrors()); errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. Error count: " + result.getErrorCount()); } else { errorAttributes.put("message", "No errors"); } } private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) { StringWriter stackTrace = new StringWriter(); error.printStackTrace(new PrintWriter(stackTrace)); stackTrace.flush(); errorAttributes.put("trace", stackTrace.toString()); } private void addPath(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) { String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri"); if (path != null) { errorAttributes.put("path", path); }
org.springframework.boot.autoconfigure.web.DefaultErrorAttributes#getErrorAttributes
通过上述代码我们可以知道在错误页中我们可以使用如下错误信息:
timestamp:时间戳 status:状态码 error:错误提示 exception:异常对象 message:异常信息 errors:JSR303 数据校验错误信息
自定义错误信息
上述已经描述了我们如何使用自定义的错误页,但是使用的错误信息还依旧是 SpringBoot 默认配置的,如果我们想要自己定制错误信息,则可通过如下方式。
方便下面测试先编写如下异常类及控制器:
package com.springboot.webdev2.ex; public class MyException extends RuntimeException { public MyException() { super("运行期间出异常了"); } }
com.springboot.webdev2.ex.MyException
package com.springboot.webdev2.controller; import com.springboot.webdev2.ex.MyException; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class TestController { @RequestMapping("test") public void test1(){ throw new MyException(); } }
com.springboot.webdev2.controller.TestController
方式一:自定义异常处理器
package com.springboot.webdev2.component; import com.springboot.webdev2.ex.MyException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class MyExceptionHandler { @ResponseBody @ExceptionHandler(MyException.class) public Map<String,Object> handleException(Exception e){ Map<String, Object> map = new HashMap<>(); map.put("code", "myCode"); map.put("msg", "自定义的异常"); return map; } }
com.springboot.webdev2.component.MyExceptionHandler
该方式是 SpringMVC 提供的异常处理方式,缺点:使用该方式失去了 SpringBoot 本身的根据客户端的不同自适应响应数据类型的功能。
方式二:转发到错误处理路径
我们已经知道默认情况下出现异常 SpringBoot 会将请求转发到 /error ,那么如果我们通过异常处理器手动转发到该路径,并可手动将我们需要的错误信息放入请求域,我们就可以解决方式一的缺点并且可以在错误页使用我们自己的错误信息了。
package com.springboot.webdev2.component; import com.springboot.webdev2.ex.MyException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(MyException.class) public String handleException(Exception e, HttpServletRequest request){ // SpringBoot 默认使用的状态码就是请求域中的 javax.servlet.error.status_code request.setAttribute(); Map<String, Object> map = new HashMap<>(); map.put("code", "myCode"); map.put("msg", "自定义的异常"); request.setAttribute("ext", map); return "forward:/error"; } }
com.springboot.webdev2.component.MyExceptionHandler
<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>[[${status}]]</h1> <h2>[[${message}]]</h2> <!--请求域中取错误信息--> <h2>[[${ext.msg}]]</h2> </body> </html>
templates/error/4xx.html
可以发现,该方式依旧有一个缺点:放入请求域中的数据未被序列化,所以只可在转发到的模板页中取到,而在客户端是非浏览器时是拿不到自定义的错误信息的。
方式三:自定义错误处理控制器
出现异常时 SpringBoot 会将请求转发到 /error ,而处理该请求的控制器为 BaseErrorController ,查看该控制器注册信息我们也可以知道,当我们自己定义一个 org.springframework.boot.autoconfigure.web.ErrorController 组件注册到容器中时,那么默认的 BasicErrorController 就不生效了,所以我们可以在自定义的错误处理控制器中根据我们的需要取到我们合适的信息返回。该方式比较复杂,明显不合适,了解即可,略过。
方式四:自定义ErrorAttributes
通过查看 org.springframework.boot.autoconfigure.web.BasicErrorController 我们已经知道,不管是响应 html 还是 json 错误信息,它们的错误信息都是通过 this.errorAttributes.getErrorAttributes(requestAttributes, includeStackTrace) 方法取到,而 this.errorAttributes 对应的组件实际上在错误自动配置类中已经注册,即 DefaultErrorAttributes ,所以我们可以自定义一个的 org.springframework.boot.autoconfigure.web.ErrorAttributes 组件注册到容器中,重写它的 getErrorAttributes 方法,通过手动取得自定义的错误信息返回即可。
package com.springboot.webdev2.component; import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import java.util.Map; @Component public class MyErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace); errorAttributes.put("ext", requestAttributes.getAttribute("ext", RequestAttributes.SCOPE_REQUEST)); return errorAttributes; } }
com.springboot.webdev2.component.MyErrorAttributes
package com.springboot.webdev2.component; import com.springboot.webdev2.ex.MyException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(MyException.class) public String handleException(Exception e, HttpServletRequest request){ // SpringBoot 默认使用的状态码就是请求域中的 javax.servlet.error.status_code request.setAttribute("javax.servlet.error.status_code", 400); Map<String, Object> map = new HashMap<>(); map.put("code", "myCode"); map.put("msg", "自定义的异常"); request.setAttribute("ext", map); return "forward:/error"; } }
com.springboot.webdev2.component.MyExceptionHandler
<!DOCTYPE html> <html lang="cn"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>[[${status}]]</h1> <h2>[[${message}]]</h2> <!--请求域中取错误信息--> <h2>[[${ext.msg}]]</h2> </body> </html>
templates/error/4xx.html
java框架之SpringBoot(7)-异常处理的更多相关文章
- java框架之SpringBoot(1)-入门
简介 Spring Boot 用来简化 Spring 应用开发,约定大于配置,去繁从简,just run 就能创建一个独立的.产品级别的应用. 背景: J2EE 笨重的开发.繁多的配置.低下的开发效率 ...
- java框架之SpringBoot(3)-日志
市面上的日志框架 日志抽象层 日志实现 JCL(Jakarta Commons Logging).SLF4J(Simple Logging Facade For Java).JBoss-Logging ...
- java框架之SpringBoot(4)-资源映射&thymeleaf
资源映射 静态资源映射 查看 SpringMVC 的自动配置类,里面有一个配置静态资源映射的方法: @Override public void addResourceHandlers(Resource ...
- java框架之SpringBoot(5)-SpringMVC的自动配置
本篇文章内容详细可参考官方文档第 29 节. SpringMVC介绍 SpringBoot 非常适合 Web 应用程序开发.可以使用嵌入式 Tomcat,Jetty,Undertow 或 Netty ...
- java框架之SpringBoot(14)-任务
使用 maven 创建 SpringBoot 项目,引入 Web 场景启动器. 异步任务 1.编写异步服务类,注册到 IoC 容器: package zze.springboot.task.servi ...
- java框架之SpringBoot(15)-安全及整合SpringSecurity
SpringSecurity介绍 Spring Security 是针对 Spring 项目的安全框架,也是 Spring Boot 底层安全模块默认的技术选型.它可以实现强大的 Web 安全控制.对 ...
- java框架之SpringBoot(16)-分布式及整合Dubbo
前言 分布式应用 在分布式系统中,国内常用 Zookeeper + Dubbo 组合,而 SpringBoot 推荐使用 Spring 提供的分布式一站式解决方案 Spring + SpringBoo ...
- 【java框架】SpringBoot(5)--SpringBoot整合分布式Dubbo+Zookeeper
1.理论概述 1.1.分布式 分布式系统是若干独立计算机的集合,这些计算机对于用户来讲就像单个系统. 由多个系统集成成一个整体,提供多个功能,组合成一个板块,用户在使用上看起来是一个服务.(比如淘宝网 ...
- java框架之SpringBoot(2)-配置
规范 SpringBoot 使用一个全局的配置文件,配置文件名固定为 application.properties 或 application.yml .比如我们要配置程序启动使用的端口号,如下: s ...
随机推荐
- .NET CORE控制器里的方法取传参的坑
把以前的ASP.NET MVC的项目改成用.NET CORE来写,写好了部署上去了,结果问题一大堆,今天慢慢检查了一下,发现一个大坑: 写控制器里的方法接收参数数都是直接写在控制器的方法参数里的,如: ...
- select 与 time.After 配合使用的问题
今天在工作中发现了一个有趣的现象. 在一个select中设定了两个定时器,本来预计哪个定时器到达就运行相应指令的,但是发现最终只有时间最短的定时器一直得到执行,其它定时器完全没有得到执行. packa ...
- docker下创建crontab定时任务失败
创建过程 基础镜像采用的centos7.2,需要安装一下crontab,在dockerfile中加以下语句就可以了: # crontab jobs RUN yum -y install crontab ...
- EL表达式具体解释
在 JSP 页面中,使用标签库取代传统的 Java 片段语言来实现页面的显示逻辑已经不是新技术了,然而.由自己定义标签非常easy造成反复定义和非标准的实现.鉴于此.出现了 JSTL ( JSP St ...
- 使用Docker快速创建.Net Core2.0 Nginx负载均衡节点
本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址 www.cnblogs.com/tdws 一.Self-Host Kestrel 1. 在vs2017中新建dotnet core2. ...
- windows上,任务管理器中,进程命令行太长怎么办
一.前言 在windows上,有时候需要查看进程命令行,但是有的进程的命令行太长了,很难看全 此时,可以使用下面的方法解决(红框改为自己要查看的进程即可): C:\Users\Gaoyu>wmi ...
- 在k8s上配置ingress并启用HTTPS证书
第一步,定义Secret文件 该文件设置tls的证书私钥和公钥内容,通过base64编码的内容 tls.crt: 证书公钥 tls.key: 证书私钥 示例 apiVersion: v1 kind: ...
- Timeline Storyteller 现已加入自定义图表库
前言 下载地址: https://store.office.com/en-us/app.aspx?assetid=WA104381136&sourcecorrid=328f5e2b-e973- ...
- [原创]WB Android客户端架构总结:发WB工作队列设计
先简单说下需求,发一条WB包含多种类型,例如图片.视频.文字等,发送工作不能阻塞UI,工作队列易于扩展,方便优化. 几个重要的类: JobManager:统一管理Job列表,包括job的添加.启动.终 ...
- XML文档中的xmlns、xmlns:xsi和xsi:schemaLocation
文章转载自:https://yq.aliyun.com/articles/40353 相信很多人和我一样,在编写Spring或者Maven或者其他需要用到XML文档的程序时,通常都是将这些XML文档头 ...