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:
- 如果controller返回View或者ModelAndView中还有View,则直接使用该view来进行渲染。
- 如果controller的方法返回String作为view名字,或者ModelAndView中的View是一个String,则Spring需要将该view的名字解析为真正的view,这个解析过程需要解析器,即ViewResolver,这是Spring FrameworkServlet中配置,及在dispacher中配置。
- 如果返回model或者model属性,则是通过请求的url翻译为view 名字,这是通过配置好的RequestToViewNameTranslator实现,再通过ViewResolver找到view。
- 如果返回一个response entity,则根据内容协商来找到相应的view。
直接返回View和View名字
本学习的小例子采用代码配置的方式。下面给出重定向到一个jsp文件的例子,这里实际分为两步:
- 重定向:将/重定向至/dashboard
- 重定向采用直接返回RedirectView
- 通过jsp渲染:将/dashboard,通过/WEB-INF/jsp/view/home/dashboard.jsp渲染呈现出来。
- 返回view的名字,并通过配置好的ViewResolver,获得合适的view实例,因为采用jsp文件进行渲染,采用JstlView。
重定向:返回View
@Controller
public 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。
下面规则由先后顺序
- 文件的扩展名字,例如http://localhost:8080/chapter13/user/12.xml,则说明格式为xml,
- 根据请求的参数,缺省为format,也可以配置为其他,例如http://localhost:8080/chapter13/user/12?format=json,格式为json
- 根据请求中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类,我们提供相关的标记:
@XmlRootElement
public 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转换。