java我爱编程

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

2017-04-06  本文已影响175人  yjaal

一、Spring MVC起步

1.1 跟踪Spring MVC的请求

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

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[] { "/" };
  }
}

说明:

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);
  }
}

说明:

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";
    }
}

说明:

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";
  }
}

说明:

@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";
    }
}

说明:

@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model) {
  model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
  return "spittles";
}
@RequestMapping(method=RequestMethod.GET)
public String spittles(Map model) {
  model.put("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
  return "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表达式。

上一篇下一篇

猜你喜欢

热点阅读