《Spring实战》笔记(四):Spring MVC

2018-04-19  本文已影响0人  JacobY

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(),在AbstractAnnotationConfigDispatcherServletInitializerDispatcherServlet注册到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允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:

    @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的定义,它会创建DispatcherServletContextLoaderListener。如果我们想往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形式的数据

  1. 配置multipart解析器
    解析器有两个选择:
    @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"));
    }
  1. 接收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有如下方式可以将异常转换为相应:

  1. 在默认情况下,Spring会将自身的一些异常自动转换为合适的状态码。


    一些Spring异常的默认映射.JPG
  2. 可以使用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。在控制器通知类可以定义一个或多个以下的方法:

@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返回。

  1. 标注在方法上,被标注的方法将会先于控制器方法执行:
    @ModelAttribute("key")
    public String initModel() {
        return "value";
    }

这样子会在controller方法返回页面的Model里面添加一个name为key而value为value的属性。

    @ModelAttribute()
    public voidinitModel(Model model) {
        model.addAttibute("key", "value");
    }

无返回值的的方法可以直接用model来设置属性。

  1. 标注在方法的参数:
    @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"的格式进行。进行重定向的时候,上次请求的参数将不能继续传递下去。要实现跨重定向传递数据,主要有两种方法:

    @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
上一篇 下一篇

猜你喜欢

热点阅读