Spring Web MVC
DispatcherServlet
- 使用 Java 配置或 web.xml 根据 Servlet 规范进行声明和映射。
- 然后使用 Spring 配置来发现请求映射、视图解析、异常处理等所需的委托组件。
Servlet 配置
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletCxt) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
ac.register(AppConfig.class);
ac.refresh();
// 注册并初始化 DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(ac);
ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
或者
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };//没有用 null 代替
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
处理过程
- 在请求中搜索 WebApplicationContext 并将其绑定为控制器和流程中的其他元素可以使用的属性
- 将语言环境解析器绑定到请求,以便让流程中的元素解析处理请求时使用的语言环境(呈现视图、准备数据等)
- 主题解析器被绑定到让视图等元素决定使用哪个主题的请求
- 如果指定了 multipart 文件解析器,将检查请求的 multipart。找到则将请求封装在 MultipartHttpServletRequest中
- 搜索适当的处理程序。找到则执行与处理程序(预处理程序、后处理程序和控制器)相关联的执行链,以便准备模型或呈现
- 如果是返回模型,则呈现视图。如果没有返回模型,则不会呈现视图,因为请求已经被执行了
特殊 Bean
-
HandlerMapping —— 将请求映射到带有前置和后置拦截器的处理程序。
细节
- 主要实现者 RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping
-
拦截器实现 HandlerInterceptor 的方法
- preHandle(..): 在处理程序之前执行
- postHandle(..): 在处理程序之后执行
- afterCompletion(..): 在请求完成之后执行
public interface HandlerMapping {
/**
* 将 HttpServletRequest 映射到 Handler,并将匹配的 HandlerInterceptor 绑定到 HandlerExecutionChain
*/
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
public class HandlerExecutionChain {
private final Object handler;
private HandlerInterceptor[] interceptors;
}
- HandlerAdapter —— 辅助 DispatcherServlet 调用映射到请求的处理程序。
控制
public interface HandlerAdapter {
/**
* 使用给定 handler 处理请求,返回模型数据和视图名字封装的 ModelAndView
*/
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}
/**
* 字符和 Map 封装成的对象
*/
public class ModelAndView {
private Object view;
private ModelMap model;
}
-
ViewResolver —— 将从处理程序返回的字符串视图名称解析为要呈现给响应的实际视图
- InternalResourceViewResolver —— 将视图解析为 Web 应用的内部资源
- UrlBasedViewResolver —— 直接根据视图的名称解析视图
- ContentNegotiatingViewResolver —— 根据请求文件名或 Accept 标头解析视图
- FreeMarkerViewResolver —— 将视图解析为 FreeMarker 模板
- ResourceBundleViewResolver —— 将视图解析为资源属性文件
- AbstractCachingViewResolver —— 抽象的视图解析器类,提供了缓存视图的功能
public interface ViewResolver {
/**
* 根据指定的视图名称解析返回
*/
View resolveViewName(String viewName, Locale locale) throws Exception;
}
public interface View {
/**
* 根据指定的模型数据渲染视图
*/
void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
-
HandlerExceptionResolver —— 解析异常的策略,可能将异常映射到处理程序、HTML 错误视图或其他目标
- SimpleMappingExceptionResolver —— 异常类名称和错误视图名称间的映射
- DefaultHandlerExceptionResolver —— 解析 Spring MVC 引发的异常,并将它们映射到 HTTP 状态代码
- ResponseStatusExceptionResolver —— 使用 @ResponseStatus 解析异常,并将它们映射到指定 HTTP 状态代码
- ExceptionHandlerExceptionResolver —— 在 @Controller 或 @ControllerAdvice 类调用 @ExceptionHandler 解析异常
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", request.getAttribute("javax.servlet.error.status_code"));
map.put("reason", request.getAttribute("javax.servlet.error.message"));
return map;
}
}
- LocaleResolver —— 提供国际化的视图,解析客户端本地环境
- ThemeResolver —— 提供个性化的布局,解析 web 应用程序可以使用的主题
- MultipartResolver —— 提供表单文件上传,解析 multi-part 请求
注解 Controller
- 声明配置 —— 激活 @Controller 的自动检测
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {
// ...
}
-
请求映射 —— @RequestMapping 完成请求到 Controller 方法的映射
- 变种:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping
- 通过 @PathVariable 访问 URI 变量(支持置于类和方法上)
- {varName:regex} 使用正则表达式声明 URI 变量
- consumes 指定请求的 Content-Type 来限制请求的映射
- produces 指定请求头的 Accept 来限制请求的映射
- params 指定请求的参数
- headers 指定请求的请求头
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
// ...
}
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}
@GetMapping(path = "/pets/{petId}", produces = "application/json;charset=UTF-8")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String petId) {
// ...
}
@GetMapping(path = "/pets", headers = "myHeader=myValue")
public void findPet(@PathVariable String petId) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
- 异常处理
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
- ControllerAdvice —— 全局异常处理、全局数据预处理、全局数据绑定
@ControllerAdvice(basePackages = "com.cc.controller")
public class SpringControllerAdvice {
@ExceptionHandler(RuntimeException.class)
public ModelAndView runtimeException(RuntimeException e) {
return new ModelAndView("error");
}
}
@ControllerAdvice(basePackages = "com.cc.controller")
public class SpringControllerAdvice {
@InitBinder
public void globalInitBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
}
@ControllerAdvice(basePackages = "com.cc.controller")
public class SpringControllerAdvice {
@ModelAttribute(value = "message")
public String globalModelAttribute() {
return "this is from model attribute";
}
}
MVC 配置
- @EnableWebMvc 激活
- 实现 WebMvcConfigurer 添加功能
- 重写 addFormatters 进行类型转换
- 重写 getValidator 自定义全局 Validator 实例,也可以通过 @InitBinder 添加
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
- 重写 addInterceptors 添加拦截器
- 重写 configureContentNegotiation 配置请求的媒体类型
- 重写 configureMessageConverters 配置消息转换器
- 重写 addViewControllers 添加视图控制器
- 重写 configureViewResolvers 配置视图解析器
- 重写 addResourceHandlers 添加静态资源处理器
- 重写 configureDefaultServletHandling 配置默认 Servlet 处理器
- 重写 configurePathMatch 配置路径匹配
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
@Override
public Validator getValidator(); {
// ...
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON);
configurer.mediaType("xml", MediaType.APPLICATION_XML);
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(new ParameterNamesModule());
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.jsp();
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCachePeriod(31556926);
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();//或者 configurer.enable("myCustomDefaultServlet");
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setUseSuffixPatternMatch(true)
.setUseTrailingSlashMatch(false)
.setUseRegisteredSuffixPatternMatch(true)
.setPathMatcher(antPathMatcher())
.setUrlPathHelper(urlPathHelper())
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
}
@Bean
public UrlPathHelper urlPathHelper() {
//...
}
@Bean
public PathMatcher antPathMatcher() {
//...
}
}
视图技术
- Thymeleaf —— 现代化服务器端 Java 模板引擎,支持原生 HTML,可在浏览器中双击预览
- FreeMarker —— 用于生成从 HTML 到电子邮件等类型文本输出的模板引擎
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freemarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
return configurer;
}
}
<#import "/spring.ftl" as spring/>
<html>
...
<form action="" method="POST">
Name:
<@spring.bind "myModelObject.name"/>
<input type="text"
name="${spring.status.expression}"
value="${spring.status.value?html}"/><br>
<#list spring.status.errorMessages as error> <b>${error}</b> <br> </#list>
<br>
...
<input type="submit" value="submit"/>
</form>
...
</html>
- Groovy Markup —— 生成类似于 XML 的标记
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.groovy();
}
// Configure the Groovy Markup Template Engine...
@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("/WEB-INF/");
return configurer;
}
}
yieldUnescaped '<!DOCTYPE html>'
html(lang:'en') {
head {
meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')
title('My page')
}
body {
p('This is an example of HTML contents')
}
}
- 脚本视图
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
</body>
</html>
- JSP
<form:form>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
- RSS and Atom
public class SampleContentAtomView extends AbstractAtomFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Feed feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List<Entry> buildFeedEntries(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
public class SampleContentRssView extends AbstractRssFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Channel feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List<Item> buildFeedItems(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
- PDF and Excel
public class PdfWordList extends AbstractPdfView {
protected void buildPdfDocument(Map<String, Object> model, Document doc, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response) throws Exception {
List<String> words = (List<String>) model.get("wordList");
for (String word : words) {
doc.add(new Paragraph(word));
}
}
}
public class ExcelViewBuilder extends AbstractExcelView {
protected void buildExcelDocument(Map<String,Object> model,HSSFWorkbook wb,HttpServletRequest req, HttpServletResponse resp) {
// ...
}
}
- XSLT
@EnableWebMvc
@ComponentScan
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public XsltViewResolver xsltViewResolver() {
XsltViewResolver viewResolver = new XsltViewResolver();
viewResolver.setPrefix("/WEB-INF/xsl/");
viewResolver.setSuffix(".xslt");
return viewResolver;
}
}
@Controller
public class XsltController {
@RequestMapping("/")
public String home(Model model) throws Exception {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element root = document.createElement("wordList");
List<String> words = Arrays.asList("Hello", "Spring", "Framework");
for (String word : words) {
Element wordNode = document.createElement("word");
Text textNode = document.createTextNode(word);
wordNode.appendChild(textNode);
root.appendChild(wordNode);
}
model.addAttribute("wordList", root);
return "home";
}
}
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes"/>
<xsl:template match="/">
<html>
<head><title>Hello!</title></head>
<body>
<h1>My First Words</h1>
<ul>
<xsl:apply-templates/>
</ul>
</body>
</html>
</xsl:template>
<xsl:template match="word">
<li><xsl:value-of select="."/></li>
</xsl:template>
</xsl:stylesheet>