本节书摘来自异步社区《Spring MVC学习指南(第2版)》一书中的第2章,第2.6节,作者:【美】Paul Deck著,更多章节内容可以访问云栖社区“异步社区”公众号查看
在过去数年间,依赖注入技术作为代码可测试性的一个解决方案已经广泛应用。实际上,Spring、Struts2等伟大框架都采用了依赖注入技术。那么,什么是依赖注入技术?
有两个组件A和B,A依赖于B。假定A是一个类,且A有一个方法importantMethod使用到了B,如下:
public class A { public void importantMethod() { B b = ... // get an instance of B b.usefulMethod(); ... } ... }要使用B,类A必须先获得组件B的实例引用。若B是一个具体类,则可通过new关键字直接创建组件B实例。但是,如果B是接口,且有多个实现,则问题就变得复杂了。我们固然可以任意选择接口B的一个实现类,但这也意味着A的可重用性大大降低了,因为无法采用B的其他实现。
示例appdesign4使用了一个自制依赖注入器。在现实世界的应用程序中,应该使用Spring。
示例应用程序用来生成PDF。它有两个动作,form和pdf。 第一个没有action类,只是转发到可以用来输入一些文本的表单;第二个生成PDF文件并使用PDFAction类,操作类本身依赖于生成PDF的服务类。
PDFAction和PDFService类分别见清单2.11和清单2.12。
清单2.11 PDFAction类
package action; import service.PDFService; public class PDFAction { private PDFService pdfService; public void setPDFService(PDFService pdfService) { this.pdfService = pdfService; } public void createPDF(String path, String input) { pdfService.createPDF(path, input); } }清单2.12 PDFService类
package service; import util.PDFUtil; public class PDFService { public void createPDF(String path, String input) { PDFUtil.createDocument(path, input); } }PDFService使用了PDFUtil类,PDFUtil最终采用了Apache PDFBOx库来创建PDF文档,如果对创建PDF的具体代码有兴趣,可以进一步查看PDFUtil类。
这里的关键在于,如代码2.11所示,PDFAction需要一个PDFService来完成它的工作。换句话说,PDFAction依赖于PDFService。没有依赖注入,你必须在PDFAction类中实例化PDFService类,这将使PDFAction更不可测试。除此之外,如果需要更改PDFService的实现,你必须重新编译PDFAction。
使用依赖注入,每个组件都有注入它的依赖项,这使得测试每个组件更容易。对于在依赖注入环境中使用的类,你必须使其支持注入。一种方法是为每个依赖关系创建一个set方法。例如,PDFAction类有一个setPDFService方法,可以调用它来传递PDFService。注入也可以通过构造方法或类属性进行。
一旦所有的类都支持注入,你可以选择一个依赖注入框架并将它导入你的项目。Spring框架、Google Guice、Weld和PicoContainer是一些好的选择。
注意
依赖注入的Java规范是JSR 330和JSR 299appdesign4程序使用DependencyInjector类(见清单2.13)来替代依赖注入框架(在现实世界的应用程序中,你会使用一个合适的框架)。这个类专为appdesign4应用设计,可以容易地实例化。一旦实例化,必须调用其start方法来执行初始化,使用后,应调用其shutdown方法以释放资源。在此示例中,start和shutdown都为空。
清单2.13 DependencyInjector类
package util; import action.PDFAction; import service.PDFService; public class DependencyInjector { public void start() { // initialization code } public void shutDown() { // clean-up code } /* * Returns an instance of type. type is of type Class * and not String because it's easy to misspell a class name */ public Object getObject(Class type) { if (type == PDFService.class) { return new PDFService(); } else if (type == PDFAction.class) { PDFService pdfService = (PDFService) getObject(PDFService.class); PDFAction action = new PDFAction(); action.setPDFService(pdfService); return action; } return null; } }要从DependencyInjector获取对象,须调用其getObject方法,并传递目标对象的Class。 DependencyInjector支持两种类型,即PDFAction和PDFService。例如,要获取PDFAction的实例,你将通过传递PDFAction.class来调用getObject:
PDFAction pdfAction =(PDFAction)dependencyInjector.getObject(PDFAction.class);DependencyInjector(和所有依赖注入框架)的优雅之处在于它返回的对象注入了依赖。如果返回的对象所依赖的对象也有依赖,则所依赖的对象也会注入其自身的依赖。例如,从DependencyInjector获取的PDFAction已包含PDFService。无需在PDFAction类中自己创建PDFService。
appdesign4中的servlet控制器如清单2.14所示。请注意,它在其init方法中实例化DependencyInjector,并在其destroy方法中调用DependencyInjector的shutdown方法。 servlet不再创建它自己的依赖,相反,它从DependencyInjector获取这些依赖。
清单2.14 appdesign4中ControllerServlet
package servlet; import action.PDFAction; import java.io.IOException; import javax.servlet.ReadListener; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import util.DependencyInjector; @WebServlet(name = "ControllerServlet", urlPatterns = { "/form", "/pdf"}) public class ControllerServlet extends HttpServlet { private static final long serialVersionUID = 6679L; private DependencyInjector dependencyInjector; @Override public void init() { dependencyInjector = new DependencyInjector(); dependencyInjector.start(); } @Override public void destroy() { dependencyInjector.shutDown(); } protected void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ReadListener r = null; String uri = request.getRequestURI(); /* * uri is in this form: /contextName/resourceName, * for example: /app10a/product_input. * However, in the case of a default context, the * context name is empty, and uri has this form * /resourceName, e.g.: /pdf */ int lastIndex = uri.lastIndexOf("/"); String action = uri.substring(lastIndex + 1); if ("form".equals(action)) { String dispatchUrl = "/jsp/Form.jsp"; RequestDispatcher rd = request.getRequestDispatcher(dispatchUrl); rd.forward(request, response); } else if ("pdf".equals(action)) { HttpSession session = request.getSession(true); String sessionId = session.getId(); PDFAction pdfAction = (PDFAction) dependencyInjector .getObject(PDFAction.class); String text = request.getParameter("text"); String path = request.getServletContext() .getRealPath("/result") + sessionId + ".pdf"; pdfAction.createPDF(path, text); // redirect to the new pdf StringBuilder redirect = new StringBuilder(); redirect.append(request.getScheme() + "://"); redirect.append(request.getLocalName()); int port = request.getLocalPort(); if (port != 80) { redirect.append(":" + port); } String contextPath = request.getContextPath(); if (!"/".equals(contextPath)) { redirect.append(contextPath); } redirect.append("/result/" + sessionId + ".pdf"); response.sendRedirect(redirect.toString()); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { process(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { process(request, response); } }ervlet支持两种URL模式,form和pdf。 对于表单模式,servlet简单地转发到表单。 对于pdf模式,servlet使用PDFAction并调用其createDocument方法。此方法有两个参数:文件路径和文本输入。所有PDF存储在应用程序目录下的result目录中,用户的会话标识符用做文件名,而文本输入作为PDF文件的内容;最后,重定向到生成的PDF文件。以下是创建重定向URL并将浏览器重定向到新URL的代码:
// redirect to the new pdf StringBuilder redirect = new StringBuilder(); redirect.append(request.getScheme() + "://"); //http or https redirect. append(request.getLocalName()); // the domain int port = request.getLocalPort(); if (port != 80) { redirect.append(":" + port); } String contextPath = request.getContextPath(); if (!"/".equals(contextPath)) { redirect.append(contextPath); } redirect.append("/result/" + sessionId + ".pdf"); response.sendRedirect(redirect.toString());现在访问如下URL来测试appdesign4应用。
http://localhost:8080/appdesign4/form应用将展示一个表单(见图2.7)。
图2.7 PDF表单
如果在文本字段中输入一些内容并按提交按钮,服务器将创建一个PDF文件并发送重定向到浏览器(见图2.8)。
图2.8 PDF文件
请注意,重定向网址将采用此格式。
http://localhost:8080/appdesign4/result/sessionId.pdf由于依赖注入器,appdesign4中的每个组件都可以独立测试。例如,可以运行清单2.15中的PDFActionTest类来测试类的createDocument方法。
清单2.15 PDFActionTest类
package test; import action.PDFAction; import util.DependencyInjector; public class PDFActionTest { public static void main(String[] args) { DependencyInjector dependencyInjector = new DependencyInjector(); dependencyInjector.start(); PDFAction pdfAction = (PDFAction) dependencyInjector.getObject( PDFAction.class); pdfAction.createPDF("/home/janeexample/Downloads/1.pdf", "Testing PDFAction...."); dependencyInjector.shutDown(); } }如果你使用的是Java 7 EE容器,如Glassfish,可以让容器注入对servlet的依赖。 应用appdesign4中的servlet将如下所示:
public class ControllerServlet extends HttpServlet { @Inject PDFAction pdfAction; ... @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { ... } @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { ... } } 相关资源:Spring MVC学习指南 高清完整.pdf版下载