Spring Web MVC 框架(通常简称“Spring MVC”)是一个富“模型视图控制”的web框架。Spring MVC 可以让你创建特殊的@Controller 或 @RestController beans来处理 HTTP请求。控制器中映射到HTTP的方法使用 @RequestMapping 注解。
下面是一个@RestController 响应JSON数据的经典例子:
[java] view plain copy
<span style="font-size:14px;">@RestController
@RequestMapping(value="/users")
public class MyRestController {
@RequestMapping(value="/{user}", method=RequestMethod.GET)
public User getUser(@PathVariable Long user) {
// ...
}
@RequestMapping(value="/{user}/customers", method=RequestMethod.GET)
List<Customer> getUserCustomers(@PathVariable Long user) {
// ...
}
@RequestMapping(value="/{user}", method=RequestMethod.DELETE)
public User deleteUser(@PathVariable Long user) {
// ...
}
}</span>
Spring MVC 是spring框架核心的一部分,详细信息可以在 reference documentation.获取。 这里也有一些涉及Spring MVC的指南可以在 spring.io/guides 获取。
27.1.1 Spring MVC 自动配置
Spring Boot 为Spring MVC提供自动配置以便使很多应用可以良好的工作。
自动配置在Spring基础上遵循一下特点:
包含 ContentNegotiatingViewResolver 和BeanNameViewResolver beans.
支持服务端静态资源, 包括支持 WebJars (详见下文).
自动注解 Converter, GenericConverter, Formatter beans.
支持 HttpMessageConverters (详见下文).
自动注解MessageCodesResolver (详见下文).
支持静态index.html .
支持自定义 Favicon (详见下文).
自动使用 ConfigurableWebBindingInitializer bean (详见下文).
如果你想保持 Spring Boot MVC特性且想增加一些额外的 MVC configuration ( 拦截器,格式器, 视图控制等),你可以增加你自己的WebMvcConfigurerAdapter类型的 @Configuration 类且不使用 @EnableWebMvc 。如果你想提供自定义的 RequestMappingHandlerMapping,RequestMappingHandlerAdapter 或 ExceptionHandlerExceptionResolver 实例,你可以声明一个 WebMvcRegistrationsAdapter 实例提供以上组件;
如果你想获得完全的Spring MVC控制,你可以在你自己的 @Configuration注解上增加@EnableWebMvc 。
27.1.2 HttpMessageConverters
Spring MVC 使用 HttpMessageConverter 接口来转换HTTP请求和响应。很明显默认开箱即用,例如对象可以自动转换成JSON(使用jackson库)或XML(如果可用使用jackson XML拓展否则使用JAXB)。字符串默认使用 UTF-8 编码。
如果你需要增加或自定义转换器,你可以使用Spring Boot的 HttpMessageConverters 类:
[java] view plain copy
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.*;
import org.springframework.http.converter.*;
@Configuration
public class MyConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = ...
HttpMessageConverter<?> another = ...
return new HttpMessageConverters(additional, another);
}
}
任何 HttpMessageConverter bean出现在环境中都将被加入转换器列表。所以你也可考虑重写默认的转换器。
27.1.3 定制 JSON 序列化 和发序列化
如果你正在使用 Jackson来序列化和反序列化JSON数据,那么你可能是想写自己的JsonSerializer 和JsonDeserializer 类。自定义序列化通常 通过一个模块使用Jackson注册 ,但是Spring Boot 提供一个 @JsonComponent 注解可以让其立马注册为Spring Beans。
你可以直接在 JsonSerializer 或 JsonDeserializer 实现上使用 @JsonComponent 。也可以用在包含序列化/反序列化的内部类上。例如:
import java.io.*;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import org.springframework.boot.jackson.*;
@JsonComponent
public class Example {
public static class Serializer extends JsonSerializer<SomeObject> {
// ...
}
public static class Deserializer extends JsonDeserializer<SomeObject> {
// ...
}
}
所有在 ApplicationContext 中的 @JsonComponent beans都将自动使用Jackson注册,且由于 @JsonComponent 使用了元注解 @Component,所以常见的组件扫描规则也适用于 @JsonComponent 。
Spring Boot 也提供 JsonObjectSerializer 和 JsonObjectDeserializer 基础类来实现当序列化对象时使用标准的Jackson版本。详见Java文档(See the Javadoc for details)。
27.1.4 MessageCodesResolver
Spring MVC 有一个为绑定了错误的错误消息而生成错误代码的策略: MessageCodesResolver。如果你设置了 spring.mvc.message-codes-resolver.format 属性为 PREFIX_ERROR_CODE 或POSTFIX_ERROR_CODE (参考 DefaultMessageCodesResolver.Format中的枚举)则Spring Boot将为你创建一个消息码转换器。
27.1.5 Static Content
默认情况下Spring Boot将classpath路径或者 ServletContext 根路径 下一个名为 /static (或 /public 或/resources 或/META-INF/resources)的目录视作静态内容。它使用Spring MVC中的 ResourceHttpRequestHandler 来实现,所以你可以通过增加自己的 WebMvcConfigurerAdapter 然后重写 addResourceHandlers 方法来修改这个行为。
在单独运行的web应用中容器中的默认servlet也将启动,作为后备用来处理Spring决定不处理的 ServletContext 根下内容。通常情况下这种情况不会发生(除非你修改了默认的MVC配置),因为Spring总 能够通过 DispatcherServlet 处理请求。
通常,资源文件映射在 /** 路径,但是你可以通过 spring.mvc.static-path-pattern 来修改。例如,迁移所有资源到 /resources/** 可以通过以下修改完成:
spring.mvc.static-path-pattern=/resources/**
你也可以使用spring.resources.static-locations ( 使用目录路径列表替换默认值) 自定义静态资源位置。如果你这样做了,默认的欢迎页将切换至你定义的位置进行查找,如果在你启用的任何位置中有 index.html 文件,那么它将作为应用的欢迎页使用。
除了上述的标准静态资源位置,特殊情况适用于 Webjars content. 任何在 /webjars/** 路径下以webjars格式打包的资源将以jar文件的方式来加载。
如果你的应用将使用jar的方式打包,不要使用 src/main/webapp 目录。 尽管这个目录是一个常见的标准,它仅在war打包的方式下工作而在生成jar包时被大多数编译工具所忽略。
Spring Boot同样支持Spring MVC的高级资源处理特性,允许使用诸如缓存静态资源或使用带版本Webjars的URLs。
要使用带版本的Webjars URLs,只需添加 webjars-locator 依赖。然后声明你的Webjar,拿jQuery来举例就像在 "/webjars/jquery/x.y.z/dist/jquery.min.js" 中的 "/webjars/jquery/dist/jquery.min.js",其中 x.y.z 是Webjars的版本。
如果你在使用JBOOS, 你需要声明 webjars-locator-jboss-vfs依赖来代替 webjars-locator; 否则 所有的Webjars将作为404处理
要使用缓存,以下配置会为所有的静态资源配置一个缓存方案,有效的在URLs上增加一个内容哈希,像这样
<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>:
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**
使用模板的资源链接在运行时会重写, 这是由于ResourceUrlEncodingFilter, 为 Thymeleaf and FreeMarker自动配置的. 你可以在使用 JSPs时手动声明这个过滤器. 其他的模板引擎目前暂不自动支持, 但可通过宏/辅助工具自定义模板和使用 ResourceUrlProvider来实现
当动态资源被加载,例如一个JavaScript模块加载后,重命名的文件就不支持。这也是为何会提供其他策略且允许一同使用。一个“固定”的策略会在URL中增加静态版本字符串,除非文件名发生变化:
spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**
spring.resources.chain.strategy.fixed.enabled=true
spring.resources.chain.strategy.fixed.paths=/js/lib/
spring.resources.chain.strategy.fixed.version=v12
有了以上配置,在 "/js/lib/" 下的JavaScript模块可以使用一个固定版本策略 "/v12/js/lib/mymodule.js" 而其他资源仍可以使用<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>
查看 ResourceProperties 获取更多支持操作。
这个特性已经完整地在这个专门的博客 blog post 和在Spring 框架的 reference documentation中写明
27.1.6 Custom Favicon
Spring Boot会在配置的静态目录位置和classpath(按默认顺序)根路径下寻找 favicon.ico ,如果存在它将自动用作这个应用的图标。
27.1.7 ConfigurableWebBindingInitializer
Spring MVC 使用 WebBindingInitializer 来为特殊请求初始化一个 WebDataBinder .如果你创建了你自己的 ConfigurableWebBindingInitializer @Bean, Spring Boot将自动配置 Spring MVC去使用它.
27.1.8 Template engines
除了用作REST Web服务外,你也可以使用Spring MVC来为动态HTML内容服务。Spring MVC支持多种模板技术包括 Thymeleaf,FreeMarker 和JSPs。很多其他的模板引擎也发布了他们自己的Spring MVC集成方案。
Spring Boot 内支持以下模板引擎的自动配置:
FreeMarker
Groovy
Thymeleaf
Mustache
尽可能的不使用JSPs , 这有一些已知的问题( known limitations )在使用内嵌的servlet容器时
当你使用上述中的一种模板引擎时,默认配置情况下,你的模板将自动从 src/main/resources/templates来加载。
IntelliJ IDEA 决定不同的 classpath 依赖于你如何运行你的应用。 在IDE中通过main方法运行和通过使用Maven或Gradle打包的jar文件来运行应用的顺序是不同的。这会导致Spring Boot不能发现classpath下的模板。如果你受此问题影响,你可以在IDE中重新调整模块级别和资源优先级。 或者, 你可以配置模板前缀在classpath中搜索每一个模板目录:classpath*:/templates/.
27.1.9 Error Handling
Spring Boot默认提供了一个 /error 映射以一种合理的方式处理所有的错误,它会在servlet容器中注册一个全局的错误页。于客户端而言,它会返回一个JSON响应包含错误的详细信息,HTTP状态和异常消息。对浏览器而言则提供一个使用HTML格式的相同数据展现的白页错误视图(如想定制,仅需增加一个解决错误的视图)。要想完全替换默认的行为,你需要实现 ErrorController 且注册一个此类型的bean,或者增加一个使用现有机制但是替换内容的 ErrorAttributes 类型的bean。
这BasicErrorController 可以用作定制ErrorController的基础类。 这对你想增加一个对新内容类型的处理特别有用 (默认是明确处理 text/html 且返回一个回调). 你只需继承 BasicErrorController 创建你自己的类,然后增加一个用 @RequestMapping 带有 produces 属性的公共方法。
你也可以为一个贴别的控制器或其他类型增加 @ControllerAdvice 来定制返回的JSON文档。
@ControllerAdvice(basePackageClasses = FooController.class)
public class FooControllerAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler(YourException.class)
@ResponseBody
ResponseEntity<?> handleControllerException(HttpServletRequest request, Throwable ex) {
HttpStatus status = getStatus(request);
return new ResponseEntity<>(new CustomErrorType(status.value(), ex.getMessage()), status);
}
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
return HttpStatus.valueOf(statusCode);
}
}
上述例子中,如果FooController包下定义的任何controller抛出了 YourException ,将返回一个代表 CustomerErrorType 对象的json替换原有的 ErrorAttributes 对象。
定制错误页
如果你想为给定的状态码定制HTML错误页,你需要在/error 文件夹下增加相应的页面。错误页既可以使用静态HTML(也就是在任何静态资源文件夹下增加)或者使用模板构建。但是文件名必须是准确的状态码或者掩码。
例如,映射 404 到一个静态HTML文件,你的文件结构应该类似如下:
src/
+- main/
+- java/
| + <source code>
+- resources/
+- public/
+- error/
| +- 404.html
+- <other public assets>
要使用一个FreeMarker 模板来映射所有的 5xx 错误,你需要一个类似下面的结构:
src/
+- main/
+- java/
| + <source code>
+- resources/
+- templates/
+- error/
| +- 5xx.ftl
+- <other templates>
对于更复杂的映射,你需要增加实现 ErrorViewResolver接口的beans。
public class MyErrorViewResolver implements ErrorViewResolver {
@Override
public ModelAndView resolveErrorView(HttpServletRequest request,
HttpStatus status, Map<String, Object> model) {
// Use the request or status to optionally return a ModelAndView
return ...
}
}
你也可以使用普通的SpringMVC特性诸如 @ExceptionHandler methods [email protected] 。那么 ErrorController 将接管任何未处理的异常。
映射SpringMVC之外的错误页
对于不使用SpringMVC的应用,你可以使用 ErrorPageRegistrar 接口直接注册 ErrorPages 。那么尽管你没有SpringMVC的 DispatcherServlet , 提取工作仍将在内嵌的servlet容器下直接工作且有效运行。
@Bean
public ErrorPageRegistrar errorPageRegistrar(){
return new MyErrorPageRegistrar();
}
// ...
private static class MyErrorPageRegistrar implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
}
}
注意,如果你注册一个 ErrorPage 到一个路径上,确保它被一个 Filter 拦截处理(例如像一些通常没有Spring-web框架的做法,如Jersy和Wicket),并且这个 Filter 需明确被注册为一个 ERROR dispatcher,例如:
@Bean
public FilterRegistrationBean myFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new MyFilter());
...
registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
return registration;
}
(默认的 FilterRegistrationBean 不包含 ERROR 调度类型)。
WebSphere 应用服务器错误处理
一旦发布到一个servlet容器中,Spring Boot会使用它的错误页拦截器去转发带有错误状态的请求到合适的错误页上。请求只有在响应尚未提交时才会被正确转发。通常,WebSphere应用服务器8.0及后续版本会在完成一个servlet服务方法之后便提交响应。你需要通过设置com.ibm.ws.webcontainer.invokeFlushAfterService 为 false来关闭此行为。
27.1.10 Spring HATEOAS
如果你想开发一个用于超媒体的REST风格API,Spring Boot为 大多数应用可以良好工作的Spring HATEOAS提供自动配置。自动配置代替了需要使用 @EnableHypermediaSupport 和注册beans数量来方便构建超媒体基础应用,包含一个 LinkDiscoverers (对客户端的支持)和一个 ObjectMapper 配置来正确引导响应给所需。 ObjectMapper 配置需要基于 spring.jackson.* 属性或者一个存在的Jackson2ObjectMapperBuilder bean。
你可以通过使用 @EnableHypermediaSupport 控制Spring HATEOAS的配置。注意这将不显示上面所说的 ObjectMapper 的定义。
27.1.11 CORS 支持
Cross-origin
resource sharing (CORS) 是一个 W3C
specification 被most
browsers 实现来让你方便区分哪个跨域请求被授权, 用以替换像IFRAME或JSONP的不安全和不强大。
在4.2版本中,SpringMVC就很好的支持CORS。在你的Spring Boot应用中使用带有 @CrossOrigin 注解的控制器方法CORS 配置不需要任何特殊配置。全局CORS配置会通过注册一个含有自定义 addCorsMappings(CorsRegistry) 方法的WebMvcConfigurer bean定义:
@Configuration
public class MyConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**");
}
};
}
}
27.2 JAX-RS 和Jersey
对于REST终端,如果你更喜欢使用JAX-RS设计模型,你可以使用可代替SpringMVC的任何解决方案。如果在你的应用环境中将Jersey 1.x和Apache CXF的 Servlet 或Filter 注册为一个 @Bean ,那么Jersey 1.x和Apache CXF会得到很好的支持。Jersey 2.x 开始对Spring有了原生支持,所以我们在Spring Boot中为她提供了自动配置支持作为起步。
要用Jersey 2.x起步,仅需加入 spring-boot-starter-jersey 依赖,然后在你所有注册的终端中需要配一个 ResourceConfig 类型的 @Bean :
@Component
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
register(Endpoint.class);
}
}
Jersey对可执行文件扫描支持有限。例如她在运行一个可执行的war文件时不会扫描 WEB-INF/classes 下面的终端。为了避免这个限制,不建议使用打包技术且终端应如上所示使用 register 方法单独注册。
你也可注册任意数量的实现 ResourceConfigCustomizer的beans来满足更多定制需求。
所有的注册终端应该被带有HTTP方法的 @Components 注解(如@GET 等) 例如:
@Component
@Path("/hello")
public class Endpoint {
@GET
public String message() {
return "Hello";
}
}
由于终端作为Spring的一个组件后,她的生命周期被Spring管理,所以你可以@Autowired 依赖注入,使用 @Value获取外部的配置值。Jersey servlet会被注册进来且默认映射到 /* 。你可以通过增加@ApplicationPath 到你的 ResourceConfig 来改变这个映射规则。
通常Jersey将被视为一个名为 jerseyServletRegistration 类型为 ServletRegistrationBean 的 @Bean 的 Servlet。默认情况下,这个servlet是懒加载,除非你自定义了 spring.jersey.servlet.load-on-startup 。你可以通过创建同名bean来禁用或重写上述的bean ,也可以使用一个拦截器通过设置 spring.jersey.type=filter 代替servlet(此情况下需要一个Bean来替换或重写jerseyFilterRegistration )。Servlet可以通过 @Order (你可以设置spring.jersey.filter.order 来启用)配置顺序。Servlet和Filter都可以通过 spring.jersey.init.* 指定的map属性给定初始化参数。
为了让你明白如何配置你可以参考这个Jersey例子。同样给出一个 Jersey 1.x 的例子。注意在Jersey 1.x的样例中Spring-boot maven插件被配置为不打包某些Jersey的jar包以便他们可以被JAX_RS实现扫描到(因为在样例中需要他们被扫描到Filter中)。你可能需要在任何你的JAX-RS方法作为内嵌Jars打包的时刻做同样的操作。
27.3 内部Servlet容器支持
Spring Boot引入了对内嵌Tomcat,Jetty,和Undertow 服务器的支持。开发者仅需使用适当的“Starter”来获得一个全配置的实例。嵌入式服务默认会在 8080 监听HTTP请求。
如果你选择在CentOS上使用Tomcat需要注意, 默认情况下, 需要一个临时文件夹用于存储编译的JSPs,上传的文件等. 这个目录可能被 tmpwatch 删除导致你的应用加载失败. 为了避免该情况的发生,你需要配置你的 tmpwatch 以便 tomcat.*目录不被删除, 或者配置 server.tomcat.basedir 让内嵌的Tomcat使用一个不同的位置。
27.3.1 Servlets, 拦截器, 监听器
在使用内嵌的Servlet容器时,你可以按Servlet手册注册Servlets,Filtes和所有的监听器(例如 HttpSessionListener)通过使用Spring beans的方式或者扫描查找Servlet组件的方式。
注册Servlets,拦截器,监听器为Spring beans
任何 Servlet, Filter 或Servlet *Listener 实例作为一个Spring bean被注册到内嵌的容器中。这样很方便你从 application.properties 中获取一个配置期间配置的值。
默认情况下,如果环境中仅有一个Servlet会被映射到 / ,在 多Servlet beans的情况下,bean名字将用作路径前缀。拦截器会映射到 /*。
基于约定的映射不是足够的灵活,你可以通过使用 ServletRegistrationBean,FilterRegistrationBean 和 ServletListenerRegistrationBean 类来获得完全控制。
27.3.2 Servlet 环境初始化
内嵌的Servlet容器不会直接执行Servlet 3.0+的javax.servlet.ServletContainerInitializer 接口,或Spring的 org.springframework.web.WebApplicationInitializer 接口。这个人为设计移栽减少在一个war包中运行的第三方类库破坏Spring Boot应用的风险。
如果你需要在Spring Boot应用中执行Servlet环境的初始化,你需要注册一个实现了org.springframework.boot.context.embedded.ServletContextInitializer 接口的bean。这个唯一 onStartup 方法提供了对 ServletContext的访问,在需要时可以很方便的当已存在的 WebApplicationInitializer 的适配器。
扫描获取Servlets, Filters, and listeners
在使用内嵌的容器时,使用 @ServletComponentScan 可以自动发现并注册 @WebServlet, @WebFilter, 和@WebListener 注解的类。
@ServletComponentScan 在单独容器中无效,因为容器内部的发现机制会代替她。
27.3.3 嵌入式Web应用环境
在Spring Boot中为内嵌的Servlet容器使用了一个新类型的 ApplicationContext 。 EmbeddedWebApplicationContext 是一个特殊的 WebApplicationContext ,通过寻找一个单一的EmbeddedServletContainerFactory bean来自引导。通常TomcatEmbeddedServletContainerFactory, JettyEmbeddedServletContainerFactory, 或UndertowEmbeddedServletContainerFactory 会被自动配置。
你通常不需要了解这些实现类。大部分的应用会为你自动配置和创建 ApplicationContext andEmbeddedServletContainerFactory 。
27.3.4 自定义内嵌Servlet容器
使用Spring的 Environment 属性可以设置通用Servlet容器配置。通常你会在你的 application.properties 文件中进行定义。
通用服务设置包含:
网络设置: HTTP请求的监听端口(server.port),绑定接口地址到 server.address,等。
Session设置:session是否持久态(server.session.persistence), session超时 (server.session.timeout),session数据位置(server.session.store-dir) 和session-cookie配置(server.session.cookie.*).。
错误管理:错误页位置 (server.error.path)等。
SSL
HTTP compression
Spring Boot会尽可能的暴露相同的设置,但这并不一定合适。针对此情况,命名空间提供了server-specific定制(参见server.tomcat 和server.undertow) 。对于实体,访问日志配置了内嵌Servlet容器的特征码。
参考 ServerProperties 类获取完整列表
编程方式自定义
如果你需要以编程的方式配置你内嵌的Servlet容器,你可以注册一个实现 EmbeddedServletContainerCustomizer 接口的Spring bean。 EmbeddedServletContainerCustomizer 对 提供很多自定义设置方法的ConfigurableEmbeddedServletContainer 的访问。
import org.springframework.boot.context.embedded.*;
import org.springframework.stereotype.Component;
@Component
public class CustomizationBean implements EmbeddedServletContainerCustomizer {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.setPort(9000);
}
}
直接定义ConfigurableEmbeddedServletContainer
如果上述自定义技术有太多限制,你也可注册你自己的TomcatEmbeddedServletContainerFactory, JettyEmbeddedServletContainerFactory或 UndertowEmbeddedServletContainerFactory bean。
@Bean
public EmbeddedServletContainerFactory servletContainer() {
TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
factory.setPort(9000);
factory.setSessionTimeout(10, TimeUnit.MINUTES);
factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/notfound.html"));
return factory;
}
Setters提供了很多的配置选项。一些保护的方法‘hooks’也会提供给你以便你需要更自由地处理一些事情。查看源码去了解详细。
37.3.5 JSP 局限性
当使用内嵌的Servlet容器(被作为可执行文件方式打包)运行一个Spring Boot应用时,对JSP的支持会有一些限制:
如果你使用Tomcat的方式进行War打包,即一个可执行的war可以运行,也可被标准容器(没有限制,不限于Tomcat)解包。一个可执行的JAR无效,因为在Tomcat中很难解码
如果你使用Jetty的方式进行War打包,即得到一个可执行的war,且同样可被任何标准容器解包。
Undertow 不支持JSPs
创建一个自定义的 error.jsp 也不会覆盖这个默认的错误处理视图,该视图原本应被自定义的错误页替换。
原文地址:https://www.cnblogs.com/zhq-blogs/p/8972956.html