Spring MVC -- 基于注解的控制器
在Spring MVC -- Spring MVC入门中,我们创建了两个采用传统风格控制器的Spring MVC应用程序,其控制器是实现了Controller接口。Spring 2.5版本引入了一个新途径:使用控制器注解类型。本篇博客将介绍基于注解的控制器,以及各种对应用程序有用的注解类型。
一 Spring MVC注解类型
使用基于注解的控制器具有以下优点:
- 一个控制器可以处理多个动作(而实现了Controller接口的一个控制器只能处理一个动作)。这就允许将相关的操作写在同一个控制器类中,从而减少应用程序中类的数量;
- 基于注解的控制器的请求映射不需要存储在配置文件中。使用RequestMapping注释类型,可以对同一个方法进行请求处理;
Controller和RequestMapping注解类型是Spring MVC API最重要的两个注解类型,本节将会重点介绍着两个,并简要介绍一些其它不太流行的注解类型。
1、Controller注解类型
org.springframework.stereotype.Controller注解类型位于spring-context-x.x.x.RELEASE.jar包下,用于指示Spring类的实例是一个控制器。下面是一个带注解@Controller的例子:
package com.example.controller; import org.springframework.stereotype.Controller; ... @Controller public class CustomerController { //request-handing methods here }
Spring使用扫描机制来找到应用程序中所有基于注解的控制器类。为了保证Spring能找到控制器,需要完成两件事情:
1)需要在Spring MVC的配置文件中声明,spring-context,如下所示:
<beans ... xmlns:context="http://www.springframework.org/schema/context" ...
>
... </beans>
2)需要应用<component-scan/>元素,如下所示:
<context:component-scan base-package="basePackage"/>
请在<component-scan/>元素中指定控制器类的基本包。例如,若所有的控制器类都在com.example.controller及其子包下,则需要编写一个如下所示的<component-scan/>元素:
<context:component-scan base-package="com.example.controller"/>
现在整个在配置文件看上去如下所示:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.example.controller"/> </beans>
请确保所有控制器类都在基本包下,并且不要指定一个太广泛的基本包,因为这样会使得Spring MVC扫描了无关的包。
注意:默认情况下,<component-scan/>元素查找使用构造型(stereotype)注解所标注的类,包括@Component(组件),@Service(服务),@Controller(控制器),@Repository(数据仓库)。更多内容可以参考博客:<context:component-scan>详解。
2、RequestMapping注解类型
现在,我们需要在控制器器类的内部为每一个动作开发相应的处理函数。要让Spring知道用哪一种方法来处理它的动作,需要使用org.springframework.web.bind.annotation.RequestMapping注解类型来将动作映射到相应的处理函数。
RequestMapping注解类型的作用同其名字所暗示的:将一个请求映射到一个方法。使用@RequestMapping注解一种方法(或者类)。
一个采用@RequestMapping注解的方法将成为一个请求处理方法,并由调度程序在接收到对应URL请求时调用。
下面是一个控制器类,使用@RequestMapping注解了inputCustomer()方法:
package com.example.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class CustomerController { @RequestMapping(value="/input-customer") public String inputCustomer() { //do something here return "CustomerForm"; } }
使用RequestMapping注解的value属性将URL映射到方法。在上面的例子中,我们将input-customer映射到inputCustomer()方法。这样可以使用如下URL访问inputCustomer()方法:
http://domain/context/input-customer
由于value属性是RequestMapping注解的默认属性,因此,若只有唯一的属性,则可以省略属性名称。话句话说,如下两个标注含义相同:
@RequestMapping(value="input-customer")
@RequestMapping("input-customer")
但是有多个属性时,就必须写入value属性名称。
请求映射的值value可以是一个空字符串,此时该方法被映射到以下网址:
http://domain/context
RequestMapping除了具有value属性外,还有其它属性。例如,method属性用来指示该方法仅处理哪些HTTP方法。
例如,仅当在HTTP POST或PUT方法时,才访问到下面的processOrder()方法:
... import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; ... @RequestMapping(value="/process-order", method={RequestMethod.POST,RequestMethod.PUT}) public String processOrder() { //do something here return "OrderForm"; } ...
若method属性只有一个HTTP方法值,则无需花括号,例如:
@RequestMapping(value="/process-order",method=RequestMethod.POST)
如果没有指定method属性值,则请求处理方法可以处理任意HTTP方法。
此外,RequestMapping注解类型也可以用来注解一个控制器类,如下所示:
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; ... @Controller @RequestMapping(value="/customer") public class CustomerController { ... }
在这种情况下,所有的方法都将映射为相对于类级别的请求。例如,下面的deleteCustomer()方法:
... import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; ... @Controller @RequestMapping(value="/customer") public class CustomerController { @RequestMapping(value="/delete", method={RequestMethod.POST,RequestMethod.PUT}) public String deleteCustomer() { //do something here return ...; } }
由于控制器类的映射使用"/customer",而deleteCustomer()方法映射为"/delete",则如下URL方法会映射到deleteCustomer()方法上:
http://domain/context/customer/delete
二 编写请求处理方法
每个请求处理方法可以有多个不同类型的参数,以及一个多种类型的返回结果。例如,如果在请求处理方法中需要访问HttpSession对象,则可以添加HttpSession作为参数,Spring会将对象正确的传递给方法:
@RequestMapping(value="/uri") public String myMethod(HttpSession session) { ... session.addAttribute(key,value); ... }
或者,如果需要访问客户端语言环境和HttpServletRequest对象,则可以在方法签名上包括这样的参数:
@RequestMapping(value="/uri") public String myOtherMethod(HttpServletRequest request,Locale locale) { ... //access Locale and HttpServletRequest here ... }
下面是可以在请求处理方法中出现的参数类型:
- javax.servlet.ServletRequest或javax.servlet.http.HttpServletRequest;
- javax.servlet.ServletResponse或javax.servlet.http.HttpServletResponse;
- javax.servlet.http.HttpSession;
- org.springframework.web.context.request.WebRequest或org.springframework.web.context.request.NativeWebRequest;
- java.util.Locale;
- java.io.InputStream或java.io.Reader;
- java.io.OutputStream或java.io.Writer;
- java.security.Principal;
- HttpEntity<?>paramters;
- org.springframework.ui.Model;
- org.springframework.ui.ModelMap;
- org.springframework.web.servlet.mvc.support.RedirectAttributes.;
- org.springframework.validation.Errors;
- org.springframework.validation.BindingResult;
- 命令或表单对象;
- org.springframework.web.bind.support.SessionStatus;
- org.springframework.web.util.UriComponentsBuilder;
- 带@PathVariable、@MatrixVariable、@RequestParam、@RequestHeader、@RequestBody或@RequestPart注解的对象;
特别重要的是org.springframework.ui.Model类型。这不是一个Servlet API类型,而是一个包含java.util.Map字段的Spring MVC类型。每次调用请求处理方法时,Spring MVC都创建Model对象,用于增加需要显示在视图中的属性,这些属性就可以像被添加到HttpServletRequest那样访问了。。
请求处理方法可以返回如下类型的对象:
- ModelAndView;
- Model;
- 包含模型的属性的Map;
- View;
- 代表逻辑视图名的String;
- void;
- 提供对Servlet的访问,以响应HTTP头部和内容HttpEntity或ReponseEntity对象;
- Callable;
- 其它任意类型,Spring将其视作输出给View的对象模型;
三 应用基于注解的控制器(annotated1应用)
本节将创建一个名为annotated1的应用,展示了包含有两个请求处理方法的一个控制器类。该应用和Spring MVC -- Spring MVC入门中应用的主要区别是:annotated1的控制器类使用了注解@Controller,而不是实现Controller接口。此外,Spring配置文件也增加了一些元素。
1、目录结构
annotated1应用的目录结构如下:
注意:annotated1应用只有一个控制器类,而不是两个。
2、配置文件
annotated1应用由两个配置文件:
- 第一个为部署描述符(web.xml文件)中注册Spring MVC的DispatcherServlet;
- 第二个为springmvc-config.xml,即Spring MVC的配置文件;
部署描述符(web.xml文件):
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/config/springmvc-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
在部署描述符中的<servlert-mapping/>元素,Spring MVC的DispatcherServlet的URL模式设置为"/",因为这所有的请求(包括那些用于静态资源)都被映射到DispatcherServlet。为了正确处理静态资源,需要在Spring MVC配置文件中添加一些<resources/>元素:
Spring MVC配置文件(springmvc-servlet.xml):
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="controller"/> <mvc:annotation-driven/> <mvc:resources mapping="/css/*" location="/css/"/> <mvc:resources mapping="/*.html" location="/"/> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
Spring MVC的配置文件中最主要的是<component-scan/>元素,这是要指示Spring MVC扫描目标包中的控制器类,本例是controller包。
接下来是<annotation-driven/>元素,该元素做了很多事情,其中包括注册用于控制器注解的bean对象;
最后是<resources/>元素,用于指示Spring MVC哪些静态资源需要单独处理(不通过DispatcherServlet)。
在上面的配置中,有两个<resources/>元素。第一个确保在/css目录下的所有文件可见,第二个允许显示所有的/.html文件。
注意:如果没有<annotation-driven/>元素,<resources/>元素会阻止任意控制器被调用。若不需要使用<resources/>元素,则不需要<annotation-driven/>元素元素。
3、Controller类
如前所述,使用Controller注解类型的一个优点在于:一个控制器类可以包含多个请求处理方法。ProductController类中有inputProduct()和saveProduct()两个请求处理方法:
package controller; import java.math.BigDecimal; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import domain.Product; import form.ProductForm; @Controller public class ProductController { private static final Log logger = LogFactory.getLog(ProductController.class); @RequestMapping(value="/input-product") public String inputProduct() { logger.info("inputProduct called"); return "ProductForm"; } @RequestMapping(value="/save-product") public String saveProduct(ProductForm productForm, Model model) { logger.info("saveProduct called"); // no need to create and instantiate a ProductForm // create Product Product product = new Product(); product.setName(productForm.getName()); product.setDescription(productForm.getDescription()); try { product.setPrice(new BigDecimal(productForm.getPrice())); } catch (NumberFormatException e) { } // add product model.addAttribute("product", product); return "ProductDetails"; } }
其中saveProduct()方法的第一个参数是ProductForm类型,这是一个表单类型。Spring MVC会创建一个ProductForm实例,用来接收表单传递过来的参数,这样我们就可以直接获取到表单的数据,然后直接赋值给领域对象product即可。
saveProduct()方法的第二个参数是org.springframework.ui.Model类型。无论是否会使用,Spring MVC都会在每一个请求处理方法被调用时创建一个Model实例,用于增加需要显示在视图中的属性。例如,通过调用model.addAttribute()来增加Product对象:
model.addAttribute("product", product);
Product对象就可以像被添加到HttpServletRequest那样访问了。
其中ProductForm类如下:
package form; public class ProductForm { private String name; private String description; private String price; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getPrice() { return price; } public void setPrice(String price) { this.price = price; } }
Product类如下:
package domain; import java.io.Serializable; import java.math.BigDecimal; public class Product implements Serializable { private static final long serialVersionUID = 5784L; private long id; private String name; private String description; private BigDecimal price; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } }
4、视图
annotated1应用程序包含两个jsp页面:ProductForm.jsp页面和ProductDetails页面:
ProductForm.jsp:
<!DOCTYPE HTML> <html> <head> <title>Add Product Form</title> <style type="text/css">@import url(css/main.css);</style> </head> <body> <div id="global"> <form action="save-product" method="post"> <fieldset> <legend>Add a product</legend> <p> <label for="name">Product Name: </label> <input type="text" id="name" name="name" tabindex="1"> </p> <p> <label for="description">Description: </label> <input type="text" id="description" name="description" tabindex="2"> </p> <p> <label for="price">Price: </label> <input type="text" id="price" name="price" tabindex="3"> </p> <p id="buttons"> <input id="reset" type="reset" tabindex="4"> <input id="submit" type="submit" tabindex="5" value="Add Product"> </p> </fieldset> </form> </div> </body> </html>
ProductDetails.jsp:
<!DOCTYPE HTML> <html> <head> <title>Save Product</title> <style type="text/css">@import url(css/main.css);</style> </head> <body> <div id="global"> <h4>The product has been saved.</h4> <p> <h5>Details:</h5> Product Name: ${product.name}<br/> Description: ${product.description}<br/> Price: $${product.price} </p> </div> </body> </html>
ProductDetails页面通过EL表达式语言访问product对象的各种属性。
main.css:
#global { text-align: left; border: 1px solid #dedede; background: #efefef; width: 560px; padding: 20px; margin: 100px auto; } form { font:100% verdana; min-width: 500px; max-width: 600px; width: 560px; } form fieldset { border-color: #bdbebf; border-width: 3px; margin: 0; } legend { font-size: 1.3em; } form label { width: 250px; display: block; float: left; text-align: right; padding: 2px; } #buttons { text-align: right; } #errors, li { color: red; }
5、测试应用
将项目部署到tomcat服务器,然后启动服务器,假设示例应用运行在本机的8000端口上,则可以通过如下URL访问应用:
http://localhost:8008/annotated1/input-product
会看到如下产品表单页面:
在表单中输入相应的值后单击Add Product按钮,会调用saveProduct()方法,在下一页中看到产品属性:
四 应用@Autowired和@Service进行依赖注入(annotated2应用)
使用Spring 框架的一个好处是容易进行依赖注入。毕竟,Spring框架一开始就是一个依赖注入容器。将依赖注入到Spring MVC控制器的最简单方法是,通过注解@Autowired到字段或方法。Autowired注解类型属于org.springframework.beans.factory.annotation包。
此外,为了能作为依赖注入,类必须要注明为@Service。该类型是org.springframework.stereotype包的成员。Service注解类型指示类是一个服务,此外,在配置文件中,还需要增加一个<component-scan/>元素来扫描依赖基本包。
<context:component-scan base-package="dependencyPackage"/>
下面是以annotated2应用进一步说明Spring MVC如何应用依赖注入。应用的目录结构如下:
注意:这里引入了taglibs-standard-impl-1.2.5.jar包,在后面会介绍到。下载地址:http://tomcat.apache.org/download-taglibs.cgi。
1、视图
annotated2包含三个jsp页面,ProductForm.jsp、ProductDetails.jsp、ProductView.jsp:
ProductForm.jsp:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE HTML> <html> <head> <title>Add Product Form</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <form action="save-product" method="post"> <fieldset> <legend>Add a product</legend> <p> <label for="name">Product Name: </label> <input type="text" id="name" name="name" tabindex="1"> </p> <p> <label for="description">Description: </label> <input type="text" id="description" name="description" tabindex="2"> </p> <p> <label for="price">Price: </label> <input type="text" id="price" name="price" tabindex="3"> </p> <p id="buttons"> <input id="reset" type="reset" tabindex="4"> <input id="submit" type="submit" tabindex="5" value="Add Product"> </p> </fieldset> </form> </div> </body> </html>
ProductDetails.jsp:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE HTML> <html> <head> <title>Save Product</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <h4>The product has been saved.</h4> <p> <h5>Details:</h5> Product Name: ${product.name}<br/> Description: ${product.description}<br/> Price: $${product.price} </p> </div> </body> </html>
ProductView.jsp:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE HTML> <html> <head> <title>View Product</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <h4>${message}</h4> <p> <h5>Details:</h5> Product Name: ${product.name}<br/> Description: ${product.description}<br/> Price: $${product.price} </p> </div> </body> </html>
2、ProductController类
在annotated2应用程序中,ProductController类已经不同于annotated1中的类:
package controller; import java.math.BigDecimal; 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.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import domain.Product; import service.ProductService; import form.ProductForm; @Controller public class ProductController { private static final Log logger = LogFactory .getLog(ProductController.class); //为ProductService字段添加@Autowired注解会使ProductService的一个实例被注入到ProductController实例中 @Autowired private ProductService productService; @RequestMapping(value = "/input-product") public String inputProduct() { logger.info("inputProduct called"); return "ProductForm"; } @RequestMapping(value = "/save-product", method = RequestMethod.POST) public String saveProduct(ProductForm productForm, RedirectAttributes redirectAttributes) { logger.info("saveProduct called"); // no need to create and instantiate a ProductForm // create Product Product product = new Product(); product.setName(productForm.getName()); product.setDescription(productForm.getDescription()); try { product.setPrice(new BigDecimal(productForm.getPrice())); } catch (NumberFormatException e) { } // add product Product savedProduct = productService.add(product); redirectAttributes.addFlashAttribute("message", "The product was successfully added."); return "redirect:/view-product/" + savedProduct.getId(); } @RequestMapping(value = "/view-product/{id}") public String viewProduct(@PathVariable Long id, Model model) { Product product = productService.get(id); model.addAttribute("product", product); return "ProductView"; } }
与annotated1相比,annotated2中的ProductController类做了一系列的调整。首先是在如下的私有字段上增加了@Autowired注解。
@Autowired private ProductService productService;
ProductService 是一个提供各种处理产品的方法的接口。为ProductService字段添加@Autowired注解会使ProductService 的一个实例被注入到ProductController实例中。
此外请求处理方法saveProduct()中使用了重定向和Flash属性,这在后面将会单独介绍。
ProductController还提供了请求处理方法viewProduct(),请求处理方法中使用到了路径变量@PathVariable,这在后面也将会单独介绍。
3、ProductService接口及实现类ProductServiceImpl1
下面是ProductService接口及实现类ProductServiceImpl1的源代码。注意:为了使类能被Spring扫描到,必须为其标注@Service,并需要在配置文件中使用<component-scan/>元素指定服务类的基本包。
ProductService接口:
package service;
import domain.Product;
public interface ProductService {
Product add(Product product);
Product get(long id);
}
可以看到ProductService需要实现两个方法,一个用于新增产品信息,另一个是根据id号获取产品信息。
ProductServiceImpl1类:
package service; import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import org.springframework.stereotype.Service; import domain.Product; @Service public class ProductServiceImpl implements ProductService { private Map<Long, Product> products = new HashMap<Long, Product>(); private AtomicLong generator = new AtomicLong(); public ProductServiceImpl() { Product product = new Product(); product.setName("JX1 Power Drill"); product.setDescription("Powerful hand drill, made to perfection"); product.setPrice(new BigDecimal(129.99)); add(product); } @Override public Product add(Product product) { long newId = generator.incrementAndGet(); product.setId(newId); products.put(newId, product); return product; } @Override public Product get(long id) { return products.get(id); } }
4、配置文件
annotated2应用由两个配置文件:
- 第一个为部署描述符(web.xml文件)中注册Spring MVC的DispatcherServlet;
- 第二个为springmvc-config.xml,即Spring MVC的配置文件;
部署描述符(web.xml文件):
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" > <servlet> <servlet-name>springmvc</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/config/springmvc-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
Spring MVC配置文件(springmvc-servlet.xml):
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="controller"/> <context:component-scan base-package="service"/> <mvc:annotation-driven/> <mvc:resources mapping="/css/*" location="/css/"/> <mvc:resources mapping="/*.html" location="/"/> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
annotated2的Spring MVC配置文件中有两个<component-scan/>元素,一个用于扫描控制器类@Controller,另一个用于扫描服务类@Service。
5、重定向和Flash属性
作为一名经验丰富的servlet/JSP程序员,必须知道转发和重定向的区别。转发比重定向快,因为重定向经过客户端,而转发没有。但是,有时采用重定向更好,若需要重定向到一个外部网站,则无法使用转发。
使用重定向的另一个场景是避免用户重新加载页面时再次调用同样的动作。例如,在annotated1中,当提交产品表单时,saveProduct()方法将被调用,并执行相应的动作。在一个真实的应用程序中,这可能将所述产品加入到数据库中,但是如果在提交表单后重新加载页面,saveProduct()就会再次被调用,同样的产品将可能被再次添加。为了避免这种情况,提交表单后,你可能更愿意将用户重定位到一个不同的页面。这个网页任意重新加载都没有副作用。例如,在annotated1中,可以在提交表单后,将用户重新定义到一个新的ProductView页面。
在annotated2中,ProductController类中的请求处理方法saveProduct()以如下所示行结束:
return "redirect:/view-product/" + savedProduct.getId();
这里,使用重定向而不是转发来防止当用户重新加载页面时,saveProduct()方法被二次调用。
使用重定向的一个不便之处是:无法轻松地传值给目标页面。若采用转发,则可以简单地将属性添加到Model,使用目标视图可以轻松方法。由于重定向经过客户端,所以Model中的一切都在重定向时失去。好在,Spring 3.1版本以及更高版本通过Flash属性提供了一种供重定向传值的方法。
要使用Flash属性,必须在Spring MVC配置文件中有一个 <annotation-driven/>元素,然后,还必须在方法上添加一个新的参数类型org.springframework.web.servlet.mvc.support.RedirectAttributes。具体如下:
@RequestMapping(value = "/save-product", method = RequestMethod.POST) public String saveProduct(ProductForm productForm, RedirectAttributes redirectAttributes) { logger.info("saveProduct called"); // no need to create and instantiate a ProductForm // create Product Product product = new Product(); product.setName(productForm.getName()); product.setDescription(productForm.getDescription()); try { product.setPrice(new BigDecimal(productForm.getPrice())); } catch (NumberFormatException e) { } // add product Product savedProduct = productService.add(product); redirectAttributes.addFlashAttribute("message", "The product was successfully added."); return "redirect:/view-product/" + savedProduct.getId(); }
6、请求参数和路径变量
请求参数和路径变量都可以用于发送值给服务器。二者都是URL的一部分。
1)请求参数
请求参数采用key=value形式,并用"&"分隔。例如,如下URL带有一个名为productId的请求参数,其值为3:
http://localhost:8008/annotated2/view-product?productId=3
在传统的servlet编程中,可以使用HttpServletRequest的getParamter()方法来获取一个请求参数值:
String productId = request.getParameter("productId");
Spring MVC提供了一个更简单的方法来获取请求参数值:使用org.springframework.web.bind.annotation.RequestParam注解类型来注解方法参数。例如:下面的方法包含了一个获取请求参数productId值的参数:
public void sendProduct(@RequestParam int productId)
正如你所看到的,@RequestParam注解的参数类型不一定是字符串。
我们可以在ProductController类中追加如下代码:
//请求参数 请求URL:/request-param?id=5 @RequestMapping(value="/request-param") public String requestParamTest(@RequestParam Long id,Model model) { model.addAttribute("id", id); return "RequestParam"; }
然后在jsp目录下创建文件RequestParam.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>请求传入的参数id:${id}</h1> </body> </html>
部署项目,并在浏览器输入:
http://localhost:8008/annotated2/request-param?id=12223
输出如下:
2)路径变量
路径变量类似请求参数,但是没有key部分,只有一个值。例如:在annotated2中,view-product动作映射到如下URL:
/view-product/productId
其中的productId是表示产品标识符的整数。在Spring MVC中,productId称为路径变量,用来发送一个值到服务器。
ProductController类中的viewProduct()方法演示了一个路径变量的使用:
@RequestMapping(value = "/view-product/{id}") public String viewProduct(@PathVariable Long id, Model model) { Product product = productService.get(id); model.addAttribute("product", product); return "ProductView"; }
为了使用路径变量,首先需要在RequestMapping注解的值属性中添加一个变量,该变量必须放在花括号{}之间。例如,下面的RequestMapping注解定义了一个名为id的路径变量:
@RequestMapping(value = "/view-product/{id}")
然后,在方法签名中添加一个同名变量,并加上@PathVariable 注解。当该方法被调用时,请求URL的id值将被复制到路径变量中,并可以在方法中使用,路径变量的类型可以不是字符串。Spring MVC将尽力转换成一个非字符串类型。这个Spring MVC的强大功能将会在后面的博客中详细介绍。
可以在请求映射中使用多个路径变量,例如,下面定义userId和orderId两个路径变量。
@RequestMapping(value = "/view-product/{userId}/{orderId}")
在浏览器输入如下URL,来测试viewProduct()方法的路径变量:
http://localhost:8008/annotated2/view-product/1
注意:我们每次点击/input-product页面AddProduct都会将产品信息添加到ProductController类的productService实例中(注意这个实例是通过@Autowired和@Service进行依赖注入的,默认是单例的,也就是说Spring容器中只存在一个ProductService类型的实例),这个实例保存着所有产品的标识符以及产品信息。因此可以可以通过更改productId的值取出对应的产品信息。
有时,使用路径变量会遇到一个小问题:在某些情况下,浏览器可能会误解路径变量。考虑如下URL:
http://example.com/context/abc
浏览器会(正确)认为abc是一个动作。任何静态文件路径的解析,如css文件,将使用http://example.com/context作为基本路径。这就是说,若服务器发送的网页包含如下img元素:
<img src="logo.png"/>
该浏览器将试图通过http://example.com/context/logo.png来加载logo.png。
然而,若一个应用程序被部署为默认上下文(默认上下文路径就是一个空字符串),则对于同一个目标的URL,会是这样的:
http://example.com/abc
下面是带有路径变量的URL:
http://example.com/abc/a
在这种情况下,浏览器会认为abc是上下文,没有动作。如果在页面中使用<img src="logo.png"/>,浏览器将试图通过http://example.com/abc/logo.png来加载图像,并且它将找不到该图像。
好在,我们有一个简单的解决方案,即通过使用JSTL标记的URL(后面博客会详细介绍JSTL)。要使用JSTL标记,我们必须在JSP开头处声明这个taglib指令:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
注意:这里需要引入taglibs-standard-impl-1.2.5.jar包。下载地址:http://tomcat.apache.org/download-taglibs.cgi。
标签会通过正确解析URL来修复该问题。例如annotated2中所有的JSP页面导入的所有CSS,从:
<style type="text/css">@import url(css/main.css);</style>
修改为:
<style type="text/css">@import url("<c:url value="/css/main.css"/>");</style>
若程序部署不在默认上下文,链接标签会将URL转换成如下所示形式:
<style type="text/css">@import url("/css/main.css");</style>
若程序部署为默认上下文,则会被转换成如下所示形式:
<style type="text/css">@import url("/annotated2/css/main.css");</style>
五 @ModelAttribute
前面谈到的Spring MVC在每次调用请求处理方法时,都会创建Model类型的一个实例。若打算使用该实例,则可以在方法中添加一个Model类型的参数。
//路径变量 @RequestMapping(value = "/view-product/{id}") public String viewProduct(@PathVariable Long id, Model model) { Product product = productService.get(id); model.addAttribute("product", product); return "ProductView"; }
事实上,还可以使用在方法中添加ModelAttribute注解类型来访问Model实例,该注解类型也是org.springframework.web.bind.annotation包的成员。
1)可以用@ModelAttribute来注解方法参数。带@ModelAttribute注解的方法参数会将该参数对象添加到Model对象(若方法中没有显示添加)。例如,Spring MVC将在每次调用submitOrder()方法时创建一个Order实例。
@RequestMapping(method=RequestMethod.POST) public String submitOrder(@ModelAttribute("@newOrder") Order order,Model model) { ... return ...; }
输入或创建的Order实例将用”newOrder“键值添加到Model对象中。如果未定义键值名,则将使用该对象类型的名称。例如,每次调用如下方法,会使用键值”Order“将Order实例添加到Model对象中。
public String submitOrder(@ModelAttribute Order order,Model model)
2)@ModelAttribute的第二个用途是标注一个非请求的处理方法。被@ModelAttribute注解的方法会在每次调用该控制器类的请求处理方法时被调用。这意味着,如果一个控制器有两个请求处理方法,以及一个@ModelAttribute注解的方法,该方法的调用次数就会比每个请求处理方法更频繁。
Spring MVC会在调用请求处理方法之前调用带@ModelAttribute注解的方法,@ModelAttribute注解的方法可以返回一个对象或一个void类型。如果返回一个对象,则返回对象会自动的添加到Model中。
@ModelAttribute public Product addProduct(@RequestParam String productId) { return productService.get(productId); }
若方法返回void,则还必须添加一个Model类型的参数,并自动将实例添加到Model中,如下面的例子所示:
@ModelAttribute public void populateModel(@RequestParam String id,Model model) { model.addAttribute(new Account(id)); }
参考文章
[1]Spring MVC学习指南