创建 Spring Web 应用程序

  1. Spring MVC
    • 请求生命周期
      • DispatcherServlet

1.2 Setting up Spring MVC

DispatcherServlet is the centerpiece of Spring MVC. It’s where the request first hits the framework, and it’s responsible for routing the request through all the other components.

Historically, servlets like DispatcherServlet have been configured in a web.xml file that’s carried in the web application’s WAR file. Certainly that’s one option for con-figuring DispatcherServlet.

Instead of a web.xml file, you’re going to use Java to configure DispatcherServlet in the servlet container.

public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    // 定义 ContextLoaderListener
  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { RootConfig.class };
  }

    // 定义 DispatcherServlet bean 的上下文
  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { WebConfig.class };
  }

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

}

it’s probably sufficient to know that any class that extends AbstractAnnotationConfigDispatcherServletInitializer will auto-matically be used to configure DispatcherServlet and the Spring application context in the application’s servlet context. [任何继承自 AbstractAnnotationConfigDispatcherServletInitializer 的类都会被自动用来配置 DispatcherServlet 和 Spring 应用上下文环境]

The first method, getServletMappings(), identifies one or more paths that DispatcherServlet will be mapped to. In this case, it’s mapped to /, indicating that it will be the application’s default servlet. It will handle all requests coming into the application.

In order to understand the other two methods, you must first understand the relationship between DispatcherServlet and a servlet listener known as ContextLoaderListener .

两个应用上下文

When DispatcherServlet starts up, it creates a Spring application context and starts loading it with beans declared in the configuration files or classes that it’s given. 通过 getServletConfigClasses() 方法,你告诉 DispatcherServletWebConfig.java 类中来加载定义了一些 bean 的应用程序上下文。

然而在 Spring web 应用程序中,还存在另外一种 application context,叫做 ContextLoaderListener.

Whereas DispatcherServlet is expected to load beans containing web components such as controllers, view resolvers, and handler mappings, ContextLoaderListener is expected to load the other beans in your application. These beans are typically the middle-tier and data-tier components that drive the back end of the application.

Under the covers, AbstractAnnotationConfigDispatcherServletInitializer creates both a DispatcherServlet and a ContextLoaderListener. The @Configuration classes returned from getServletConfigClasses() will define beans for DispatcherServlet ’s application context. Meanwhile, the @Configuration class’s returned getRootConfigClasses() will be used to configure the application context created by ContextLoaderListener

The only gotcha with configuring DispatcherServlet in this way, as opposed to in a web.xml file, is that it will only work when deploying to a server that supports Servlet 3.0, such as Apache Tomcat 7 or higher.

开启 Spring MVC

The very simplest Spring MVC configuration you can create is a class annotated with @EnableWebMvc:

@Configuration
@EnableWebMvc
public class WebConfig {
}

这虽然工作,然而:

  • No view resolver is configured. As such, Spring will default to using BeanNameViewResolver , a view resolver that resolves views by looking for beans whose ID matches the view name and whose class implements the View interface.
  • Component-scanning isn’t enabled. Consequently, the only way Spring will find any controllers is if you declare them explicitly in the configuration.
  • As it is, DispatcherServlet is mapped as the default servlet for the application and will handle all requests, including requests for static resources, such as images and stylesheets (which is probably not what you want in most cases). [接受所有请求]
@Configuration
@EnableWebMvc
@ComponentScan("spittr.web") // 扫描 `spitter.web` 包
public class WebConfig extends WebMvcConfigurerAdapter {

  @Bean
  public ViewResolver viewResolver() { // 视图解析器
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp"); 
    return resolver;
  }

  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    // 静态资源让 Spring 默认处理器来处理
    configurer.enable();
  }

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // TODO Auto-generated method stub
    super.addResourceHandlers(registry);
  }

}

2. 编写一个简单的 Controller

什么是 Controller: In Spring MVC, controllers are just classes with methods that are annotated with @RequestMapping to declare the kind of requests they’ll handle.

@Controller
public class HomeController {

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

}

As you can see, the home() method doesn’t do much: it returns a String value of “home”. This String will be interpreted by Spring MVC as the name of the view that will be rendered. DispatcherServlet will ask the view resolver to resolve this logical view name into an actual view.

2.1 测试 Controller:

public class HomeControllerTest {

  @Test
  public void testHomePage() throws Exception {
    HomeController controller = new HomeController();
    MockMvc mockMvc = standaloneSetup(controller).build();
    mockMvc.perform(get("/"))
           .andExpect(view().name("home"));
  }

}

2.2 定义 class-level 请求处理

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

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

}

@RequestMapping("/") 从方法移到 class 级别,可以把这个请求路径应用到该类的所有方法上。

While you’re tinkering with the @RequestMapping annotations, you can make another tweak to HomeController . The value attribute of @RequestMapping accepts an array of String. So far, you’ve only given it a single String value of “/”. But you can also map it to requests whose path is /homepage by changing the class-level @RequestMapping to look like this:

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

2.3 把 model data 传递给 view

First you need to define a repository for data access. For decoupling purposes, and so you don’t get bogged down in database specifics, you’ll define the repository as an interface now and create an implementation of it later.

package spittr.data;
import java.util.List;
import spittr.Spittle;
public interface SpittleRepository {
    List<Spittle> findSpittles(long max, int count);
}

编写 SpittleController :

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

  private static final String MAX_LONG_AS_STRING = "9223372036854775807";

  private SpittleRepository spittleRepository;

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

  @RequestMapping(method=RequestMethod.GET)
  public String spittles(Model model) { // Model 其实就是一个 key-value
      model.addAttribute( spittleRepository.findSpittles(Long.MAX_VALUE, 20) );
      return "spittles"; // 返回 view 视图器的名字
  }

}

When addAttribute() is called without specifying a key, the key is inferred from the type of object being set as the value [从值的类型反推]. If you’d prefer to be explicit about the model key, you’re welcome to specify it:

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

不喜欢 Model,也可以用 Map:

Likewise, if you’d prefer to work with a non-Spring type, you can ask for a java.util.Map instead of Model. And while we’re on the subject of alternate implementations, here’s another way to write the spittles() method:

@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles() {
    return spittleRepository.findSpittles(Long.MAX_VALUE, 20));
}

JSTL 语言写的 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>
</c:forEach>

1.3 接收输入

Spring MVC provides several ways that a client can pass data into a controller’s handler method [给 Controller 传递参数的几种方法]. These include:

  • Query parameters
  • Form parameters
  • Path variables

1.3.1 参数查询

@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(@RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
                              @RequestParam(value="count", defaultValue="20") int count) {
    return spittleRepository.findSpittles(max, count);
}

1.3.2 路径参数查询

@RequestMapping(value="/show", method=RequestMethod.GET)
public String showSpittle(
                          @RequestParam("spittle_id") long spittleId,
                          Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}

这个方法可以处理 /spittles/show?spittle_id=12345 这种路径。 As a general rule, query parameters should not be used to identify a resource. A GET request for /spittles/12345 is better than one for /spittles/show?spittle_id=12345. The former identifies a resource to be retrieved. The latter describes an operation with a parameter—essentially RPC over HTTP .

To accommodate these path variables, Spring MVC allows for placeholders in an @RequestMapping path. The placeholders are names surrounded by curly braces ({ and }). Although all the other parts of the path need to match exactly for the request to be handled, the placeholder can carry any value.

@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(
                      @PathVariable("spittleId") long spittleId,
                      Model model) {
                    model.addAttribute(spittleRepository.findOne(spittleId));
                    return "spittle";
                }

If the request is a GET request for /spittles/54321, then 54321 will be passed in as the value of spittleId. Because the method parameter name happens to be the same as the placeholder name, you can optionally omit the value parameter on @PathVariable:

@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(@PathVariable long spittleId, Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}

If no value attribute is given for @PathVariable, it assumes the placeholder’s name is the same as the method parameter name. This can make the code a little cleaner by not duplicating the placeholder name any more than necessary. But be cautioned: if you decide to rename the parameter, you must also change the placeholder name to match.

The data in the Spittle object can then be rendered in the view by referring to the request attribute whose key is spittle (the same as the model key). Here’s a snippet of a JSP view that renders the Spittle :

<div class="spittleView">
  <div class="spittleMessage"><c:out value="${spittle.message}" /></div>
  <div>
    <span class="spittleTime"><c:out value="${spittle.time}" /></span>
  </div>
</div>

4. 处理表单

@Controller
@RequestMapping("/spitter")
public class SpitterController {
    @RequestMapping(value="/register", method=GET)
    public String showRegistrationForm() {
        return "registerForm";
    }
}

4.1 表单处理 Handler

@Controller
@RequestMapping("/spitter")
public class SpitterController {

  private SpitterRepository spitterRepository;

  @Autowired
  public SpitterController(SpitterRepository spitterRepository) {
    this.spitterRepository = spitterRepository;
  }

  @RequestMapping(value="/register", method=GET)
  public String showRegistrationForm() {
    return "registerForm";
  }

  @RequestMapping(value="/register", method=POST)
  public String processRegistration(
      Spitter spitter, 
      Errors errors) {
    spitterRepository.save(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
  }

}

The showRegistrationForm() method is still in place. But notice the new processRegistration() method: it’s given a Spitter object as a parameter. This object has firstName, lastName, username, and password properties that will be populated from the request parameters of the same name.

The last thing that processRegistration() does is return a String specifying the view. But this view specification is different from what you’ve seen before. Rather than just return a view name and let the view resolver sort it out, here you’re returning a redirect specification [返回一个重定向].

When InternalResourceViewResolver sees the redirect: prefix on the view specification, it knows to interpret it as a redirect specification instead of as a view name. In this case, it will redirect to the path for a user’s profile page. For example, if the Spitter.username property is jbauer, then the view will redirect to /spitter/jbauer.

It’s worth noting that in addition to redirect: , InternalResourceViewResolver also recognizes the forward: prefix. When it sees a view specification prefixed with forward:, the request is forwarded to the given URL path instead of redirected [除了认识 redirect: 前缀,还认识 forward: 前缀].

Because you’re redirecting to the user’s profile page, you should probably add a handler method to SpitterController to handle requests for the profile page. Here’s a showSpitterProfile() method that will do the trick:

@Controller
@RequestMapping("/spitter")
public class SpitterController {

  @RequestMapping(value="/{username}", method=GET)
  public String showSpitterProfile(@PathVariable String username, Model model) {
    Spitter spitter = spitterRepository.findByUsername(username);
    model.addAttribute(spitter);
    return "profile";
  }

}

4.2 验证表单

One way to handle validation, albeit naive, is to add code to the processRegistration() method to check for invalid values and send the user back to the registration form unless the data is valid. It’s a short method, so tossing in a few extra if statements won’t do much harm. Right?

Rather than litter your handler methods with validation logic, however, you can take advantage of Spring’s support for the Java Validation API (a.k.a. JSR-303). Starting with Spring 3.0, Spring supports the Java Validation API in Spring MVC . No extra configuration is required to make Java Validation work in Spring MVC. You just need to make sure an implementation of the Java API, such as Hibernate Validator, is in the project’s classpath. [只需要找到一个实现 Java Validation 的库就行了]

The Java Validation API defines several annotations [定义了几个注解] that you can put on properties to place constraints on the values of those properties. All of these annotations are in the javax.validation.constraints package. it’s also possible to define your own constraints. [你也可以定义你自己的约束]

public class Spitter {

    private Long id;

    @NotNull
    @Size(min=5, max=16)
    private String username;

    @NotNull
    @Size(min=5, max=25)
    private String password;

    @NotNull
    @Size(min=2, max=30)
    private String firstName;

    @NotNull
    @Size(min=2, max=30)
    private String lastName;

}

Now that you have annotated Spitter with validation constraints, you need to change the processRegistration() method to apply validation.

@RequestMapping(value="/register", method=POST)
public String processRegistration(
                                  @Valid Spitter spitter, 
                                  Errors errors) {
    if (errors.hasErrors()) {
        return "registerForm";
    }

    spitterRepository.save(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
}

Just having validation constraints on the Spitter’s properties won’t prevent the form from being submitted. Even if the user fails to fill in a field on the form or gives a value whose length exceeds the maximum length, the processRegistration() method will still be called. This gives you a chance to deal with the validation problems however you see fit in processRegistration().

results matching ""

    No results matching ""