Java for Web学习笔记(六三):Controller替代Servlet(5)Model和View

来源:互联网 发布:网络上有重名上不去网 编辑:程序博客网 时间:2024/06/05 10:04

View和Model

我们可以这样简单地理解MVC,C是controller,进行具体的处理,处理后得到的结果(数据)放入Model中,将Model传递到view,view具体负责向client呈现。

Spring提供了很多View:

  • InternalResourceView:使用传统的jsp
  • JstlView:使用支持JSTL的jsp,JstView和InternalResourceView将Model的属性转换为request的属性,因此可以jsp中通过EL来获取model中的值。
  • FreeMarkerView:支持 FreeMarker模板yinq
  • VelocityView:支持Apache Velocity模板引擎
  • TilesView:支持Apache Tiles模板引擎
  • MappingJackson2JsonView:输出为json格式
  • MarshallingView:输出为XML格式
  • RedirectView:重定向,在HTTP1.1的303 See Other或者HTTP1.0的302 Found的Location header中给出重定向地址

Spring如何找到view:

  1. 如果controller返回View或者ModelAndView中还有View,则直接使用该view来进行渲染。
  2. 如果controller的方法返回String作为view名字,或者ModelAndView中的View是一个String,则Spring需要将该view的名字解析为真正的view,这个解析过程需要解析器,即ViewResolver,这是Spring FrameworkServlet中配置,及在dispacher中配置。
  3. 如果返回model或者model属性,则是通过请求的url翻译为view 名字,这是通过配置好的RequestToViewNameTranslator实现,再通过ViewResolver找到view。
  4. 如果返回一个response entity,则根据内容协商来找到相应的view。

直接返回View和View名字

本学习的小例子采用代码配置的方式。下面给出重定向到一个jsp文件的例子,这里实际分为两步:

  1. 重定向:将/重定向至/dashboard
    • 重定向采用直接返回RedirectView
  2. 通过jsp渲染:将/dashboard,通过/WEB-INF/jsp/view/home/dashboard.jsp渲染呈现出来。
    • 返回view的名字,并通过配置好的ViewResolver,获得合适的view实例,因为采用jsp文件进行渲染,采用JstlView。

重定向:返回View

@Controllerpublic class HomeController {    @RequestMapping("/")    public View home(){        // 重定向很简单,直接返回一个RedirectView实例,给出具体的URL。        // URL可以是绝对路径,开始是协议说明,例如http://或者https://,或者网络前缀//        // URL也可以是相对路径,则是基于当前的URL的相对路径,如果以/开头,缺省是基于server的URL,即如果返回 new RedirectView("/dashboard"),则会重定向到http://localhost:8080/dashboard中。在绝大多数的情况下,我们需要的是本web app context的URL,采用new RedirectView(String url, boolean contextRelative),第二个参数设置为true,表示基于app context。        return new RedirectView("/dashboard",true);                    }}

这个例子很简单,我们根本无需使用到model,直接给出具体的URL,但是我们在此学习如何使用model,如何在里面放入数据,如何在代码中获取model里面数据的案例。

@RequestMapping("/")public View home(Map<String,Object> model){  // [1]通过方法参数获取model    // [2] 向model放入数据    model.put("dashboardUrl", "dashboard");    // [3] RedirectView如何从model中获取数据    return new RedirectView("/{dashboardUrl}",true);                }

通过jsp渲染:返回view名字

(1) 配置ViewResolver

对于Controller方法返回String作为view名字,需要通过ViewResolver 实例(bean)获得真正的view实例。通过jsp来渲染,使用JstlView,由我们在Servlet context配置中设置ViewResolver获得相应的view。在配置中所有的实例均为Bean,因此加上@Bean的标记,这也是满足Spring自动注入的需要。

@Configuration@ComponentScan(               basePackages = "cn.wei.flowingflying.chapter13.site",               useDefaultFilters = false,               includeFilters = @ComponentScan.Filter(Controller.class))public class ServletContextConfiguration {    // 1)必须将ViewResolver设置为@Bean。从某种意义上配置需要注入实例的是Bean,而其余代码中需要注入实例的是Service    @Bean     public ViewResolver viewResolver(){        // 2)使用InternalResourceViewResolver作为resolver,可以将view的名字转为文件名字。        InternalResourceViewResolver resolver = new InternalResourceViewResolver();                    // 3)根据需求,view是JstlView,即允许JSTL的tag。这句可以不加,不加将采用缺省的AbstractUrlBasedView,由于后缀给出.jsp,则会自动采用JstlView        resolver.setViewClass(JstlView.class);         // 4)view实例和具体的URL相关,可能就是构造函数的参数,下面给出jsp URL组成:/WEB-INF/jsp/view/xxxx.jsp。通过这些设置,resolver可以根据view的名字生成JstlView实例        resolver.setPrefix("/WEB-INF/jsp/view/");         resolver.setSuffix(".jsp");        return resolver;    }}

如果采用xml配置,可以写为:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">    <property name="prefix" value="/WEB-INF/jsp/view"/>    <property name="suffix" value=".jsp"/></bean>

(2) jsp文件

下面是/WEB-INF/jsp/view/home/dashboard.jsp文件

<%--@elvariable id="text" type="java.lang.String"--%><%--@elvariable id="date" type="java.time.Instant"--%><!DOCTYPE html><html><head>    <title>Dashboard</title></head><body>    Text : ${text}<br/>    Date : ${date}</body></html>

(3) Controller返回String作为view的名字,同时提供model

这里我们通过log4j2对方法的输入和输出进行跟踪。

@RequestMapping(value="/dashboard", method = RequestMethod.GET)public String dashboard(Map<String,Object> model){    logger.entry(model);    model.put("text", "This is a model attribute.");    model.put("date",Instant.now());    return logger.traceExit("home/dashboard");}

model的设置在之前已经学习,方法返回为"home/dashboard",通过在ServletContextConfiguration配置的ViewResolver实例(bean)获得了具体的jtslView,我们看看log,可以看到JstlView将Model中数据放入request中,这样可以在jsp中通过EL来获取,然后将请求forward到/WEB-INF/jsp/view/home/dashboard.jsp中。

17:23:13.063 [TRACE] HomeController:36 dashboard() - Enter params({})17:23:13.064 [TRACE] HomeController:39 dashboard() - Exit with(home/dashboard)17:23:13.065 [DEBUG] (Spring) DispatcherServlet - Rendering view [org.springframework.web.servlet.view.JstlView: name 'home/dashboard'; URL [/WEB-INF/jsp/view/home/dashboard.jsp]] in DispatcherServlet with name 'springDispatcher'17:23:13.065 [DEBUG] (Spring) JstlView - Added model object 'text' of type [java.lang.String] to request in view with name 'home/dashboard'17:23:13.065 [DEBUG] (Spring) JstlView - Added model object 'date' of type [java.time.Instant] to request in view with name 'home/dashboard'17:23:13.065 [DEBUG] (Spring) JstlView - Added model object 'org.springframework.validation.BindingResult.date' of type [org.springframework.validation.BeanPropertyBindingResult] to request in view with name 'home/dashboard'17:23:13.066 [DEBUG] (Spring) JstlView - Forwarding to resource [/WEB-INF/jsp/view/home/dashboard.jsp] in InternalResourceView 'home/dashboard'17:23:13.066 [DEBUG] (Spring) DispatcherServlet - Successfully completed request

根据请求URL获得view

RequestToViewNameTranslator

如果controller返回的是一个model,或者一个model的属性,见下面例子,则根据请求的URL来获得view。同样的,我们需要一个resolver来完成将请求转换为view名字,然后通过之前的ViewResovler来从view名字获得view实例。我们在servletContext配置中加上:

@Configuration@ComponentScan(               basePackages = "cn.wei.flowingflying.chapter13.site",               useDefaultFilters = false,               includeFilters = @ComponentScan.Filter(Controller.class))public class ServletContextConfiguration {     @Bean    public ViewResolver viewResolver(){  ... ...  }    // 1) 和ViewResovler一样,RequestToViewNameTranslator作为@Bean。    @Bean    public RequestToViewNameTranslator viewNameTranslator(){        // 2) 使用DefaultRequestToViewNameTranslator,将请求的url去掉web app context URL,以及最后的文件后缀,例如http://localhost:8080/chapter13/foo,将给出view name=foo,例如http://localhost:8080/chapter13/home/foo.html,将给出home/foo        return new DefaultRequestToViewNameTranslator();    }}

使用例子

下面是contrller的方法:

// 根据请求,则view的名字为user/home,则对应的jsp文件名字为/WEB-INF/jsp/view/user/home.jsp,相关jsp文件,从略//@ModelAttribute 相当于model.put("currentUser",user),如果不指明,则model的属性名字通类名(但首字母小写),即user。@RequestMapping(value = "/user/home", method = RequestMethod.GET)@ModelAttribute("currentUser") public User userHome(){    User user = new User();    user.setUserId(1234987234L);    user.setUsername("adam");    user.setName("Adam Johnson");    return logger.traceExit(user);}

返回Response entity,根据内容协商获得view

内容协商

对于request entity,Spring根据Content-Type选择合适的转换器,对于Resposne entity,Spring根据以下规则判断返回的格式,然后根据格式类型决定使用哪个转换器,例如转换为xml,转换为json。

下面规则由先后顺序

  1. 文件的扩展名字,例如http://localhost:8080/chapter13/user/12.xml,则说明格式为xml,
  2. 根据请求的参数,缺省为format,也可以配置为其他,例如http://localhost:8080/chapter13/user/12?format=json,格式为json
  3. 根据请求中Accept消息头来决定,下图是一个HTTP请求头

本例将提供xml和json的转换。需要在pom.xml中含有相关的jar包:

<!-- 下面是json解析相关的 --><dependency>    <groupId>com.fasterxml.jackson.core</groupId>    <artifactId>jackson-annotations</artifactId>    <version>${jackson.version}</version>    <scope>compile</scope></dependency><dependency>    <groupId>com.fasterxml.jackson.core</groupId>    <artifactId>jackson-databind</artifactId>    <version>${jackson.version}</version>    <scope>compile</scope></dependency><dependency>    <groupId>com.fasterxml.jackson.datatype</groupId>    <artifactId>jackson-datatype-jsr310</artifactId>    <version>${jackson.version}</version>    <scope>compile</scope> </dependency><!-- 下面是xml解析相关的 --><dependency>    <groupId>org.springframework</groupId>    <artifactId>spring-oxm</artifactId>    <version>${spring.framework.version}</version>    <scope>compile</scope></dependency>

配置转换器

我们需要在servletContext中配置相关的转换器,转换器有两个方向,将输入转换到java对象,已经将输出按格式转换,是双向的,相关代码如下:

// 1)添加消息内容的转换器,需要扩展WebMvcConfigurerAdapter,并在此配置允许@Configuration@EnableWebMvc @ComponentScan(               basePackages = "cn.wei.flowingflying.chapter13.site",               useDefaultFilters = false,               includeFilters = @ComponentScan.Filter(Controller.class))public class ServletContextConfiguration extends WebMvcConfigurerAdapter {    // 2)提供具体用于实现转换的实例,采用自动注入方式。得到实例的方法可以配置在servletContext中,即本类中,也可以考虑到可其他上下文使用,设置在root context中。例子采用后者            // 2.1) 注入用于xml封装和解析的实例    @Inject Marshaller marshaller;    @Inject Unmarshaller unmarshaller;    // 2.2)注入用于jason解析的实例    @Inject ObjectMapper objectMapper;    // 3)重新设置消息内容的转换器,需要加上@EnableWebMvc,这个override才能有效。例子中使用configureMessageConverters(),将对转换器完全进行重新设置,我们也可以使用extendMessageConverters()添加我们的转换器,这个在后面讨论    @Override    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {        //【注意】添加顺序是重要的的,因为可能同时适配若干个转换器。        // 3.1)下面几个是常规的转换器        converters.add(new ByteArrayHttpMessageConverter());        converters.add(new StringHttpMessageConverter());        converters.add(new FormHttpMessageConverter());        converters.add(new SourceHttpMessageConverter<>());        // 3.2)添加xml转换器,设置支持application/xml和text/xml                 MarshallingHttpMessageConverter xmlConverter = new MarshallingHttpMessageConverter();        xmlConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "xml"),                                                          new MediaType("text", "xml")));        xmlConverter.setMarshaller(this.marshaller);        xmlConverter.setUnmarshaller(this.unmarshaller);        converters.add(xmlConverter);        // 3.3)添加json转换器,设置支持application/json和text/json                   MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();        jsonConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "json"),                                                           new MediaType("text","json")));        jsonConverter.setObjectMapper(this.objectMapper);        converters.add(jsonConverter);        logger.debug(converters); //看看有什么log    }    // 4)定制化设置内容协商配置,如果不定制,均采用缺省值    @Override    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {        //表示允许路径扩展后缀(缺省true),如.xml,并允许通过Java Activation Framework来将扩展解析到特定MediaType中(缺省false)        configurer.favorPathExtension(true).useJaf(true)                   .favorParameter(true).parameterName("mediaType") //允许参数设置format(缺省flase),不采用缺省值,设置为mediaType                  .ignoreAcceptHeader(false)                       //允许通过Accept头来协商(缺省flase)                  .defaultContentType(MediaType.APPLICATION_XML)   //设置缺省为application/xml                  .mediaType("xml", MediaType.APPLICATION_XML)     //对于路径扩展或者参数为xml,对应到application/xml                  .mediaType("json", MediaType.APPLICATION_JSON);  //对于路径扩展或者参数为json,对应到application/json     }     ... ...}

log为:

[org.springframework.http.converter.ByteArrayHttpMessageConverter@64ad8e, org.springframework.http.converter.StringHttpMessageConverter@b0ad74, org.springframework.http.converter.FormHttpMessageConverter@118165f, org.springframework.http.converter.xml.SourceHttpMessageConverter@1a0bdff, org.springframework.http.converter.xml.MarshallingHttpMessageConverter@a97f18, org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@1b3bef4]

本例将xml和json解析器的实例获取防止root上下文,相关代码如下:

@Configurable@ComponentScan(               basePackages = "cn.wei.flowingflying.chapter13.site",               excludeFilters = @ComponentScan.Filter(Controller.class))public class RootContextConfiguration {    // 为自动注入提供xml解析和封装的实例,Jaxb2Marshaller同时Marshaller和UnMarshaller的继承。再次说明,这个可以根据需要加载root上下文,或者servlet上下文中    @Bean //所有的实例是Bean,因此需要加上@Bean     public Jaxb2Marshaller jaxb2Marshaller(){        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();        //which package to scan for XML-annotated entities        marshaller.setPackagesToScan(new String[] { "cn.wei.flowingflying.chapter13.site" });        return marshaller;    }    // 为自动注入提供json解析和封装的实例     @Bean    public ObjectMapper objectMapper(){        ObjectMapper mapper = new ObjectMapper();        //寻找和注册所有的扩展模块,例如JSR 310(Java8 Date and Time)支持模块        mapper.findAndRegisterModules();         //date序列化时不作为timestamp的long,而是作为ISO 8601的string        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);         //date反序列化是没有时区时,认为是UTC,而不适配到当前时区        mapper.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false);        return mapper;    }}

我们注意到xml是自动扫描xml标记的,在User类,我们提供相关的标记:

@XmlRootElementpublic class User {    private long userId;    private String username;    private String name;    ... ...}

例子

@RequestMapping(value="/user/{userId}", method = RequestMethod.GET)@ResponseBody // 响应消息体,也即Response entity,将触发内容协商public User getUser(@PathVariable("userId") long userId){    User user = new User();    user.setUserId(userId);    user.setUsername("john");    user.setName("John Smith");    return user;}

我们可以测试一下http://localhost:8080/chapter13/user/12将给出xml格式,因为缺省配置为xml,http://localhost:8080/chapter13/user/12.json 以及http://localhost:8080/chapter13/user/12&mediaType=json ,都将给出json格式。

强大的Spring生态

我们修改一下ServletContextConfig文件,注释掉configureMessageConverters(),改用extendMessageConverters(),同时增加log,看看已经具有哪些转换器

public class ServletContextConfiguration2 extends WebMvcConfigurerAdapter {    private static final Logger logger = LogManager.getLogger();    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {        logger.info("Converters : " + converters);        //这里加入我们需要新增的转换器。    }    ... ...}

Log如下:
15:28:31.166 [DEBUG] - Converters : [org.springframework.http.converter.ByteArrayHttpMessageConverter@1d91848, org.springframework.http.converter.StringHttpMessageConverter@3678ca, org.springframework.http.converter.ResourceHttpMessageConverter@461d29, org.springframework.http.converter.xml.SourceHttpMessageConverter@c05b0a, org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter@1020682, org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter@ffdbc0, org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@5425a3]

我们注意到已经含有Jaxb2RootElementHttpMessageConverter和MappingJackson2HttpMessageConverter,实际上只要有相关的jar包在lib,就能够提供转换器,包括xml,包括json。因此,我们无需添加转换器,无需注入,只需配置内容协商,也能实现输出xml或者json格式。这就是spring的强大,有很多第三方jar包围着它转,生态够大。

我们也同时注意到,xml使用的转换器是Jaxb2RootElementHttpMessageConverter,而不是之前配置的MarshallingHttpMessageConverter,我们可以不导入spring-oxm,同样也能支持xml转换。


相关链接: 我的Professional Java for Web Applications相关文章
阅读全文
0 0
原创粉丝点击