使用@Autowired和@Service进行依赖注入
使用Spring框架的一个好处是容易进行依赖注入,将依赖注入到Spring MVC控制器的最简单的方法是通过注解@Autowired到字段或者方法。Autowired注解类型属于org.springframework.beans.factory.annotation包
分两步
- 在配置文件中添加context:component-scan元素扫描依赖基本包
- 标注注解,如果是服务层,标注@Service
我们新建个maven工程来演示该功能
首先我们先看下Controller类
package com.artisan.springmvc.controller; import org.apache.log4j.Logger; 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 com.artisan.springmvc.domain.Product; import com.artisan.springmvc.form.ProductForm; import com.artisan.springmvc.service.ProductService; @Controller public class ProductController { private static final Logger logger = Logger.getLogger(ProductController.class); @Autowired private ProductService productService; @RequestMapping(value = "/product_input") public String inputProduct() { logger.info("inputProduct called ...."); return "ProductForm"; } @RequestMapping(value = "/product_save", method = RequestMethod.POST) public String saveProduct(ProductForm productForm, RedirectAttributes attributes) { 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(Float.parseFloat(productForm.getPrice())); } catch (NumberFormatException e) { } // add product Product savedProduct = productService.add(product); attributes.addFlashAttribute("message", "The product has been saved successfully"); return "redirect:/product_view/" + savedProduct.getId(); } /** * * @param id * @param model * @return * @PathVariable用来获得请求url中的动态参数 */ @RequestMapping(value = "/product_view/{id}") public String getProductById(@PathVariable Long id, Model model) { logger.info("getProductById called ...."); Product product = productService.get(id); model.addAttribute("product", product); return "ProductView"; } }
该ProductController类与上个例子中的ProductController类相比,做了一些调整
1. 首先自动注入了ProductService ,加了@AutoWired注解
private ProductService productService; public ProductService getProductService() { return productService; } @Autowired public void setProductService(ProductService productService) { this.productService = productService; }
或者
@Autowired private ProductService productService;
productService是一个提供跟踪处理产品方法的接口,为productService字段或者set方法注入@Autowired会使ProductService的一个实例被注入到ProductController实例中。
为了使类能够被Spring扫描到,必须要标注@Service
ProductService接口
package com.artisan.springmvc.service; import com.artisan.springmvc.domain.Product; public interface ProductService { Product add(Product product); Product get(long id); }
ProductServiceImpl类
package com.artisan.springmvc.service; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import org.springframework.stereotype.Service; import com.artisan.springmvc.domain.Product; /** * * @author Mr.Yang * 标注了@Service的服务层 * */ @Service public class ProductServiceImpl implements ProductService { private Map<Long, Product> products = new HashMap<Long, Product>(); // 使用AtomicLong能让long的操作在多线程下保持原子型 private AtomicLong generator = new AtomicLong(); public ProductServiceImpl() { Product product = new Product(); 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); } }
2. 然后在SpringMVC配置文件中有两个component-scan元素,一个是用于扫描控制器类,另一个新增加的为扫描服务类
<?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="com.artisan.springmvc.controller"/> <context:component-scan base-package="com.artisan.springmvc.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>
重定向和Flash属性
经常写Servlet/JSP的童鞋都知道转发和重定向的区别
转发比重定向要快,因为转发不经过客户端,而重定向会经过客户端。但是有时候采用重定向会更好,比如需要重定向到一个外部网站,则无法使用转发
另外一个使用重定向的场景是避免在用户重新加载页面的时候再次调用相同的动作 ,比如,这个示例中, 当提交产品表单时,saveProduct方法会被调用,并执行相应的动作。在真实应用中,这些所述产品会加入到数据库中。但是如果提交表单后重新加载页面,saveProduct会被再此调用,同样的产品可能被再此添加。为了避免这种情况,提交表单后,你可能更愿意将用户重定向到一个不同的页面。这个网页任意加载都没有副作用。我们这个示例中,提交表单后,将用户重定向到一个ViewProduct页面.
在这个示例中,ProductController#saveProduct方法 返回
return "redirect:/product_view/" + savedProduct.getId();
这里使用重定向,而不是转发来防止当用户重新加载页面时,saveProduct被二次调用。
使用重定向有个不方便的地方:无法轻松的传值给目标页面,而转发则可以简单的将属性添加到Model中,使目标页面轻松访问。由于重定向经过客户端,所以Model中的一切都在重定向时丢失了。 幸运的是Spring3.1版本及更高的版本通过Flash属性提供了一种重定向传值的方法
要使用Flash属性,必须在Spring MVC的配置文件中有一个<mvc:annotation-driven/>元素,然后,还必须在方法上添加一个新的参数类型 org.springframework.web.servlet.mvc.support.RedirectAttributes
如下所示
请求参数和路径变量
获取请求参数
请求参数和路径变量都可以用于发送值给服务器,二者都是URL的一部分。 请求参数采用key=value形式,并用&分割。
比如
http://localhost:8080/chapter04b/productRetrive?productId=5
在传统的Servlet编程中,可以使用HttpServletRequest的getParameter方法来获取一个请求参数值
String productId=httpServletRequest.getParameter("productId");
Spring MVC则提供了一个更简单的方法来获取请求参数的值:org.springframework.web.bind.annotation.RequestParam注释类型来获取注释方法参数,比如
public void sendProduct(@RequestParam int productId)
获取路径变量
路径变量类似请求参数,但是没有key部分,只有一个值, 比如我们这个示例中 product_view动作映射到如下URL
/product_view/productId
其中productId表示产品标识符的整数。在SpringMVC中,productId被称作路径变量,用来发送一个值到服务器
接下来我们看下viewProduct方法演示了一个路径变量的使用
/** * * @param id * @param model * @return * @PathVariable用来获得请求url中的动态参数 */ @RequestMapping(value = "/product_view/{id}") public String viewProduct(@PathVariable Long id, Model model) { logger.info("getProductById called ...."); Product product = productService.get(id); model.addAttribute("product", product); return "ProductView"; }
为了使用路径变量,首先需要在RequestMapping注解的值属性中添加一个变量,该变量必须放在花括号之间,例如下面的RequestMapping注解定义一个名为id的路径变量
@RequestMapping(value = "/product_view/{id}")
然后在方法签名中添加一个同名变量,并添加上@PathVariable注解。 当viewProduct方法别调用时,请求URL的id值将被复制到路径变量中,并可以在方法中使用。 路径变量的类型可以不是字符串,Spring MVC将尽量转换为非字符串类型,这个强大的功能,后续在数据绑定和表单参数中详解。
可以在请求映射中使用多个路径变量,比如userId和orderId两个路径变量
@RequestMapping(value="/produtc_view/{userId}/{orderId}")
直接在浏览器中输入URL,来测试viewProduct方法的路径变量
http://localhost:8080/chapter04b/product_view/5
使用路径变量有可能出现的问题
有时候,使用路径变量会遇到一个小问题:在某些情况下,浏览器可能会误解路径变量。 考虑下面的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/1
这种情况下,浏览器会认为abc是上下文,没有动作。 如果在页面中使用<img src='logo.png'/>,浏览器将试图通过http://example.com/abc/logo.png来寻找图片,并且它将找不到图片。
幸运的是,我们有个简单的解决方法,即通过JSTL标记的URL。 标签会通过正确解析URL来修复该问题。
比如我们该案例中的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("<c:url value="/chapter04b/css/main.css"/>");</style>
我们这个示例中的上下文为chapter04b , 通过f12查看如下方式加载css
http://localhost:8080/chapter04b/css/main.css
如果写成@import url(css/main.css);将加载不到css的样式。
@ModelAttribute
前面讲到Spring MVC在每次调用请求处理方法时,都会创建Model类型的一个实例。若打算使用该实例,则可以在方法中添加一个Model类型的参数。事实上还可以使用在方法中添加ModelAttribute注释类型来访问Model实例。 该注释类型也是org.springframework.web.bind.annotation包的成员。
@ModelAttribute的第一个用途
可以用@ModelAttribute来注释方法参数或者方法。
带@ModelAttribute注解的方法会将其输入的或创建的参数对象添加到Model对象中(若方法中没有显式添加)。
比如,Spring MVC将在每次调用submitOrder方法时创建一个Order实例
@RequestMapping(value="/submitOrder",method=RequestMethod.Post) public String submitOrder(@ModelAttribute("newOrder") Order order,Model model){ ... }
输入或者创建的Order实例将用newOrder键值添加到Model对象中,如果未定义键值名,则使用该对象类型的名称。
比如,每次调用如下方法,会使用键值order将Order实例添加到Model对象中
@RequestMapping(value="/submitOrder",method=RequestMethod.Post) public String submitOrder(@ModelAttribute Order order,Model model){ ... }
@ModelAttribute的第二个用途
@ModelAttribute的第二个用途是标注一个非请求的处理方法。 被@ModelAttribute注释的方法会在每次调用该控制器类的请求处理方法时被调用。
这就意味着,如果一个控制器类有两个请求处理方法,以及一个带有@ModelAttribute注解的方法,该方法的调用次数就会比每个处理请求方法更加频繁。
Spring MVC会在调用请求处理方法之前调用带有@ModelAttribute注解的方法,带@ModelAttribute注解的方法可以返回一个对象或者一个void类型,
如果返回一个对象,则返回对象会自动添加到Model中
@ModelAttribute public Product addProduct(@RequestParam String productId){ return productService.get(produtId) }
若返回的是void,这还必须添加一个Model类型的参数,并自行将实例添加到Model中
@ModelAttribute public void populateModel(@RequestParam String id,Model model){ model.addAttribute(new Account(id)); }
总结
这里介绍了如何编写基于注解的控制器的Spring MVC应用,也讲解了各种注解类、方法或者方法的参数的注释类型