Spring学习--构建Spring Web应用程序

Posted 灰色天空_graySky

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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() {
以上是关于Spring学习--构建Spring Web应用程序的主要内容,如果未能解决你的问题,请参考以下文章

Spring实战5-基于Spring构建Web应用

20191114 Spring Boot官方文档学习(4.7)

Spring MVC官方文档学习笔记之Web入门

spring构建 spring web 应用程序

Spring学习总结- IOC

spring boot学习系列