构建Spring Web应用程序
跟踪Spring MVC的请求
使用Spring构建的Web程序中,请求最先接触到的是Spring中的DispatcherServlet。从图中可以看见DispatcherServlet相当一个调度者,所有的核心环节最终都要汇总到DispatcherServlet中。
对图流程的概要说明:
- DispatcherServlet其实是一个Servlet,用于拦截客户端的所有请求。
- 拦截到客户端的请求会调用处理映射器,找到处理请求对应的控制器(controller)。
- 控制器会根据请求参数进行相关的业务处理,一般情况下控制器只负责解析请求中的参数,然后调用其它组件(如业务处理组件)并传递参数来处理请求。控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅仅给用户返回原始的信息是不够的——这些信息需要以用户友好的方式进行格式化,一般会是HTML。所以,信息需要发送给一个视图(view),通常会是JSP。
- 在处理完成客户的请求后会返回逻辑视图的名称和模型。
- DispatcherServlet会调用视图解析器,将逻辑视图名称解析成物理视图名称(也就是视图在项目中的路径)。
- DispatcherServlet会调用视图实现,渲染视图,将模型交付给视图。
- 通过响应对象response将渲染完成的视图传递给客户端。这样就完成了响应。
构建Spring Web应用程序
书中给出的Spittr应用有点抽象,理解上不太符合中国程序员的国情。这里用一个简单的博客发布系统来替代Spittr应用来了解如何构建Spring Web应用程序。博客系统主要实现博客文章列表查看、博客文章查询和博客发布功能。Blog完全使用java类来配置Spring web,这也是个人对这种配置方式的一次体验。以下是Blog应用程序的截图程序结构截图:
配置DispatcherServlet
我们通常会在web.xml文件中配置DispatcherServlet。但是,借助于Servlet 3.0规范和Spring3.1功能的增强,这种方式已经不是唯一方式。我们将使用java类来配置DispatcherServlet。BlogWebAppInitializer 继承 AbstractAnnotationConfigDispatcherServletInitializer,这个类已经隐身的帮我配置了DispatcherServlet。我们找到传统的web.xml文件方式配置容器的方式和java类配置容器的方式进行对比,就会发现在java类配置方式中能找到和web.xml文件方式一一对应的地方,在接下来的代码中我们会看到对应的地方。Blog相关配置类如下:
BlogWebAppInitializer
package blog.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
* java类配置Spring
*
* @author Administrator
*
*/
public class BlogWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 配置WebApplicationContent上下文
*/
@Override
protected Class<?>[] getRootConfigClasses() {
// TODO Auto-generated method stub
return new Class<?>[] { RootConfig.class };
}
/**
* 配置WebApplicationContent上下文
*/
@Override
protected Class<?>[] getServletConfigClasses() {
// TODO Auto-generated method stub
return new Class<?>[] { WebConfig.class };
}
@Override
protected String[] getServletMappings() {
// 配置DispatcherServlet拦截路径
return new String[] { "/" };
}
}
RootConfig类
package blog.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* 用于初始化Sring的ApplicationContent(整个应用的上下文)
* ApplicationContent一般包含后端相关的bean和配置,例如业务层和持久化层Bean
* @author Administrator
*
*/
@Configuration
@Import(DataConfig.class)
@ComponentScan(basePackages = { "blog" },
excludeFilters = { //不扫描blog.web //不扫描类上标记EnableWebMvc注解(也就是不扫描WebConfig.java)
@Filter(type = FilterType.REGEX, pattern = "blog\\.web"),
@Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
}
)
public class RootConfig {
}
DataConfig类package blog.config;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
/**
* 配置访问数据相关的Bean
* @author Administrator
*
*/
@Configuration
public class DataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)//设置数据库类型为H2内存数据库
.addScript("blog.sql") //数据库的建表脚步以及数据初始化脚步
.build();
}
@Bean
public JdbcOperations jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
RootConfig类package blog.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* 用于初始化Sring的ApplicationContent(整个应用的上下文)
* ApplicationContent一般包含后端相关的bean和配置,例如业务层和持久化层Bean
* @author Administrator
*
*/
@Configuration
@Import(DataConfig.class)
@ComponentScan(basePackages = { "blog" },
excludeFilters = { //不扫描blog.web //不扫描类上标记EnableWebMvc注解(也就是不扫描WebConfig.java)
@Filter(type = FilterType.REGEX, pattern = "blog\\.web"),
@Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
}
)
public class RootConfig {
}
AbstractAnnotationConfigDispatcherServletInitializer剖析
我们发现BlogWebAppInitializer继承了AbstractAnnotationConfigDispatcherServletInitializer类。在Servlet 3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果能发现的话,就会用它来配置Servlet容器。
Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。
Spring 3.2引入了一个便利的WebApplicationInitializer基础实现,也就是AbstractAnnotationConfigDispatcherServletInitializer。因为我们的扩展了AbstractAnnotationConfig DispatcherServletInitializer(同时也就实现了WebApplicationInitializer),因此当部署到Servlet 3.0容器中的时候,容器会自动发现它,并用它来配置Servlet上下文。
两个应用上下文
当DispatcherServlet的时候,Spring会创建两个应用上下文。一个是通过getServletConfigClasses()方法创建,这个应用上下文是
Spring的WebApplicationContent(Web应用上下文),一般包含前端Bean和配置,例如前段的Controller类。另一个是通过getRootConfigClasses()方法创建,这个应用上下文是
Sring的ApplicationContent(整个应用的上下文)一般包含后端相关的bean和配置,例如业务层和持久化层Bean。
编写基本的控制器
BlogManager类中的Controller实现了查询全部博客文章、根据条件查询博客文章和博客发布功能。我们从这里可以看到主要涉及到@Controller和@RequestMapping注解。
@Controller:这个注解标记在类上类,表示该类是一个控制器。@Controller、@Service、@Repository注解都使用@Component注解标记,他们同@Component都是表示标记的类是一个Spring组件,使用@Controller、@Service、@Repository标记在类上只不过是语义更明确。@Service表示一个业务组件,@Repository表示一个持久化组件,@Repository更笼统些,就是表示一个组件。
@RequestMapping:这个注解标记在类和方法上。标记在方法用于配置方法处理的请求地址,其中value属性配置地址,method属性配置请求方式。标记在类似用于配置value属性的前缀。类如在类上标记@RequestMapping("/blogManager"),query()方法标记@RequestMapping(value = "/blogList", method = GET),那么query()方法将处理get方式请求的/blogManager/blogList地址。
package blog.web;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
import java.io.UnsupportedEncodingException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import blog.data.AboutBlogRepository;
import blog.model.AboutBlogModel;
@Controller
@RequestMapping("/blogManager")
public class BlogManager {
private AboutBlogRepository aboutBlogReq;
private static final Log log = LogFactory.getLog(BlogManager.class);
@Autowired
public BlogManager(AboutBlogRepository aboutBlogReq) {
this.aboutBlogReq = aboutBlogReq;
}
/**
* 查询全部博客文章
* @param model
* @return
*/
@RequestMapping(value = "/blogList", method = GET)
public String query(Model model) {
// 将模型传递到视图有A1,A2,A3三种方式
// A1:key-value表示一个模型
model.addAttribute("blogList", aboutBlogReq.query());
// A2:传入模型数据,key默认为模型数据的类型,这里query返回类型是List<AboutBlogModel>,key名称为aboutBlogModelList
// model.addAttribute(aboutBlogReq.query());
return "blogList";
}
/* //A3:使用Map来代替Model
* @RequestMapping(value = "/blogList", method = GET)
* public String query(Map map) {
* map.put("blogList",aboutBlogReq.query());
* return "blogList";
* }
*/
/**
* 根据参数查询博客文章
* 使用Spring MVC接受查询参数
* @param title
* @param context
* @param model
* @return
*/
@RequestMapping(value = "/blogListByParam", method = GET)
public String queryByParam(@RequestParam(value = "title", defaultValue = "") String title,
@RequestParam(value = "context", defaultValue = "") String context, Model model) {
// 请求地址没有带有title,context查询参数,将会报404错误,指定defaultValue属性即可解决
model.addAttribute("blogList", aboutBlogReq.query(title, context));
return "queryByParam";
}
/**
* 根据blogId查询博客文章信息
* 使用Spring MVC处理路径变量参数
* @param blogId
* @param model
* @return
*/
@RequestMapping(value = "/queryOne/{blogId}", method = GET)
public String queryOne(@PathVariable() String blogId, Model model) {
// 占位符{blogId}和形参blogId名称相同时可以省略@PathVariable注解value参数
// 否则要像这样@PathVariable("blogId")显示声明占位符关联的形参
model.addAttribute("aboutBlogInfo", aboutBlogReq.queryOne(blogId));
return "queryOne";
}
/**
* 博客发布视图
* @param request
* @return
*/
@RequestMapping(value = "/wirte", method = GET)
public String showWirteForm(HttpServletRequest request) {
return "showWirteForm";
}
/**
* 博客发布操作
* 使用Spring MVC处理表单参数
* @param aboutBlogModel
* @param errors
* @param request
* @return
*/
@RequestMapping(value = "/wirte", method = POST)
public String wirte(@Valid AboutBlogModel aboutBlogModel,Errors errors, HttpServletRequest request) {
if(errors.hasErrors()) {
return "showWirteForm";
}
String s="";
try {
s = new String(request.getParameter("name").getBytes("ISO-8859-1"),"UTF-8");
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String encoding = request.getCharacterEncoding();
log.debug("encoding:" + encoding+",name:"+s);
String id = aboutBlogReq.add(aboutBlogModel);
// 这里使用重定向,防止用户重复提交(比如多次点击刷新按钮)
return "redirect:/blogManager/queryOne/" + id;
}
}
接受请求的输入
Spring MVC允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:
- 查询参数(Query Parameter)。
- 表单参数(Form Parameter)。
- 路径变量(Path Variable)。
表单参数:是通过HTML form标签提交的请求参数。比如博客发布时候需要录入文章标题、文章关键字和文章内容等信息。这些信息参数都会通过表单参数提交给Controller。
路径变量:通过URL路径传递参数。例如通过博客文章ID查询文章的信息,如果使用路径变量,请求url是这样:
http://localhost:8080/blog/blogManager/queryOne/6f77e01b-7c84-4b9f-8b46-d674a93532df,6f77e01b-7c84-4b9f-8b46-d674a93532df为博客ID。
校验表单
我们在发布博客的时候,如果输入的关键信息为空可以提示给用户,可能还需要校验关键字必须是按逗号(,)分隔、文章内容必须在10到3000个字符等等。这些校验操作可以使用Spring对Java校验API(Java Validation API,又称JSR-303)的支持。从Spring 3.0开始,在Spring MVC中提供了对Java校验API的支持。在Spring MVC中要使用Java校验API的话,并不需要什么额外的配置。只要保证在类路径下包含这个Java API的实现即可,比如Hibernate Validator。Java校验API定义了多个注解,这些注解可以放到属性上,从而限制这些属性的值。所有的注解都位于javax.validation.constraints包中。一下列出了这些校验注解。
@AssertFalse 所注解的元素必须是Boolean类型,并且值为false
@AssertTrue 所注解的元素必须是Boolean类型,并且值为true
@DecimalMax 所注解的元素必须是数字,并且它的值要小于或等于给定的BigDecimalString值
@DecimalMin 所注解的元素必须是数字,并且它的值要大于或等于给定的BigDecimalString值
@Digits 所注解的元素必须是数字,并且它的值必须有指定的位数
@Future 所注解的元素的值必须是一个将来的日期
@Max 所注解的元素必须是数字,并且它的值要小于或等于给定的值
@Min 所注解的元素必须是数字,并且它的值要大于或等于给定的值
@NotNull 所注解元素的值必须不能为null
@Null 所注解元素的值必须为null
@Past 所注解的元素的值必须是一个已过去的日期
@Pattern 所注解的元素的值必须匹配给定的正则表达式
@Size 所注解的元素的值必须是String、集合或数组,并且它的长度要符合给定的范围
@AssertTrue 所注解的元素必须是Boolean类型,并且值为true
@DecimalMax 所注解的元素必须是数字,并且它的值要小于或等于给定的BigDecimalString值
@DecimalMin 所注解的元素必须是数字,并且它的值要大于或等于给定的BigDecimalString值
@Digits 所注解的元素必须是数字,并且它的值必须有指定的位数
@Future 所注解的元素的值必须是一个将来的日期
@Max 所注解的元素必须是数字,并且它的值要小于或等于给定的值
@Min 所注解的元素必须是数字,并且它的值要大于或等于给定的值
@NotNull 所注解元素的值必须不能为null
@Null 所注解元素的值必须为null
@Past 所注解的元素的值必须是一个已过去的日期
@Pattern 所注解的元素的值必须匹配给定的正则表达式
@Size 所注解的元素的值必须是String、集合或数组,并且它的长度要符合给定的范围
AboutBlogModel类
package blog.model;
import java.util.Date;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class AboutBlogModel {
private String id;
@NotNull
private String keyword;
@NotNull
private String description;
@NotNull
private String name;
@NotNull
private String title;
@NotNull
@Size(min=10,max=300)
private String context;
private Date wireteDate;
public String getId() {
return id;
}
public Date getWireteDate() {
return wireteDate;
}
public void setWireteDate(Date wireteDate) {
this.wireteDate = wireteDate;
}
public void setId(String id) {
this.id = id;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContext() {
return context;
}
public void setContext(String context) {
this.context = context;
}
}
Blog项目下载地址:
http://download.youkuaiyun.com/download/fly_zxy/10148466。到总结完这章内容为止,Blog还有个很大的缺陷,就是博客发布信息中有中文会乱码,在使用web.xml配置Servlet容器时将org.springframework.web.filter.CharacterEncodingFilter作为filter配置在web.xml中即可解决中文乱码问题。使用java类配置Servlet容器还没有找到思路将CharacterEncodingFilter用java类的方式配置到Servlet容器中。随着后续的阅读应该可以找到配置方法。