Spring学习(五)--构建Spring Web应用程序
一.Spring MVC起步
看过猫和老鼠的小伙伴都可以想象Tom猫所制作的捕鼠器:它的目标 是发送一个小钢球,让它经过一系列稀奇古怪的装置,最后触发捕鼠 器。小钢球穿过各种复杂的配件,从一个斜坡上滚下来,被跷跷板弹起,绕过一个微型摩天轮,然后被橡胶靴从桶中踢出去。经过这些后,小钢球会对那只可怜又无辜的老鼠进行捕获。而Spring MVC框架与捕鼠器有些类似。Spring将请求在调度Servlet、处理器映射(handler mapping)、控制器以及视图解析器(view resolver)之间移动.
--跟踪Spring MVC的请求
每当用户在Web浏览器中点击链接或提交表单的时候,请求就开始工作了。对请求的工作描述就像是快递投送员。与快递员一样,请求会将信息从一个地方带到另一个地方。但是请求是一个十分繁忙的家伙。从离开浏览器开始到获取响应返回,它会经历好多站,在每站都会留下一些信息同时也会带上其他信息:
--在请求离开浏览器时①,会带有用户所请求内容的信息,至少会包含请求的URL.但是还可能带有其他的信息,例如用户提交的表单信息.请求路程的第一站是Spring的DispatcherServlet.SpringMVC所有的请求都回通过一个前端控制器(front controller)Servlet.前端控制器是常用的Web应用程序模式,在这里一个单实例的的Servlet将请求委托给应用程序的其他组件来执行实际的处理.在Spring MVC之中,DispatcherServlet就是前端控制器.DispatcherServlet的任务是将请求发送给Spring MVC控制器(controller).控制器是一个用于处理请求的Spring组件.在典型的应用程序中中可能会有多个控制器,DispatcherServlet需要知道应该将请求发送给哪个控制器。所以DispatcherServlet以会查询一个或多个处理器映射(handler mapping)②来确定请求的下一站在哪里。
处理器映射会根据请求所携带的URL信息来进行决策。一旦选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器③。到了控制器,请求会卸下其负载(用户提交的信 息)并耐心等待控制器处理这些信息。(实际上,设计良好的控制器 本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个 服务对象进行处理。)控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回 给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅 仅给用户返回原始的信息是不够的——这些信息需要以用户友好的方 式进行格式化,一般会是HTML。所以,信息需要发送给一个视图 (view),通常会是JSP。控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染 输出的视图名。它接下来会将请求连同模型和视图名发送回 DispatcherServlet ④。
这样,控制器就不会与特定的视图相耦合,传递给 DispatcherServlet的视图名并不直接表示某个特定的JSP。实际 上,它甚至并不能确定视图就是JSP。相反,它仅仅传递了一个逻辑 名称,这个名字将会用来查找产生结果的真正视 图。DispatcherServlet将会使用视图解析器(view resolver)⑤ 来将逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是 JSP。既然DispatcherServlet已经知道由哪个视图渲染结果,那请求的任务基本上也就完成了。它的最后一站是视图的实现(可能是 JSP)⑥ ,在这里它交付模型数据。请求的任务就完成了。视图将使用 模型数据渲染输出,这个输出会通过响应对象传递给客户端(不会像听上去那样硬编码) ⑦。
--请求要经过很多的步骤,最终才能形成返回给客户端的响 应。大多数的步骤都是在Spring框架内部完成的.我们来进行Spring MVC的基础搭配;
--配置DispatcherServlet
DispatcherServlet是Spring MVC的核心。在这里请求会第一次 接触到框架,它要负责将请求路由到其他的组件之中。 按照传统的方式,像DispatcherServlet这样的Servlet会配置在 web.xml文件中,这个文件会放到应用的WAR包里面。这是配 置DispatcherServlet的方法之一,但是,借助于Servlet 3规范和 Spring 3.1的功能增强,这种方式已经不是唯一的方案了,我们可以使用Java将DispatcherServlet配置在Servlet容器中,而不会再使用web.xml文件。如下的程序清单展示了所需的Java类:
1 package config;
2
3 import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
4
5 /**
6 * @author : S K Y
7 * @version :0.0.1
8 */
9 public class SpringWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
10 @Override
11 protected Class<?>[] getRootConfigClasses() {
12 return new Class<?>[]{RootConfig.class};
13 }
14
15 @Override
16 protected Class<?>[] getServletConfigClasses() { //指定配置类
17 return new Class<?>[]{WebConfig.class};
18 }
19
20 @Override
21 protected String[] getServletMappings() {
22 return new String[]{"/"}; //将DispatcherServlet映射到"/"
23 }
24 }
--我们可能只需要知道扩展AbstractAnnotationConfigDispatcherServletInitializer的任意类都会自动地配置Dispatcher-Servlet和Spring应用上下文,Spring的应用上下文会位于应用程序的Servlet上下文之中。
--AbstractAnnotationConfigDispatcherServletInitializer剖析
在Servlet 3.0环境 中,容器会在类路径中查找实现 javax.servlet.ServletContainerInitializer接口的类, 如果能发现的话,就会用它来配置Servlet容器。 Spring提供了这个接口的实现,名 为SpringServletContainerInitializer,这个类反过来又会 查找实现WebApplicationInitializer的类并将配置的任务交给 它们来完成。Spring 3.2引入了一个便利的 WebApplicationInitializer基础实现,也就 是AbstractAnnotationConfigDispatcherServletInitializer 因为我们的SpringWebAppInitializer扩展了 AbstractAnnotationConfig DispatcherServletInitializer(同时也就实现了 WebApplicationInitializer),因此当部署到Servlet 3.0容器中的时候,容器会自动发现它,并用它来配置Servlet上下文。 尽管它的名字很长,但 是AbstractAnnotationConfigDispatcherServlet- Initializer使用起来很简便。
--我们重写了AbstractAnnotationConfigDispatcherServletInitializer中的三个方法:
1.getServletMappings():它会将一个或多个路径映 射到DispatcherServlet上。在本例中,它映射的是“/”,这表示 它会是应用的默认Servlet。它会处理进入应用的所有请求。
--为了理解其他的两个方法,我们首先要理解DispatcherServlet 和一个Servlet监听器(也就是ContextLoaderListener)的关系:
当DispatcherServlet启动的时候,它会创建Spring应用上下文,并加载配置文件或配置类中所声明的bean。在SpringWebAppInitializer.getServletConfigClasses()方法中,我们要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类(使用Java配置)中的bean。但是在Spring Web应用中,通常还会有另外一个应用上下文。另外的这个应用上下文是由ContextLoaderListener创建的。我们希望DispatcherServlet加载包含Web组件的bean,如控制 器、视图解析器以及处理器映射,而ContextLoaderListener要 加载应用中的其他bean。这些bean通常是驱动应用后端的中间层和数 据层组件。
实际上,AbstractAnnotationConfigDispatcherServletInitializer 会同时创建DispatcherServlet和 ContextLoaderListener。GetServlet-ConfigClasses() 方法返回的带有@Configuration注解的类将会用来定 义DispatcherServlet应用上下文中的 bean。getRootConfigClasses()方法返回的带 有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文中的bean。
在本例中,根配置定义在RootConfig中,DispatcherServlet 的配置声明在WebConfig中(目前还没有进行实际定义)。如果按照这种方式配置DispatcherServlet,而不是使用web.xml 的话,那唯一问题在于它只能部署到支持Servlet 3.0的服务器中才能 正常工作,如Tomcat 7或更高版本。Servlet 3.0规范在2009年12月份就 发布了,因此很有可能你会将应用部署到支持Servlet 3.0的Servlet容 器之中。
--启用Spring MVC
我们有多种方式来配置DispatcherServlet,与之类似,启用 Spring MVC组件的方法也不仅一种。以前,Spring是使用XML进行配 置的,你可以使用<mvc:annotation-driven>启用注解驱动的 Spring MVC。当然我们也可以基于JavaConfig进行配置:
1 package config;
2
3 import org.springframework.context.annotation.Configuration;
4 import org.springframework.web.servlet.config.annotation.EnableWebMvc;
5
6 /**
7 * @author : S K Y
8 * @version :0.0.1
9 */
10 @Configuration
11 @EnableWebMvc
12 public class WebConfig {
13 }
--这可以运行起来,它的确能够启用Spring MVC,但还有不少问题要解决:
1.没有配置视图解析器。如果这样的话,Spring默认会使用BeanNameView-Resolver,这个视图解析器会查找ID与视图名称匹配的bean,并且查找的bean要实现View接口,它以这样 的方式来解析视图。
2.没有启用组件扫描。这样的结果就是,Spring只能找到显式声明 在配置类中的控制器。
3.这样配置的话,DispatcherServlet会映射为应用的默认 Servlet,所以它会处理所有的请求,包括对静态资源的请求,如图片和样式表(在大多数情况下,这可能并不是你想要的效果)。
--我们需要在WebConfig这个最小的Spring MVC配置上再加一 些内容,从而让它变得真正有用:
1 package config;
2
3 import org.springframework.context.annotation.Bean;
4 import org.springframework.context.annotation.ComponentScan;
5 import org.springframework.context.annotation.Configuration;
6 import org.springframework.web.servlet.ViewResolver;
7 import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
8 import org.springframework.web.servlet.config.annotation.EnableWebMvc;
9 import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
10 import org.springframework.web.servlet.view.InternalResourceViewResolver;
11
12 /**
13 * @author : S K Y
14 * @version :0.0.1
15 */
16 @Configuration
17 @EnableWebMvc //启用Spring MVC
18 @ComponentScan(basePackages = {"web"}) //启用组件扫描
19 public class WebConfig extends WebMvcConfigurerAdapter {
20 @Bean
21 public ViewResolver viewResolver() {
22 //配置JSP视图解析器
23 InternalResourceViewResolver resolver = new InternalResourceViewResolver();
24 resolver.setPrefix("/WEB-INF/views/"); //设置jsp所在的目录
25 resolver.setSuffix(".jsp"); //设置后缀名称
26 resolver.setExposeContextBeansAsAttributes(true);
27 return resolver;
28 }
29
30 @Override
31 public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
32 configurer.enable(); //配置静态资源的处理
33 }
34 }
--在WebConfig中第一件需要注意的事情是WebConfig现在添加了 @ComponentScan注解,因此将会扫描web包来查找组件。 我们所编写的控制器将会带有@Controller注 解,这会使其成为组件扫描时的候选bean。因此,我们不需要在配置类中显式声明任何的控制器。我们重写了configureDefaultServletHandling()方法使得DispatcherServlet将对静态资源的请求转发到Servlet容器中默认的Servlet上,而不是使用DispatcherServlet本身来处理此类请求。
--配置RootConfig
1 package com.sky.config;
2
3 import org.springframework.context.annotation.ComponentScan;
4 import org.springframework.context.annotation.Configuration;
5 import org.springframework.context.annotation.FilterType;
6 import org.springframework.web.servlet.config.annotation.EnableWebMvc;
7
8 /**
9 * @author : S K Y
10 * @version :0.0.1
11 */
12 @Configuration
13 //在进行Web编程的时候还是得规范化包名,因此web包也在com.sky下
14 @ComponentScan(basePackages = {"com.sky"},
15 excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)})
16 public class RootConfig {
17 }
--唯一需要注意的是RootConfig使用了@ComponentScan注解。这样的话,我们就有很多机会用非Web的组件来充实完善 RootConfig。
--SpringWeb应用简介
为了实现在线社交的功能,我们将要构建一个简单的微博 (microblogging)应用。在很多方面,我们所构建的应用与最早的微 博应用Twitter很类似。在这个过程中,我们会添加一些小的变化。当 然,我们要使用Spring技术来构建这个应用。 因为从Twitter借鉴了灵感并且通过Spring来进行实现,所以它就有了 一个名字:Spitter。Spittr应用有两个基本的领域概念:Spitter(应用的用户)和 Spittle(用户发布的简短状态更新)。
二.编写基本的控制器
在Spring MVC中,控制器只是方法上添加了@RequestMapping注解 的类,这个注解声明了它们所要处理的请求。开始的时候,我们尽可能简单,假设控制器类要处理对“/”的请求, 并渲染应用的首页:
1 package com.sky.web;
2
3 import org.springframework.stereotype.Controller;
4 import org.springframework.web.bind.annotation.RequestMapping;
5 import org.springframework.web.bind.annotation.RequestMethod;
6
7 /**
8 * @author : S K Y
9 * @version :0.0.1
10 */
11 @Controller //声明为一个控制器
12 public class WebController {
13 @RequestMapping(value = "/", method = RequestMethod.GET)
14 public String home() {
15 return "home";
16 }
17 }
--@Controller是一个构造型(stereotype)的注解,它基于 @Component注解。在这里,它的目的就是辅助实现组件扫描。因 为HomeController带有@Controller注解,因此组件扫描器会自 动找到HomeController,并将其声明为Spring应用上下文中的一个 bean。其实,你也可以让HomeController带有@Component注解,它所 实现的效果是一样的,但是在表意性上可能会差一些,无法确定 HomeController是什么组件类型。
--HomeController唯一的一个方法,也就是home()方法,带 有@RequestMapping注解。它的value属性指定了这个方法所要处 理的请求路径,method属性细化了它所处理的HTTP方法。在本例 中,当收到对“/”的HTTP GET请求时,就会调用home()方法。home()方法其实并没有做太多的事情:它返回了一 个String类型的“home”。这个String将会被Spring MVC解读为要 渲染的视图名称。DispatcherServlet会要求视图解析器将这个 逻辑名称解析为实际的视图。
--定义一个简单的JSP
1 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
2 <html>
3 <head>
4 <title>微博</title>
5 </head>
6 <body>
7 <h1>欢迎来到微博</h1>
8 </body>
9 </html>
--这样我们启动Tomcat服务器就可以正常访问到我们的主页了,当然这里有一个坑,在idea中默认的打包输出的名称并不是项目名称,因此我们需要修改,在File -->Project Sturcture:
将上方的name修改为我们的项目名称,当然为了确保顺利,可以在idea的Tomcat中查看一下是否正确,在这里附上修改Tomcat中文乱码问题的方法:打开tomcat的安装路径找到conf文件夹,打开server.xml配置文件
--添加默认字符集为UTF-8,如果说idea中控制台打印的中文信息的乱码也让你不舒服的话,可以打开idea安装的bean路径,找到idea.exe.vmoptions及idea64.exe.vmoptions,打开之后在末尾加上-Dfile.encoding=UTF-8,随后重启idea,我们的乱码问题就可以得到有效的解决
--我们可以看到最终顺利的打开了我们的网站
--当然我们也可以通过自动化测试来测试我们的home()方法
1 package com.sky.test;
2
3 import com.sky.web.WebController;
4 import org.junit.Assert;
5 import org.junit.Test;
6
7 /**
8 * @author : S K Y
9 * @version :0.0.1
10 */
11 public class WebControllerTest {
12 @Test
13 public void testHomePage(){
14 WebController webController = new WebController();
15 Assert.assertEquals("home",webController.home());
16 }
17 }
--但它只测试了home()方法中会发生什 么。在测试中会直接调用home()方法,并断言返回包含“home”值的 String。它完全没有站在Spring MVC控制器的视角进行测试。这个 测试没有断言当接收到针对“/”的GET请求时会调用home()方法。因 为它返回的值就是“home”,所以也没有真正判断home是视图的名称。从Spring 3.2 开始,我们可以按照控制器的方式来测试SpringMVC中的控制器了,Spring现在包含了一种mock SpringMVC并针对控制器执行HTTP请求的机制.
1 package com.sky.test;
2
3 import com.sky.web.WebController;
4 import org.junit.Assert;
5 import org.junit.Test;
6 import org.springframework.test.web.servlet.MockMvc;
7
8 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
9
10 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
11 import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
12
13 /**
14 * @author : S K Y
15 * @version :0.0.1
16 */
17 public class WebControllerTest {
18 @Test
19 public void testHomePage() throws Exception {
20 WebController webController = new WebController();
21 Assert.assertEquals("home", webController.home());
22 //搭建MockMVC
23 MockMvc mockMvc = standaloneSetup(webController).build();
24
25 mockMvc.perform(get("/")) //对"/"执行GET请求
26 .andExpect(view().name("home")); //预期得到home视图
27 }
28 }
--我们可以借用MockMvcBuilders提供的静态方法standaloneSetup()来加载我们的controller,再使用bulid()方法来获取MockMvc的实例.在mock.perform()中我们传入的参数为MockMvcRequestBuilders中的静态方法get(String url),用于指定我们进行访问的get请求地址,该方法会返回一个ResultActions实例,我们可以使用andExpect()方法指定参数为MockMvcResultMatchers.view().name(String name),来传入我们预期得到的视图的名称.
--定义类级别的请求处理
我们可以拆分@RequestMapping,并将其路径映射部分到类级别上:
1 package com.sky.web;
2
3 import org.springframework.stereotype.Controller;
4 import org.springframework.web.bind.annotation.RequestMapping;
5 import org.springframework.web.bind.annotation.RequestMethod;
6
7 /**
8 * @author : S K Y
9 * @version :0.0.1
10 */
11 @Controller //声明为一个控制器
12 @RequestMapping("/") //将控制器映射到"/"
13 public class WebController {
14 @RequestMapping(method = RequestMethod.GET) //处理GET请求
15 public String home() {
16 return "home";
17 }
18 }
--此次我们在WebController 中,将路径转义到了类级别的@RequestMapping上,而HTTP方法依然映射在方法级别上.当控制器在类级别上添加@RequestMapping注解时,这个注解会应用到控制器的所有处理方法上.处理器方法上的@RequestMapping注解会对类级别上的@RequestMapping的声明进行补充.就WebController而言,这里只有一个控制器方法.与类级别的@RequestMapping合并之后,这个方法的@RequestMapping表明home()将会处理对"/"路径的GET请求.
--我们可以观察@RequestMapping的构成:
1 package org.springframework.web.bind.annotation;
2
3 import java.lang.annotation.Documented;
4 import java.lang.annotation.ElementType;
5 import java.lang.annotation.Retention;
6 import java.lang.annotation.RetentionPolicy;
7 import java.lang.annotation.Target;
8 import java.util.concurrent.Callable;
9
10 import org.springframework.core.annotation.AliasFor;
11
12
13 @Target({ElementType.METHOD, ElementType.TYPE})
14 @Retention(RetentionPolicy.RUNTIME)
15 @Documented
16 @Mapping
17 public @interface RequestMapping {
18
19 String name() default "";
20
21 @AliasFor("path")
22 String[] value() default {};
23
24 @AliasFor("value")
25 String[] path() default {};
26
27 RequestMethod[] method() default {};
28
29 String[] params() default {};
30
31 String[] headers() default {};
32
33 String[] consumes() default {};
34
35 String[] produces() default {};
36
37 }
--我们可以发现其可以接受的value值并不只是一个String参数,而是一个String类型的数组,这表名了我们可以对一个@RequestMapping设置多个请求映射
1 package com.sky.web;
2
3 import org.springframework.stereotype.Controller;
4 import org.springframework.web.bind.annotation.RequestMapping;
5 import org.springframework.web.bind.annotation.RequestMethod;
6
7 /**
8 * @author : S K Y
9 * @version :0.0.1
10 */
11 @Controller //声明为一个控制器
12 @RequestMapping(value = {"/", "homepage"}) //将控制器映射到"/"
13 public class WebController {
14 @RequestMapping(method = RequestMethod.GET) //处理GET请求
15 public String home() {
16 return "home";
17 }
18 }
--此时我们将类级别的@RequestMapping映射到了"/"及"homepage"上,我们尝试启用服务器访问homepage:
--可以发现我们再次成功访问到了我们的home.jsp.但是我们需要知道的是,目前所实现的WebController只不过是一个最简单的示例而已,真正的控制器@COntroller是不会那么简单的,在Spring应用中,我们需要有一个页面展现最近提交的Spittle列表,因此我们需要使用新的方法来处理这个页面.
--我们需要定义数据库访问的Repository.为了实现解耦以及避免陷入数据库访问的细节之中,我们可以将Repository定义为一个接口,并在稍后实现它.此时,我们只需要一个能够获取Spittle列表的Repository:
1 package com.sky.data;
2
3 import com.sky.spittle.Spittle;
4
5 import java.util.List;
6
7 /**
8 * @author : S K Y
9 * @version :0.0.1
10 */
11 public interface SpittleRepository {
12 /**
13 * 获取提交的Spittle列表
14 *
15 * @param max 所返回的Spittle属性中,Spittle ID属性的最大值
16 * @param count 要返回的Spittle对象的数量
17 * @return 返回当前查询获得的Spittle列表
18 */
19 List<Spittle> findSpittles(long max, int count);
20 }
1 package com.sky.spittle;
2
3 import org.apache.commons.lang3.builder.EqualsBuilder;
4 import org.apache.commons.lang3.builder.HashCodeBuilder;
5
6 import java.util.Date;
7 import java.util.Objects;
8
9 /**
10 * @author : S K Y
11 * @version :0.0.1
12 */
13 public class Spittle {
14 private long id; //ID属性
15 private String message; //当前提交的消息内容
16 private Date time; //当前提交的时间
17 private double latitude; //维度
18 private double longitude; //精度
19
20 public Spittle(String message, Date time) {
21 this.message = message;
22 this.time = time;
23 }
24
25 public Spittle(String message, Date time, double latitude, double longitude) {
26 this.message = message;
27 this.time = time;
28 this.latitude = latitude;
29 this.longitude = longitude;
30 }
31
32 public long getId() {
33 return id;
34 }
35
36 public String getMessage() {
37 return message;
38 }
39
40 public Date getTime() {
41 return time;
42 }
43
44 public double getLatitude() {
45 return latitude;
46 }
47
48 public double getLongitude() {
49 return longitude;
50 }
51
52 @Override
53 public boolean equals(Object o) {
54 return EqualsBuilder.reflectionEquals(this, "id", "time");
55 }
56
57 @Override
58 public int hashCode() {
59 return HashCodeBuilder.reflectionHashCode(this, "id", "time");
60 }
61 }
--就大部分内容来看,Spittle就是一个基本的POJO数据对象——没 有什么复杂的。唯一要注意的是,我们使用Apache Common Lang包来 实现equals()和hashCode()方法。这些方法除了常规的作用以 外,当我们为控制器的处理器方法编写测试时,它们也是有用的。
-- 测试SpittleController处理针对“/spittles”的GET请求
1 @Test
2 public void shouldShowRecentSpittles() throws Exception {
3 List<Spittle> expectedSpittles = createSpittleList(20);
4 //此处需要使用到mockito相关的jar包
5 SpittleRepository mockRepository = mock(SpittleRepository.class);
6 when(mockRepository.findSpittles(Long.MAX_VALUE, 20)).thenReturn(expectedSpittles);
7 SpittleController controller = new SpittleController(mockRepository);
8 MockMvc mockMvc = standaloneSetup(controller)
9 .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
10 .build();
11 mockMvc.perform(get("/spittles")) //对spittles发起GET请求
12 .andExpect(view().name("spittles"))
13 .andExpect(model().attribute("spittleList" //断言预期的值
14 , IsCollectionContaining.hasItem(expectedSpittles.toArray())));
15
16
17 }
18
19 /**
20 * 模拟创建当前返回的Spittle列表
21 *
22 * @param count 所需要创建的列表中的数据量
23 * @return 当前模拟生成的列表
24 */
25 private List<Spittle> createSpittleList(int count) {
26 List<Spittle> spittles = new ArrayList<>();
27 for (int i = 0; i < count; i++) {
28 spittles.add(new Spittle("Spittle" + i, new Date()));
29 }
30 return spittles;
31 }
1 package com.sky.web;
2
3 import com.sky.data.SpittleRepository;
4 import org.springframework.beans.factory.annotation.Autowired;
5 import org.springframework.stereotype.Controller;
6 import org.springframework.ui.Model;
7 import org.springframework.web.bind.annotation.RequestMapping;
8 import org.springframework.web.bind.annotation.RequestMethod;
9
10 /**
11 * @author : S K Y
12 * @version :0.0.1
13 */
14 @Controller
15 @RequestMapping("/spittles")
16 public class SpittleController {
17 private SpittleRepository spittleRepository;
18
19 @Autowired //注入SpittleRepository
20 public SpittleController(SpittleRepository spittleRepository) {
21 this.spittleRepository = spittleRepository;
22 }
23
24 @RequestMapping(method = RequestMethod.GET)
25 public String spittles(Model model) {
26 model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20)); //将Spittle添加到模型中
27 return "spittles"; //返回视图名
28 }
29
30 }
--这个测试首先会创建SpittleRepository接口的mock实现,这个 实现会从它的findSpittles()方法中返回20个Spittle对象。然 后,它将这个Repository注入到一个新的SpittleController 实例中,然后创建MockMvc并使用这个控制器。 需要注意的是,与HomeController不同,这个测试在MockMvc构 造器上调用了setSingleView()。这样的话,mock框架就不用解析 控制器中的视图名了。在很多场景中,其实没有必要这样做。但是对 于这个控制器方法,视图名与请求路径是非常相似的,这样按照默认 的视图解析规则时,MockMvc就会发生失败,因为无法区分视图路 径和控制器的路径。在这个测试中,构建InternalResourceView 时所设置的实际路径是无关紧要的,但我们将其设置为 与InternalResourceViewResolver配置一致。 这个测试对“/spittles”发起GET请求,然后断言视图的名称为spittles并 且模型中包含名为spittleList的属性,在spittleList中包含 预期的内容。
--我们可以看到SpittleController有一个构造器,这个构造器使 用了@Autowired注解,用来注入SpittleRepository。这 个SpittleRepository随后又用在spittles()方法中,用来获 取最新的spittle列表。 需要注意的是,我们在spittles()方法中给定了一个Model作为参 数。这样,spittles()方法就能将Repository中获取到的 Spittle列表填充到模型中。Model实际上就是一个Map(也就是 key-value对的集合),它会传递给视图,这样数据就能渲染到客户端 了。当调用addAttribute()方法并且不指定key的时候,那么key会 根据值的对象类型推断确定。在本例中,因为它是一 个List<Spittle>,因此,键将会推断为spittleList,当然我们也可以显示的去指定也是可以的:
1 @RequestMapping(method = RequestMethod.GET)
2 public String spittles(Map model) {
3 model.put("spittleList",spittleRepository.findSpittles(Long.MAX_VALUE,20));
4 return "spittles"; //返回视图名
5 }
---在我们进行spittles.jsp定义的时候,我们需要使用到jstl的c标签,具体引用方法如下:
--导入如下jar包
--在jsp中引入jstl(如果引入core引发了错误,可以修改为使用core_rt)
--最终我们的JSP页面代码:
1 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
2 <%@taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>
3 <%
4 String path = request.getContextPath();
5 String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
6 %>
7 <%--<script src="${pageContext.request.contextPath}/js/jquery-1.11.3.min.js" ></script>--%>
8 <html>
9 <head>
10 <title>微博列表</title>
11 </head>
12 <body>
13 当前最新提交的20条微博如下:
14 <c:forEach items="${spittleList}" var="spittle">
15 <li id="spittle_<c:out value="spittle.id"/>">
16 <div class="spittleMessage">
17 <c:out value="${spittle.message}"/>
18 </div>
19 <div>
20 <span class="spittleTime">
21 <c:out value="${spittle.time}"/>
22 </span>
23 <span class="splittleLocation">
24 (<c:out value="${spittle.latitude}"/>,<c:out value="${spittle.longitude}"/>)
25 </span>
26 </div>
27 </li>
28 </c:forEach>
29 </body>
30 </html>
--当然为了成功实现我们在Tomcat服务器中的模拟展示,我们需要构建一个SpittleRepository的默认实现
1 package com.sky.data;
2
3 import com.sky.spittle.Spittle;
4 import org.springframework.stereotype.Component;
5
6 import java.util.ArrayList;
7 import java.util.Date;
8 import java.util.List;
9
10 /**
11 * @author : S K Y
12 * @version :0.0.1
13 */
14 @Component
15 public class DefaultSpittleRepository implements SpittleRepository {
16 @Override
17 public List<Spittle> findSpittles(long max, int count) {
18 List<Spittle> spittles = new ArrayList<>();
19 for (int i = 0; i < count; i++) {
20 spittles.add(new Spittle("Spittle" + i, new Date()));
21 }
22 return spittles;
23 }
24 }
--最终访问我们的spittles路径地址得到如下结果:
--同时我们还可以进行单元测试来判断我们的结果是否正确
1 @Test
2 public void shouldShowRecentSpittles() throws Exception {
3 List<Spittle> expectedSpittles = createSpittleList(20);
4 //此处需要使用到mockito相关的jar包
5 SpittleRepository mockRepository = mock(SpittleRepository.class);
6 when(mockRepository.findSpittles(Long.MAX_VALUE, 20)).thenReturn(expectedSpittles);
7 SpittleController controller = new SpittleController(mockRepository);
8 MockMvc mockMvc = standaloneSetup(controller)
9 .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
10 .build();
11 mockMvc.perform(get("/spittles")) //对spittles发起GET请求
12 .andExpect(view().name("spittles"))
13 .andExpect(model().attribute("spittleList" //断言预期的值
14 , IsCollectionContaining.hasItems(expectedSpittles.toArray())));
15 }
--这里会有一个问题, IsCollectionContaining.hasItems(expectedSpittles.toArray()的断言会失败,其原因可能是使用了commons-lang的equals和hashcode生成方法导致的,我们老实改用idea自动生成的equals()以及hashcode()方法则可以顺利通过测试
三.接受请求的输入
有些Web应用是只读的.人们只能通过浏览器在站点上闲逛,阅读服务器发送到浏览器中的内容.众多的Web应用允许用户参与进去.SpringMVC允许多种方式将客户端中的数据传送到控制器的处理器方法中:
1.查询参数(Query Parameter);
2.表单参数(Form Parameter);
3.路径变量(Path Variable);
--处理查询参数
如我我们在查询Spittle的列表时,希望获得是一部分我们所期望的数据,在数据库之中,会纪录下我们所有的当前所接受到的Spittle,但是如果我们期望的是只获得最新的20条数据或者获取id比某一个数值小的一定数量的spittle,我们需要为spittles()方法传递两个参数:
1.long max:当前允许查询的最大ID数值;
2.count:当前需要查询的总数;
--我们可以采用@RequestParam注解为我们控制器的方法变量添加当前的参数信息:
1 package com.sky.web;
2
3 import com.sky.data.SpittleRepository;
4 import org.springframework.beans.factory.annotation.Autowired;
5 import org.springframework.stereotype.Controller;
6 import org.springframework.ui.Model;
7 import org.springframework.web.bind.annotation.RequestMapping;
8 import org.springframework.web.bind.annotation.RequestMethod;
9 import org.springframework.web.bind.annotation.RequestParam;
10
11 import java.util.Map;
12
13 /**
14 * @author : S K Y
15 * @version :0.0.1
16 */
17 @Controller
18 @RequestMapping("/spittles")
19 public class SpittleController {
20 private SpittleRepository spittleRepository;
21 private static final String MAX_LONG_AS_STRING = Long.toString(Long.MAX_VALUE);
22
23 @Autowired //注入SpittleRepository
24 public SpittleController(SpittleRepository spittleRepository) {
25 this.spittleRepository = spittleRepository;
26 }
27
28 @RequestMapping(method = RequestMethod.GET)
29 public String spittles(
30 @RequestParam(value = "max", defaultValue = Long.MAX_VALUE + "") long max,
31 @RequestParam(value = "count", defaultValue = "20") int count
32 , Model model) {
33 System.out.println("max: " + max + " count: " + count);
34 model.addAttribute("spittleList", spittleRepository.findSpittles(max, count));
35 return "spittles"; //返回视图名
36 }
37
38
39 }
--在上述的示例中,我们可以在@RequestParam注解中,使用value属性指定当前的参数的key值,而后我们还能指定默认的参数值(在用户未输入参数时,不至于返回一个空值或者原始的数据),由于无法在注解中直接使用方法进行参数类型的转化,所以我们定义了一个static final类型的String参数,用来指定当前max参数的默认值为Long类型的最大数值.同样我们还能定义一个新的show()方法来查看指定ID的参数列表:
1 @RequestMapping(value = "/show", method = RequestMethod.GET,produces = "text/plain;charset=UTF-8")
2 public String show(@RequestParam(value = "id", defaultValue = "0") String id, Model model) {
3 System.out.println("当前指定的id值为: " + id);
4 model.addAttribute("spittleList", spittleRepository.findSpittles(10, 1));
5 return "spittles";
6 }
--这时如果在控制台打印出来的中文信息存在乱码问题的话,可以设置tomcat在启动时的JVM参数来指定我们的编码(在VM options属性中设置默认的编码):
--事实上对于上述的数据接收形式我们还能继续进行优化,可以使用路径参数接受输入
--通过路径参数接受输入
在我们根据指定ID参数获取当前的列表信息时,我们需要在浏览器地址中输入: /spittles/show?id=12345;显然这样可以完成我们的服务需求,但是在理想情况下,要识别的资源(Spittle)应该通过URL路径进行标识,而不是通过查询参数.对"/spittles/12345"发起GET请求直接获取到我们所需要查询的spittle是更加方便的.为了实现这种路径变量,SpringMVC允许我们在@RequestMapping路径中添加占位符.占位符的名称要使用大括号"{}"包裹起来,并且在方法参数中,我们需要使用@PathVariable注解来指定当前参数指向的是哪个占位符所代表的地址:
1 @RequestMapping(value = "/{id}", method = RequestMethod.GET)
2 public String show(Model model, @PathVariable("id") int id) {
3 System.out.println("当前路径占位符所获取的数据: " + id);
4 model.addAttribute("spittleList", spittleRepository.findSpittles(10, 1));
5 return "spittles"; //返回视图名
6 }
--当然如果说我们指定的参数的变量名称和占位符中所使用的名称是相同的,那么我们就可以省略@PathVariable中的value值得指定注解
四.处理表单
web应用的功能通常并不局限于用户推送内容,大所述的应用允许用户填充表单并将数据提交回应用中,通过这种方式实现与用户的交互.使用表单分为两个方面:展现表单以及处理用户通过表单提交的数据.在Spittr应用中,我们需要有个表单让新用户进行注册,想要实现注册,在页面展示之中,我们需要为用户提供一个可以跳转到登录注册界面的按钮:
1 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
2 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
3 <%
4 String path = request.getContextPath();
5 String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
6 %>
7 <%--<script src="${pageContext.request.contextPath}/js/jquery-1.11.3.min.js" ></script>--%>
8 <html>
9 <head>
10 <title>注册</title>
11 </head>
12 <style type="text/css">
13 #reg_form {
14 width: 210px;
15 padding: 5px;
16 display: inline-block;
17 border: 1px solid black; /*设置边框*/
18 }
19
20 .text {
21 display: block;
22 width: 180px;
23 height: 20px;
24 margin: 20px 10px 0 10px;
25 }
26
27 .submit {
28 width: 100px;
29 height: 20px;
30 margin: 20px 50px 0 50px;
31 }
32 </style>
33 <body>
34 <form action="<c:url value="/spittles/register"/>" id="reg_form" method="post">
35 用户名: <input type="text" name="username" class="text"/>
36 密码: <input name="password" type="password" class="text"/>
37 确认密码: <input name="surePassword" type="password" class="text"/>
38 <button class="submit">点击注册</button>
39 </form>
40 </body>
41 </html>
--实现我们的跳转请求处理:
1 @RequestMapping(value = "/register", method = RequestMethod.GET)
2 public String register() {
3 return "register";
4 }
--我们使用@RequestMapping注解的时候,指定method的类型是具有实际意义的,现在我们所定义的是/spittles/register地址的GET请求,同时我们还可以定义该地址的POST请求,需要注意的是,为了能够正确获取到我们数据的信息,我们需要对于表单的提交,有一个对应的简单实体类实现:
1 package com.sky.spittle;
2
3 /**
4 * @author : S K Y
5 * @version :0.0.1
6 */
7 public class User {
8 private String username;
9 private String password;
10
11 public String getUsername() {
12 return username;
13 }
14
15 public void setUsername(String username) {
16 this.username = username;
17 }
18
19 public String getPassword() {
20 return password;
21 }
22
23 public void setPassword(String password) {
24 this.password = password;
25 }
26
27 @Override
28 public String toString() {
29 return "User{" +
30 "username='" + username + '\'' +
31 ", password='" + password + '\'' +
32 '}';
33 }
34 }
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerForm(User user) {
3 System.out.println(user);
4 return "home";
5 }
--此时我们就可以在控制台看到成功的接收到了我们输入的用户名即密码,但是此时如果我们尝试输入中文进行提交的话,会出现乱码:
--为了解决乱码问题,我们可以在SpringWebInitializer类中重写我们的onStartup方法来完成对于中文乱码的解决:
1 package com.sky.config;
2
3 import org.springframework.web.filter.CharacterEncodingFilter;
4 import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
5
6 import javax.servlet.ServletContext;
7 import javax.servlet.ServletException;
8
9 /**
10 * @author : S K Y
11 * @version :0.0.1
12 */
13 public class SpringWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
14 @Override
15 protected Class<?>[] getRootConfigClasses() {
16 return new Class<?>[]{RootConfig.class};
17 }
18
19 @Override
20 protected Class<?>[] getServletConfigClasses() { //指定配置类
21 System.out.println("成功加载getServletConfigClasses");
22 return new Class<?>[]{WebConfig.class};
23 }
24
25 @Override
26 public void onStartup(ServletContext servletContext) throws ServletException {
27 super.onStartup(servletContext);
28 servletContext.addFilter("characterEncodingFilter"
29 , new CharacterEncodingFilter("UTF-8", true)) //设置编码
30 .addMappingForUrlPatterns(null, false, "/*"); //设置拦截的请求
31 }
32
33 @Override
34 protected String[] getServletMappings() {
35 return new String[]{"/"}; //将DispatcherServlet映射到"/"
36 }
37
38
39 }
--在此我们补充实现对于/spittles/register中post请求的单元测试:
1 @Test
2 public void testRegisterPost() throws Exception {
3 SpittleController controller = new SpittleController(new DefaultSpittleRepository());
4 MockMvc mockMvc = standaloneSetup(controller).build();
5 mockMvc.perform(post("/spittles/register")
6 .param("username", "Java代码")
7 .param("password", "123456")
8 .param("surePassword", "123456"))
9 .andExpect(view().name("home"));
10 }
--在目前的实现中,我们在接收到了表单请求之后,是直接渲染了home.jsp,除此之外,我们还能以重定向的方式返回到我们的信息页:
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerForm(User user) {
3 System.out.println(user);
4 return "redirect:/"; //重定向到我们的基本信息页
5 }
--除了"redirect:"之外,InternalResourceViewResolver还能识别"forward:"前缀,当我们使用forward:作为前缀时,则将会前往forward指定的请求地址,而不是重定向.事实上,我们在重定向回到主页面的时候,期望的是我们可以获取到当前用户的信息,因此完善我们的controller:
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerForm(User user) throws UnsupportedEncodingException {
3 System.out.println(user);
4 String encode = URLEncoder.encode(user.getUsername(), "UTF-8"); //对于中文字符,需要进行转化
5 return "redirect:/homepage?username=" + encode; //重定向到我们的基本信息页
6 }
1 package com.sky.web;
2
3 import com.sun.org.apache.xpath.internal.operations.Mod;
4 import org.springframework.stereotype.Controller;
5 import org.springframework.ui.Model;
6 import org.springframework.web.bind.annotation.PathVariable;
7 import org.springframework.web.bind.annotation.RequestMapping;
8 import org.springframework.web.bind.annotation.RequestMethod;
9 import org.springframework.web.bind.annotation.RequestParam;
10
11 /**
12 * @author : S K Y
13 * @version :0.0.1
14 */
15 @Controller //声明为一个控制器
16 @RequestMapping //将控制器映射到"/"
17 public class WebController {
18 @RequestMapping(value = "/", method = RequestMethod.GET) //处理GET请求
19 public String home() {
20 return "home";
21 }
22
23 @RequestMapping(value = "/homepage", params = {"username"})
24 public String homeWithUsername(@RequestParam String username, Model model) {
25 System.out.println("username: " + username);
26 model.addAttribute("username", username);
27 return "home";
28 }
29
30
31 }
--如果说表单之中没有发送username或password的话,那么就相当于非法操作了,当然我们可以在前台使用js代码进行提交的验证,但是对于一部分用户来说,这样做无济于事,因此我们需要在controller之中,也需要进行一次提交的验证,避免出现数据呈现不一致的情况.
--如果说用户在提交表单的时候,username和password是空的string的话,那么我们应该组织这个用户提交成功.因此我们可以使用Spring对Java校验API(Java Validation API,又称JSR-303)的支持.从Spring3.0开始,在Spring mvc中提供了对Java校验API的支持,为了实现我们的功能,需要保证我们的类路径下存在这个Java API:Hibernate Validator,列出所需的jar:
--在Java校验API中定义了多个注解,这些注解可以放在属性上,从而限制这些属性的值,这些注解都位于javax.validation.constraints包中,列出这些校验注解:
--在User用户提交注册表单的时候,似乎我们需要做的是使用@NotNull注解以及@Size注解:
1 package com.sky.spittle;
2
3 import javax.validation.constraints.NotNull;
4 import javax.validation.constraints.Size;
5
6 /**
7 * @author : S K Y
8 * @version :0.0.1
9 */
10 public class User {
11 @NotNull
12 @Size(min = 5, max = 15)
13 private String username;
14 @NotNull
15 @Size(min = 5, max = 15)
16 private String password;
17
18 public String getUsername() {
19 return username;
20 }
21
22 public void setUsername(String username) {
23 this.username = username;
24 }
25
26 public String getPassword() {
27 return password;
28 }
29
30 public void setPassword(String password) {
31 this.password = password;
32 }
33
34 @Override
35 public String toString() {
36 return "User{" +
37 "username='" + username + '\'' +
38 ", password='" + password + '\'' +
39 '}';
40 }
41 }
--现在就表示用户在在提交表单的时候,必须保证username以及password非空,并且在5-15个字符之间,在完成了这些操作之后,我们还需要对controller的方法进行数据验证的修改,即使用@Valid注解,告知Spring这个对象需要满足校验,同时为了避免在我们提交错误格式信息时,直接返回错误页面,我们可以使用org.springframework.validation.Errors来接收我们返回的错误信息,当存在错误信息的时候,及表示存在提交数据异常(通常这个错误信息是400,Bad request):
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerForm(@Valid User user, Errors errors) throws UnsupportedEncodingException {
3 System.out.println(user);
4 if (errors.hasErrors()) {
5 System.out.println("当前提交的表单有误,需要重新提交");
6 return "register"; //返回表单提交界面
7 } else {
8 String encode = URLEncoder.encode(user.getUsername(), "UTF-8"); //对于中文字符,需要进行转化
9 return "redirect:/homepage?username=" + encode; //重定向到我们的基本信息页
10 }
11 }
--当编写控制器的处理器方法时,Spring MVC极其灵活。概括来讲,如 果你的处理器方法需要内容的话,只需将对应的对象作为参数,而它 不需要的内容,则没有必要出现在参数列表中。这样,就为请求处理 带来了无限的可能性,同时还能保持一种简单的编程模型。