13、构建Spring Web应用程序(1)(spring笔记)

一、Spring MVC起步

1.1 跟踪Spring MVC的请求

1

在请求离开浏览器时(步骤1),会带有用户所请求内容的信息,至少会包含请求的URL,但是还可能带有其他信息。

  • 请求的第一站是前端控制器DispatcherServletSpringMVC所有的请求都会通过这个前端控制器,前端控制器是常用的Web应用程序模式,在这里是一个单实例的Servlet将请求委托给应用程序的其他组件来执行实际的处理。

  • DispatcherServlet的任务是将请求转发给控制器(Controller)。为了达到此目的,DispatcherServlet需要查询一个或多个处理器映射器(handler mapping)(步骤2)来确定请求所需要的控制器是哪一个(因为控制器会有很多),处理器映射器会根据请求所携带的URL信息进行决策。

  • 一旦选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器(步骤3)。到了控制器,请求会卸下其负载(用户 提交的信息)并耐心等待控制器处理这些信息。

  • 控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过,仅仅给用户返回原始的信息是不够的——这些信息需要以用户友好的方式进行格式化,一般会是HTML。所以,信息需要发送给一个视图(view),通常是JSP

  • 控制器做的最后一件事就是将模型数据打包,并且标识出用于渲染输出的视图名。然后发送回DispatcherServlet(步骤4)。

  • 而返回给DispatcherServlet的视图名并不直接表示某个特定的JSP,可能并不是JSP。相反,它仅仅传递的是一个逻辑名称,这个名称用来查找产生结果的真正视图。这就需要使用视图解析器对逻辑视图名进行解析(步骤5)。

  • 解析完之后就会找到相关视图对数据进行渲染(步骤6),在这里交付模型数据,之后对用户响应(步骤7)。

1.2 搭建Spring MVC

1.2.1 配置DispatcherServlet

按照传统方式,像DispatcherServlet这样的Servlet会配置在web.xml文件中。但是借助Servlet3.1Spring3.1的功能增强,这里使用JavaDispatcherServlet配置在Servlet容器中。

package spittr.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import spittr.web.WebConfig;

public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  
  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { RootConfig.class };
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { WebConfig.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }
}

说明:

  • 这里我们创建的应用的名称为Spittr。要理解上述代码是如何工作的,可能只需要知道扩展AbstractAnnotationConfigDispatcherServletInitializer的任意类都会自动地配置DispatcherServletSpring应用上下文,Spring的应用上下文位于应用程序的Servlet上下文之中。

  • AbstractAnnotationConfigDispatcherServletInitializer剖析:
    Servlet3.0环境中,容器会在类路径中查找实现了javax.servlet.ServletContainerInitializer接口的类,如果能发现的话,就用它来配置Servlet容器。Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来会查找WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现,即AbstractAnnotationConfigDispatcherServletInitializer。因为我们的SpitterWebInitializer扩展了AbstractAnnotationConfigDispatcherServletInitializer(同时也就实现了WebApplicationInitializer),因此部署到Servlet3.0容器中的时候,容器会自动发现它,并用它来配置Servlet上下文。

  • 这里的第一个方法getServletMappings(),它会将一个或多个路径映射到DispatcherServlet上。本例中,它映射的是“/”,这表示它会是应用的默认Servlet。会处理进入应用的所有请求。其他两个方法在后面说明。

  • 注意:这里使用的是Java方式,和web.xml方式不同在于,应用启动时,容器会在类路径下查找ServletContainerInitializer的实现(SpringServletContainerInitializer),此实现又查找WebApplicationInitializer的实现(AbstractAnnotationConfigDispatcherServletInitializer),这个类会创建DispatcherServletContextLoaderListener,而这里应用的配置类SpitterWebInitializer继承了AbstractAnnotationConfigDispatcherServletInitializer

1.2.2 两个应用上下文之间的故事

DispatcherServlet启动的时候,会创建Spring应用上下文,并加载配置文件或配置类中所声明的bean。在上述代码中的getServletConfigClasses()方法中,我们要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类中的bean(相关代码在后面给出)。

但是在Spring Web应用中,通常还会有另外一个应用上下文。这个上下文是由ContextLoaderListener创建的。一般情况是,我们希望DispatcherServlet加载包含Web组件的bean,如控制器、视图解析器以及处理器映射器,而ContextLoaderListener要加载应用中其他的bean。这些bean通常是驱动应用后端的中间层和数据层组件(如IoC容器)

实际上,AbstractAnnotationConfigDispatcherServletInitializer会同时创建DispatcherServlet和ContextLoaderListenergetServletConfigClasses()方法返回的带有@Configuration注解的类将会用来定义DispatcherServlet应用上下文中的beangetRootConfgiClasses()方法返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建应用上下文中的bean

当然我们也可以使用传统的web.xml配置,但是其实没有必要,而这种配置方式下的应用必须部署到支持Servlet3.0的服务器中才能正常工作。

1.2.3 启用Spring MVC

有多种方式配置DispatcherServlet,于是,启用Spring MVC组件的方式也不仅一种。可以使用<mvc:annotation-driven>启用注解驱动的Spring MVC(在后面说明),这里使用Java进行配置。

package spittr.web;
import org.springframework.context.annotation.*;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc//启用Spring MVC
@ComponentScan("spittr.web")//启用组件扫描
public class WebConfig extends WebMvcConfigurerAdapter {

  @Bean
  public ViewResolver viewResolver() {
    //配置JSP视图解析器
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
  }
  
  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    // 配置静态资源的处理
    configurer.enable();
  }
  
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 在后面进行说明
    super.addResourceHandlers(registry);
  }
}

说明:

  • 这里启用了组件扫描,后面带有@Controller注解的控制器会称为扫描时的候选bean。因此,我们不需要在配置类中显式的声明任何控制器。

  • 接下来,我们添加了一个ViewResolver bean。更具体来讲InternalResourceViewResolver。在后面会更为详细的讨论视图解析器。在查找的时候,它会在视图名称上加一个特定的前缀和后缀(例如,名为home的视图将会解析为/WEB-INF/views/home.jsp)。

  • 最后,WebConfig类还扩展了WebMvcConfigurerAdapter并重写了其configureDefaultServletHandling()方法。通过调用enable()方法,我们要求DispatcherServlet将对静态资源的请求转发到Servlet容器中默认的Servlet上,而不是使用DispatcherServlet本身来处理此类请求。

  • 下面给出RootConfig

package spittr.config;
import java.util.regex.Pattern;
import org.springframework.context.annotation.*;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import spittr.config.RootConfig.WebPackage;

@Configuration
@ComponentScan(basePackages={"spittr"}, 
    excludeFilters={
        @Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class)
    })
public class RootConfig {

}

说明:这里我们使用了自动扫描配置,在后面可以使用非Web组件来完善RootConfig。这里的因为前面@EnableWebMvc已经被加载了,这里使用@excludeFilters将其排除掉。

其实ContextLoaderListener会根据根配置文件RootConfig会加载相关bean,而上述配置中将EnableWebMvc类排除了,其实还应该将spittr.web包中的类排除(修改后的代码后面给出),因为那个包中的类是由DispatcherServlet来加载的,这样两者就不会产生重复的bean了,如果产生了重复,则优先使用DispatcherServlet返回的bean,而ContextLoaderListener产生的bean无法被调用,称为内存泄漏。

@Configuration
@Import(DataConfig.class)
@ComponentScan(basePackages={"spittr"},
        excludeFilters={
                @Filter(type=FilterType.CUSTOM, value=WebPackage.class)
        })
public class RootConfig {
    public static class WebPackage extends RegexPatternTypeFilter {
        public WebPackage() {
            super(Pattern.compile("spittr\\.web"));
        }
    }
}

1.3 Spittr简介

这里Spittr表示一个类似微博的应用,其中Spitter用于表示应用的用户,而Spittle表示用户发布的简短状态更新。

二、编写基本的控制器

Spring MVC中,控制器只是方法上添加了@RequestMapping注解的类,这个注解声明了它们所要处理的请求。这里控制器尽可能简单,假设控制器类要处理对“/”的请求,并渲染应用的首页。

package spittr.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(){
        return "home";
    }
}

说明:

  • 很显然,这个类带有@Controller注解,表示这个类是一个控制器。@Controller是一个构造型(stereotype)的注解,基于@Component注解。在这里,它的目的就是辅助实现组件扫描。因为这个类带有@Controller注解,因此组件扫描的时候会自动找到此类,并将其声明为一个bean

  • HomeController有一个方法home()方法,带有@RequestMapping注解。它的value属性指定了这个方法所要处理的请求路径,method属性细化了它所处理的HTTP方法。在本例中,当收到对“/”HTTP GET请求时,就会调用此方法处理。

  • 这里home()方法返回的是一个视图的逻辑名,之后DispatcherServlet会调用视图解析器对其进行解析,找到真正的视图(/WEB-INF/views/home.jsp)。

2.1 测试控制器

以前我们经常使用单元测试对某个方法进行测试,但是对于这种Web应用来说,测试总是要启动应用和服务器,这是较为麻烦的。从Spring3.2开始,可以按照控制器的方式来测试控制器,而不仅仅将控制器作为一个POJO来测试。Spring现在包含了一种mock Spring MVC并针对控制器执行HTTP请求的机制。这样就不需要启动应用和服务器了。

package spittr.web;
import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

public class HomeControllerTest {

    @Test
    public void testHomePage() throws Exception{
        HomeController controller = new HomeController();
        //初始化MockMvc对象
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
        mockMvc.perform(MockMvcRequestBuilders.get("/"))
                .andExpect(MockMvcResultMatchers.view().name("home"));
    }
}

说明:这里首先使用真实控制器对象初始化MockMvc对象,然后使用perform()方法发起GET请求,然后使用view()方法检查返回的结果是否是“name”(也就是检查返回的逻辑视图名是否是“name”)。

2.2 定义类级别的请求处理

之前我们是将请求定义在方法级别上(在方法上使用@RequestMapping注解),下面将请求定义在类级别上。

package spittr.web;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/")
public class HomeController {

  @RequestMapping(method = RequestMethod.GET)
  public String home(Model model) {
    return "home";
  }
}

说明:

  • 这里在控制器中,路径现在被转移到类级别的@RequestMapping上,而HTTP方法依然映射在方法级别上,此时这个注解会应用到控制器的所有处理器方法上。当然我们可以同时在方法上使用@RequestMapping注解,此时,方法级别上的注解就是对类上注解的一种补充,这里两个注解合并之后(没有方法级别上的注解)标明home()方法将会处理对“/”路径的GET请求。

  • 这里注意,@RequestMappingvalue属性能够接收一个String类型的数组。

@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController {
...
}

此时,控制器的home()方法能够映射到对“/”“/homepage”GET请求。

2.3 传递模型数据到视图中

大多数控制器可能并不像上面那样简单,在Spittr应用中,我们需要一个页面展现最近提交的Spittle列表。因此,我们需要一个新的方法来处理这个页面。

为了避免对数据库进行访问,这里定义一个数据访问的Repository接口,稍后实现它。

package spittr.data;
import java.util.List;
import spittr.Spittle;

public interface SpittleRepository {

  List<Spittle> findSpittles(long max, int count);
}

说明:findSpittles()方法接受两个参数。其中max参数代表所返回的Spittle中,Spittle ID属性的最大值,而count参数表明要返回多少个Spittle对象。为了获得最新的20Spittle对象,可以这样使用:

List<Spittle> recent = spittleRepository.findSpittles(Long.MAX_VALUE, 20);

下面给出Spittle对象:

package spittr;
import java.util.Date;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class Spittle {

  private final Long id;
  private final String message;
  private final Date time;
  private Double latitude;//经度
  private Double longitude;//纬度

  public Spittle(String message, Date time) {
    this(null, message, time, null, null);
  }
  
  public Spittle(Long id, String message, Date time, Double longitude, Double latitude) {
    this.id = id;
    this.message = message;
    this.time = time;
    this.longitude = longitude;
    this.latitude = latitude;
  }

  public long getId() {
    return id;
  }

  public String getMessage() {
    return message;
  }

  public Date getTime() {
    return time;
  }
  
  public Double getLongitude() {
    return longitude;
  }
  
  public Double getLatitude() {
    return latitude;
  }
  
  @Override
  public boolean equals(Object that) {
    return EqualsBuilder.reflectionEquals(this, that, "id", "time");
  }
  
  @Override
  public int hashCode() {
    return HashCodeBuilder.reflectionHashCode(this, "id", "time");
  }
}

说明:这里唯一要注意的是使用了Apache Common Lang包实现equals()hashCode()方法。下面对新的控制器进行测试。

@Test
public void houldShowRecentSpittles() throws Exception {
    List<Spittle> expectedSpittles = createSpittleList(20);
    SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class);
    Mockito.when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
            .thenReturn(expectedSpittles);

    SpittleController controller = new SpittleController(mockRepository);
    MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
            .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
            .build();

    mockMvc.perform(MockMvcRequestBuilders.get("/spittles"))
            .andExpect(MockMvcResultMatchers.view().name("spittles"))
            .andExpect(MockMvcResultMatchers.model().attributeExists("spittleList"))
            .andExpect(MockMvcResultMatchers.model().attribute("spittleList",
                    Matchers.hasItems(expectedSpittles.toArray())));


}

说明:这里需要导入mockito-all-2.0.2-beta.jarhamcrest-all-1.3.jar两个包。这里先是使用SpittleRepository接口创建了一个mock实例,之后的when(XXX).thenReturn(XXX)其实是一种配置,即调用某个方法应该返回什么值。然后使用mock对象构造一个控制器,而这里在MockMvc构造器上调用setSingleView()。这样的话,mock框架就不用解析控制器中的试图名了。在很多场景中,其实没有必要这样做。但是对于这个控制器方法,试图名与请求路径非常相似,这样如果按照默认的视图解析规则,MockMvc就会发生失败,因为无法区分视图路径和控制器路径。之后使用perform()方法发起GET请求,同时对视图和模型都设置了一些条件,即首先断言试图名为“spittles”,同时断言模型中有key“spittleList”的对象同时对象中包含相关预期内容。在运行测试之前还需要给出相关控制器:

package spittr.web;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import spittr.web.Spittle;
import spittr.web.SpittleRepository;

@Controller
@RequestMapping("/spittles")
public class SpittleController {

    private static final String MAX_LONG_AS_STRING = "9223372036854775807";
    private SpittleRepository spittleRepository;

    @Autowired
    public SpittleController(SpittleRepository spittleRepository) {
        this.spittleRepository = spittleRepository;
    }

    @RequestMapping(method=RequestMethod.GET)
    public String spittles(Model model) {
        model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20));
        return "spittles";
    }
}

说明:

  • 这里在构造器中注入spittleRepository。需要注意的是,在spittles()方法中定义了一个Model作为参数。这样,此方法就能将Repository中获取到的Spittle列表填充到模型中。Model实际上就是一个Map,它会传递给视图,这样数据就能渲染到客户端了。当调用addAttribute()方法并且不指定key的时候,那么key会根据值的对象类型推断。在本例中,因为它是一个List<Spittle>,因此,key推断为spittleList。最后返回视图名spittles

  • 当然在使用addAttribute()方法的时候也可以指定key

@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model) {
  model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
  return "spittles";
}
  • 如果你希望使用非Spring类型的话,那么可以使用Map来代替Model
@RequestMapping(method=RequestMethod.GET)
public String spittles(Map model) {
  model.put("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
  return "spittles";
}
  • 还有另一种方式来编写spittles()方法:
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
    return spittleRepository.findSpittles(Long.MAX_VALUE, 20);
}

当处理器方法像这样返回对象或集合时,这个返回值会放到模型中,模型key会根据其类型推断得出(此处为spittleList),而逻辑视图名会根据请求路径推断出,因为这个方法处理针对“/spittles”,于是逻辑视图名为spittles。下面给出/WEB-INF/views/spittles.jsp

<c:forEach items="${spittleList}" var="spittle" >
  <li id="spittle_<c:out value="spittle.id"/>">
    <div class="spittleMessage"><c:out value="${spittle.message}" /></div>
    <div>
      <span class="spittleTime"><c:out value="${spittle.time}" /></span>
      <span class="spittleLocation">(<c:out value="${spittle.latitude}" />,
      <c:out value="${spittle.longitude}" />)</span>
    </div>
  </li>

此处使用了一些JSTL表达式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容