《Spring实战》笔记(四):Spring MVC
1 跟踪请求
Spring MVC中请求经过的路径如下:
Spring MVC请求的跟踪.JPG
2 搭建Spring MVC
2.1 配置Dispatcher
Dispatcher可以使用JavaConfig来配置,也可以使用web.xml配置,但前者要在支持Servlet 3.0以上的服务器中才能正常工作。
public class MyWebAppInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
原理:在Servlet 3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer
接口的类,如果能发现的话,就会用它来配置Servlet容器。Spring提供了这个接口的实现,名为SpringServletContainerInitializer
,这个类反过来又会查找实现WebApplicationInitializer
的类并将配置的任务交给它们来完成。Spring 3.2引入了一个WebApplicationInitializer
的基础实现,也就是AbstractAnnotationConfigDispatcherServletInitializer
。所以在我们扩展了AbstractAnnotationConfigDispatcherServletInitializer
的时候,将项目部署到Servlet 3.0以上的容器时,容器就会自动发现它并用它来配置上下文。我们也可以通过自己实现WebApplicationInitializer
来进行配置。
2.2 自定义Dispatcher配置
上面的例子实现了AbstractAnnotationConfigDispatcherServletInitializer
的三个抽象方法。如果要自定义更多的配置,可以重写其他方法。比如说customizeRegistration()
,在AbstractAnnotationConfigDispatcherServletInitializer
将DispatcherServlet
注册到Servlet容器中之后,就会调用customizeRegistration()
,并将Servlet注册后得到的Registration.Dynamic
传递进来。
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("/img"));
}
借助customizeRegistration()
方法中的ServletRegistration.Dynamic
,我们能够完成多项任务,包括通过调用setLoadOnStartup()
设置load-on-startup优先级,通过setInitParameter()
设置初始化参数,通过调用setMultipartConfig()
配置Servlet 3.0对multipart的支持等等。
2.3 两个应用上下文
DispatcherServlet
会创建一个应用上下文,用来加载配置类或配置文件中的所声明的bean。getServletConfigClasses()
用来指定DispatcherServlet
加载应用上下文的时候所用的配置。在Spring Web项目中,还有另一个上下文由ContextLoaderListener
所创建,getRootConfigClasses()
用来指定由ContextLoaderListener
创建上下文时要用到的配置。
@Configuration
@ComponentScan("me.ye.springinaction.controller")
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
/**
* 配置JSP视图解析器
*
* @return
*/
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
/**
* 配置静态资源的处理
*
* @param configurer
*/
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
@Configuration
@ComponentScan(basePackages = {"me.ye.springinaction"},
excludeFilters = {
@Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
})
public class RootConfig {
}
2.4 编写控制器
@Controller
public class HomeController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home() {
return "home";
}
}
控制器的测试:
public class HomeControllerTest {
@Test
public void testHome() throws Exception {
HomeController controller = new HomeController();
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(MockMvcRequestBuilders.get("/")).andExpect(
MockMvcResultMatchers.view().name("home")
);
}
}
如果要往视图传递数据,可以使用Model作为控制器的参数,Model实际上就是一个Map(也就是key-value对的集合),它会传递给视图,这样数据就能渲染到客户端了。当调用addAttribute()方法并且不指定key的时候,那么key会根据值的对象类型推断确定。如果你希望使用非Spring类型的话,那么可以用java.util.Map来代替Model。
@Controller
public class HomeController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Model model) {
model.addAttribute("key", "value");
return "home";
}
}
2.5 接收请求
Spring MVC允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:
- 查询参数(Query Parameter)
- 表单参数(Form Parameter)
- 路径变量(Path Variable)
1.查询参数,使用@RequestParam
声明参数,如下面的控制器将会接收来自/userInfo?id=xxx
的请求
@RequestMapping(value="/userInfo", method = RequestMethod.GET)
public String userInfo(Model model, @RequestParam("id") String id) {
User user = userService.getUserById(id);
model.addAttribute("user", user);
return "userInfo";
}
2.路径变量,和查询参数有点类似,但是参数是直接作为uri的,如下的控制器将会接收来自/userInfo/xxx
的请求
@RequestMapping(value="/userInfo/{id}", method = RequestMethod.GET)
public String userInfo(Model model, @PathVariable("id") String id) {
User user = userService.getUserById(id);
model.addAttribute("user", user);
return "userInfo";
}
3.表单参数(或者是其他实体),这一类请求需要用post
@RequestMapping(value = "/register", method=RequestMethod.POST)
public String register(User user) {
User savedUser = userService.save(user);
return "redirect: /userInfo/" + savedUser.getId();
}
如上面例子,当InternalResourceViewResolver看到视图格式中的“redirect:”前缀时,它就知道要将其解析为重定向的规则,而不是视图的名称。需要注意的是,除了“redirect:”,InternalResourceViewResolver还能识别“forward:”前缀。当它发现视图格式中以“forward:”作为前缀时,请求将会前往(forward)指定的URL路径,而不再是重定向。
3 视图解析原理
在Spring MVC中,定义了一个叫ViewResolver
的接口
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
当给resolveViewName()方法传入一个视图名和Locale对象时,它会返回一个View实例,View也是一个接口
public interface View {
String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
String PATH_VARIABLES = View.class.getName() + ".pathVariables";
String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
String getContentType();
void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}
View的任务就是接受模型以及Servlet的request和response对象,并将输出结果渲染到response中。
对于ViewResolver,Spring提供了多个实现。其中,InternalResourceViewResolver
将视图解析为Web应用的内部资源,一般为JSP。
Spring使用Thymeleaf3:
@Configuration
@ComponentScan("me.ye.springinaction.controller")
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Bean
public ViewResolver viewResolver(SpringTemplateEngine springTemplateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(springTemplateEngine);
resolver.setCharacterEncoding("UTF-8");
return resolver;
}
@Bean
public SpringTemplateEngine springTemplateEngine(
ITemplateResolver templateResolver) {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setEnableSpringELCompiler(true);
engine.setTemplateResolver(templateResolver);
return engine;
}
@Bean
public ITemplateResolver templateResolver() {
SpringResourceTemplateResolver resolver =
new SpringResourceTemplateResolver();
resolver.setApplicationContext(applicationContext);
resolver.setPrefix("/WEB-INF/templates/");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setSuffix(".html");
return resolver;
}
}
4 Spring MVC 高级技术
4.1 添加其他的Servlet和Filter
按照AbstractAnnotationConfigDispatcherServletInitializer
的定义,它会创建DispatcherServlet
和ContextLoaderListener
。如果我们想往Web容器中注册其他组件的话,只需创建一个新的初始化器就可以了。最简单的方式就是实现Spring的WebApplicationInitializer
接口。
public class MyServletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//注册Servlet
ServletRegistration.Dynamic myServlet =
servletContext.addServlet("myServlet", MyServlet.class);
//映射Servlet
myServlet.addMapping("/custom/**");
}
}
同理,我们也可以创建新的WebApplicationInitializer
实现注册Listener和Filter。
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
//注册Filter
ServletRegistration.Dynamic myServlet =
servletContext.addFilter("myFilter", MyFilter.class);
//映射Filter
myServlet.addMappingForUrlPatterns(null, false, "/custom/*");
}
4.2 处理Multipart形式的数据
- 配置multipart解析器
解析器有两个选择:
- CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求;
- StandardServletMultipartResolver:依赖于Servlet 3.0对multipart请求的支持(始于Spring 3.1)。
后者使用Servlet所提供的功能支持,并不需要依赖任何其他的项目,所以优先选用后者。
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
解析器的限制条件需要在servlet中配置,有两种方法:
(1) 如果我们采用Servlet初始化类的方式来配置DispatcherServlet的话,这个初始化类应该已经实现了WebApplicationInitializer,那我们可以在Servlet registration上调用setMultipartConfig()方法,传入一个MultipartConfigElement实例。
DispatcherServlet ds = new DispatcherServlet();
Dynamic registration = context.addServlet("appServlet", ds)'
registration.addMapping("/");
registration.setMultipartConfig(new MultipartConfigElement("/tmp"))
(2) 如果Servlet初始化类继承了AbstractAnnotationConfigDispatcherServletInitializer
,可以重写customizeRegistration()
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("/temp"));
}
- 接收MultipartFile
页面表单要将<form>
标签的enctype属性设置为multipart/form-data
<form method="post" enctype="multipart/form-data" action="/signUp">
...
<lable>Image</lable>
<input type="file" name="image" accept="image/jpeg, image/png, image/gif">
...
</form>
后台接收的时候,使用注解@RequestPart
来指定对应part的数据,对于上传的文件,可以使用byte[]
进行接收,但是使用byte[]
操作起来比较麻烦。Spring提供了MultiPartFile
接口用以接收上传的文件,该接口提供了处理上传文件的相关功能。比如说,transferTo()
方法可以很方便地将上传的文件写入到文件系统中。
@PostMapping("/signUp")
public String signUp(@RequestPart("image") MultipartFile image, User user)
throws Exception{
...
image.transferTo(new File("/image/" + image.getOriginalFilename()));
...
}
Servlet3.0还提供了一个Part
接口用来接收上传的文件,使用方法与MultiPartFile
差别不大。
4.3 异常处理
Spring有如下方式可以将异常转换为相应:
- Spring自身特定的异常会自定转换为指定的HTTP状态码
- 使用
@ResponseStatus
,将某一个异常映射为特定的HTTP状态码 - 在方法上可以添加
@ExceptionHandler
,用来处理异常
-
在默认情况下,Spring会将自身的一些异常自动转换为合适的状态码。
一些Spring异常的默认映射.JPG -
可以使用
ResponseStatus
将异常映射为特定的状态码。对于没有映射的的异常,默认为500的状态码。
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public class DemoException extends RuntimeException {
}
3.可以用一个独立的方法用来处理特定的异常,该方法要用@ExceptionHandler
标注。这样可以免去在每个处理器方法中的重复编写异常捕捉代码。但是该方法只能适用于当前的Controller。如果要使用所有的控制器,要将方法定义到控制器通知类。
@ExceptionHandler(NullPointerException.class)
public String handException() {
return "error";
}
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home() {
...
}
4.4 为控制器添加通知
为类加上@ControllerAdvice
注解,即为控制器通知类。控制器通知类的方法可以应用到整个应用程序中所有的Controller。在控制器通知类可以定义一个或多个以下的方法:
- @ExceptionHandler注解标注的方法;
- @InitBinder注解标注的方法;
- @ModelAttribute注解标注的方法。
@ControllerAdvice
public class CommonControllerHandler {
@ExceptionHandler(NullPointerException.class)
public String handException() {
return "error";
}
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyyMMdd"));
}
@ModelAttribute("key")
public String initModel() {
return "value";
}
}
@InitBinder
用于数据绑定,对于接收的参数,有时候需要进行转换,比如说将字符串类的日期转换为Date类型,这个时候就可以使用@InitBinder
,请求到后台的时候会先进入@InitBinder
标注的方法注册相关的编辑器。
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyyMMdd"));
}
@ModelAttribute
有两种用法,一种是标注在方法,为处理器方法设置Model的属性,另一种是标注在处理器方法的参数,用以接收参数并作为Model返回。
- 标注在方法上,被标注的方法将会先于控制器方法执行:
@ModelAttribute("key")
public String initModel() {
return "value";
}
这样子会在controller方法返回页面的Model里面添加一个name为key而value为value的属性。
@ModelAttribute()
public voidinitModel(Model model) {
model.addAttibute("key", "value");
}
无返回值的的方法可以直接用model来设置属性。
- 标注在方法的参数:
@GetMapping(value = "/")
public String home(@ModelAttribute("key")String key) {
return "home";
}
从客户端接收的request param可以做为Model再次返回给页面。也可以接收form-data,如下:
@PostMapping("/user")
public String user(@ModelAttribute("user") User user) {
return "user";
}
4.5 跨重定向请求传递数据
重定向使用return "redirect:/anotherPagePath"
的格式进行。进行重定向的时候,上次请求的参数将不能继续传递下去。要实现跨重定向传递数据,主要有两种方法:
- 使用URL模板以路径变量或查询参数的形式传递数据;
- 通过flash属性发送数据。
@PostMapping("/user")
public String user(User user, RedirectAttributes model) {
model.addAttribute("id", 100);
model.addAttribute("name", "Jacob");
model.addFlashAttribute("user", user);
return "redirect:/userInfo/{name}";
}
@GetMapping("/userInfo/{name}")
public String userInfo(@RequestParam("id") String id,
@PathVariable("name") String name,
Model model) {
model.addAttribute("id", id);
model.addAttribute("name", name);
return "user";
}
对于一些简单的数据,可以通过路径变量和查询参数进行传递,如果传递对象,可以使用RedirectAttributes,它继承了Model类并且新增了提供设置flash属性的方法。flash属性是通过会话实现的,在重定向执行前,所有的flash属性会复制到会话中,在重定向之后,存在会话中的flash属性会被取出,并转移到模型之中。
flash属性原理.JPG