http://www.ibm.com/developerworks/cn/java/j-lo-spring-utils1/html
文件资源的操做是应用程序中常见的功能,如当上传一个文件后将其保存在特定目录下,从指定地址加载一个配置文件等等。咱们通常使用 JDK 的 I/O 处理类完成这些操做,但对于通常的应用程序来讲,JDK 的这些操做类所提供的方法过于底层,直接使用它们进行文件操做不但程序编写复杂并且容易产生错误。相比于 JDK 的 File,Spring 的 Resource 接口(资源概念的描述接口)抽象层面更高且涵盖面更广,Spring 提供了许多方便易用的资源操做工具类,它们大大下降资源操做的复杂度,同时具备更强的普适性。这些工具类不依赖于 Spring 容器,这意味着您能够在程序中象通常普通类同样使用它们。java
Spring 定义了一个 org.springframework.core.io.Resource 接口,Resource 接口是为了统一各类类型不一样的资源而定义的,Spring 提供了若干 Resource 接口的实现类,这些实现类能够轻松地加载不一样类型的底层资源,并提供了获取文件名、URL 地址以及资源内容的操做方法。web
访问文件资源spring
假设有一个文件地位于 Web 应用的类路径下,您能够经过如下方式对这个文件资源进行访问:sql
相比于经过 JDK 的 File 类访问文件资源的方式,Spring 的 Resource 实现类无疑提供了更加灵活的操做方式,您能够根据状况选择适合的 Resource 实现类访问资源。下面,咱们分别经过 FileSystemResource 和 ClassPathResource 访问同一个文件资源:express
package com.baobaotao.io; import java.io.IOException; import java.io.InputStream; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; public class FileSourceExample { public static void main(String[] args) { try { String filePath = "D:/masterSpring/chapter23/webapp/WEB-INF/classes/conf/file1.txt"; // ① 使用系统文件路径方式加载文件 Resource res1 = new FileSystemResource(filePath); // ② 使用类路径方式加载文件 Resource res2 = new ClassPathResource("conf/file1.txt"); InputStream ins1 = res1.getInputStream(); InputStream ins2 = res2.getInputStream(); System.out.println("res1:"+res1.getFilename()); System.out.println("res2:"+res2.getFilename()); } catch (IOException e) { e.printStackTrace(); } } }
在获取资源后,您就能够经过 Resource 接口定义的多个方法访问文件的数据和其它的信息:如您能够经过 getFileName() 获取文件名,经过 getFile() 获取资源对应的 File 对象,经过 getInputStream() 直接获取文件的输入流。此外,您还能够经过 createRelative(String relativePath) 在资源相对地址上建立新的资源。apache
在 Web 应用中,您还能够经过 ServletContextResource 以相对于 Web 应用根目录的方式访问文件资源,以下所示:数组
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <jsp:directive.page import=" org.springframework.web.context.support.ServletContextResource"/> <jsp:directive.page import="org.springframework.core.io.Resource"/> <% // ① 注意文件资源地址以相对于 Web 应用根路径的方式表示 Resource res3 = new ServletContextResource(application, "/WEB-INF/classes/conf/file1.txt"); out.print(res3.getFilename()); %>
对于位于远程服务器(Web 服务器或 FTP 服务器)的文件资源,您则能够方便地经过 UrlResource 进行访问。缓存
为了方便访问不一样类型的资源,您必须使用相应的 Resource 实现类,是否能够在不显式使用 Resource 实现类的状况下,仅根据带特殊前缀的资源地址直接加载文件资源呢? Spring 提供了一个 ResourceUtils 工具类,它支持“classpath:”和“file:”的地址前缀,它可以从指定的地址加载文件资源,请看下面的例子:安全
package com.baobaotao.io; import java.io.File; import org.springframework.util.ResourceUtils; public class ResourceUtilsExample { public static void main(String[] args) throws Throwable{ File clsFile = ResourceUtils.getFile("classpath:conf/file1.txt"); System.out.println(clsFile.isFile()); String httpFilePath = "file:D:/masterSpring/chapter23/src/conf/file1.txt"; File httpFile = ResourceUtils.getFile(httpFilePath); System.out.println(httpFile.isFile()); } }
ResourceUtils 的 getFile(String resourceLocation) 方法支持带特殊前缀的资源地址,这样,咱们就能够在不和 Resource 实现类打交道的状况下使用 Spring 文件资源加载的功能了。
本地化文件资源
本地化文件资源是一组经过本地化标识名进行特殊命名的文件,Spring 提供的 LocalizedResourceHelper 容许经过文件资源基名和本地化实体获取匹配的本地化文件资源并以 Resource 对象返回。假设在类路径的 i18n 目录下,拥有一组基名为 message 的本地化文件资源,咱们经过如下实例演示获取对应中国大陆和美国的本地化文件资源:
package com.baobaotao.io; import java.util.Locale; import org.springframework.core.io.Resource; import org.springframework.core.io.support.LocalizedResourceHelper; public class LocaleResourceTest { public static void main(String[] args) { LocalizedResourceHelper lrHalper = new LocalizedResourceHelper(); // ① 获取对应美国的本地化文件资源 Resource msg_us = lrHalper.findLocalizedResource("i18n/message", ".properties", Locale.US); // ② 获取对应中国大陆的本地化文件资源 Resource msg_cn = lrHalper.findLocalizedResource("i18n/message", ".properties", Locale.CHINA); System.out.println("fileName(us):"+msg_us.getFilename()); System.out.println("fileName(cn):"+msg_cn.getFilename()); } }
虽然 JDK 的 java.util.ResourceBundle 类也能够经过类似的方式获取本地化文件资源,可是其返回的是 ResourceBundle 类型的对象。若是您决定统一使用 Spring 的 Resource 接表征文件资源,那么 LocalizedResourceHelper 就是获取文件资源的很是适合的帮助类了。
在使用各类 Resource 接口的实现类加载文件资源后,常常须要对文件资源进行读取、拷贝、转存等不一样类型的操做。您能够经过 Resource 接口所提供了方法完成这些功能,不过在大多数状况下,经过 Spring 为 Resource 所配备的工具类完成文件资源的操做将更加方便。
文件内容拷贝
第一个咱们要认识的是 FileCopyUtils,它提供了许多一步式的静态操做方法,可以将文件内容拷贝到一个目标 byte[]、String 甚至一个输出流或输出文件中。下面的实例展现了 FileCopyUtils 具体使用方法:
package com.baobaotao.io; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileReader; import java.io.OutputStream; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.util.FileCopyUtils; public class FileCopyUtilsExample { public static void main(String[] args) throws Throwable { Resource res = new ClassPathResource("conf/file1.txt"); // ① 将文件内容拷贝到一个 byte[] 中 byte[] fileData = FileCopyUtils.copyToByteArray(res.getFile()); // ② 将文件内容拷贝到一个 String 中 String fileStr = FileCopyUtils.copyToString(new FileReader(res.getFile())); // ③ 将文件内容拷贝到另外一个目标文件 FileCopyUtils.copy(res.getFile(), new File(res.getFile().getParent()+ "/file2.txt")); // ④ 将文件内容拷贝到一个输出流中 OutputStream os = new ByteArrayOutputStream(); FileCopyUtils.copy(res.getInputStream(), os); } }
每每咱们都经过直接操做 InputStream 读取文件的内容,可是流操做的代码是比较底层的,代码的面向对象性并不强。经过 FileCopyUtils 读取和拷贝文件内容易于操做且至关直观。如在 ① 处,咱们经过 FileCopyUtils 的 copyToByteArray(File in) 方法就能够直接将文件内容读到一个 byte[] 中;另外一个可用的方法是 copyToByteArray(InputStream in),它将输入流读取到一个 byte[] 中。
若是是文本文件,您可能但愿将文件内容读取到 String 中,此时您可使用 copyToString(Reader in) 方法,如 ② 所示。使用 FileReader 对 File 进行封装,或使用 InputStreamReader 对 InputStream 进行封装就能够了。
FileCopyUtils 还提供了多个将文件内容拷贝到各类目标对象中的方法,这些方法包括:
方法 | 说明 |
---|---|
static void copy(byte[] in, File out) |
将 byte[] 拷贝到一个文件中 |
static void copy(byte[] in, OutputStream out) |
将 byte[] 拷贝到一个输出流中 |
static int copy(File in, File out) |
将文件拷贝到另外一个文件中 |
static int copy(InputStream in, OutputStream out) |
将输入流拷贝到输出流中 |
static int copy(Reader in, Writer out) |
将 Reader 读取的内容拷贝到 Writer 指向目标输出中 |
static void copy(String in, Writer out) |
将字符串拷贝到一个 Writer 指向的目标中 |
在实例中,咱们虽然使用 Resource 加载文件资源,但 FileCopyUtils 自己和 Resource 没有任何关系,您彻底能够在基于 JDK I/O API 的程序中使用这个工具类。
属性文件操做
咱们知道能够经过 java.util.Properties 的 load(InputStream inStream) 方法从一个输入流中加载属性资源。Spring 提供的 PropertiesLoaderUtils 容许您直接经过基于类路径的文件地址加载属性资源,请看下面的例子:
package com.baobaotao.io; import java.util.Properties; import org.springframework.core.io.support.PropertiesLoaderUtils; public class PropertiesLoaderUtilsExample { public static void main(String[] args) throws Throwable { // ① jdbc.properties 是位于类路径下的文件 Properties props = PropertiesLoaderUtils.loadAllProperties("jdbc.properties"); System.out.println(props.getProperty("jdbc.driverClassName")); } }
通常状况下,应用程序的属性文件都放置在类路径下,因此 PropertiesLoaderUtils 比之于 Properties#load(InputStream inStream) 方法显然具备更强的实用性。此外,PropertiesLoaderUtils 还能够直接从 Resource 对象中加载属性资源:
方法 | 说明 |
---|---|
static Properties loadProperties(Resource resource) |
从 Resource 中加载属性 |
static void fillProperties(Properties props, Resource resource) |
将 Resource 中的属性数据添加到一个已经存在的 Properties 对象中 |
特殊编码的资源
当您使用 Resource 实现类加载文件资源时,它默认采用操做系统的编码格式。若是文件资源采用了特殊的编码格式(如 UTF-8),则在读取资源内容时必须事先经过 EncodedResource 指定编码格式,不然将会产生中文乱码的问题。
package com.baobaotao.io; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; import org.springframework.util.FileCopyUtils; public class EncodedResourceExample { public static void main(String[] args) throws Throwable { Resource res = new ClassPathResource("conf/file1.txt"); // ① 指定文件资源对应的编码格式(UTF-8) EncodedResource encRes = new EncodedResource(res,"UTF-8"); // ② 这样才能正确读取文件的内容,而不会出现乱码 String content = FileCopyUtils.copyToString(encRes.getReader()); System.out.println(content); } }
EncodedResource 拥有一个 getResource() 方法获取 Resource,但该方法返回的是经过构造函数传入的原 Resource 对象,因此必须经过 EncodedResource#getReader() 获取应用编码后的 Reader 对象,而后再经过该 Reader 读取文件的内容。
您几乎老是使用 Spring 框架开发 Web 的应用,Spring 为 Web 应用提供了不少有用的工具类,这些工具类能够给您的程序开发带来不少便利。在这节里,咱们将逐一介绍这些工具类的使用方法。
当您在控制器、JSP 页面中想直接访问 Spring 容器时,您必须事先获取 WebApplicationContext 对象。Spring 容器在启动时将 WebApplicationContext 保存在 ServletContext 的属性列表中,经过 WebApplicationContextUtils 工具类能够方便地获取 WebApplicationContext 对象。
WebApplicationContextUtils
当 Web 应用集成 Spring 容器后,表明 Spring 容器的 WebApplicationContext 对象将以 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 为键存放在 ServletContext 属性列表中。您固然能够直接经过如下语句获取 WebApplicationContext:
WebApplicationContext wac = (WebApplicationContext)servletContext. getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
但经过位于 org.springframework.web.context.support 包中的 WebApplicationContextUtils 工具类获取 WebApplicationContext 更方便:
WebApplicationContext wac =WebApplicationContextUtils. getWebApplicationContext(servletContext);
当 ServletContext 属性列表中不存在 WebApplicationContext 时,getWebApplicationContext() 方法不会抛出异常,它简单地返回 null。若是后续代码直接访问返回的结果将引起一个 NullPointerException 异常,而 WebApplicationContextUtils 另外一个 getRequiredWebApplicationContext(ServletContext sc) 方法要求 ServletContext 属性列表中必定要包含一个有效的 WebApplicationContext 对象,不然立刻抛出一个 IllegalStateException 异常。咱们推荐使用后者,由于它能提早发现错误的时间,强制开发者搭建好必备的基础设施。
WebUtils
位于 org.springframework.web.util 包中的 WebUtils 是一个很是好用的工具类,它对不少 Servlet API 提供了易用的代理方法,下降了访问 Servlet API 的复杂度,能够将其当作是经常使用 Servlet API 方法的门面类。
下面这些方法为访问 HttpServletRequest 和 HttpSession 中的对象和属性带来了方便:
方法 | 说明 |
---|---|
Cookie getCookie(HttpServletRequest request, String name) |
获取 HttpServletRequest 中特定名字的 Cookie 对象。若是您须要建立 Cookie, Spring 也提供了一个方便的 CookieGenerator 工具类; |
Object getSessionAttribute(HttpServletRequest request, String name) |
获取 HttpSession 特定属性名的对象,不然您必须经过 request.getHttpSession.getAttribute(name) 完成相同的操做; |
Object getRequiredSessionAttribute(HttpServletRequest request, String name) |
和上一个方法相似,只不过强制要求 HttpSession 中拥有指定的属性,不然抛出异常; |
String getSessionId(HttpServletRequest request) |
获取 Session ID 的值; |
void exposeRequestAttributes(ServletRequest request, Map attributes) |
将 Map 元素添加到 ServletRequest 的属性列表中,当请求被导向(forward)到下一个处理程序时,这些请求属性就能够被访问到了; |
此外,WebUtils 还提供了一些和 ServletContext 相关的方便方法:
方法 | 说明 |
---|---|
String getRealPath(ServletContext servletContext, String path) |
获取相对路径对应文件系统的真实文件路径; |
File getTempDir(ServletContext servletContext) |
获取 ServletContex 对应的临时文件地址,它以 File 对象的形式返回。 |
下面的片段演示了使用 WebUtils 从 HttpSession 中获取属性对象的操做:
protected Object formBackingObject(HttpServletRequest request) throws Exception { UserSession userSession = (UserSession) WebUtils.getSessionAttribute(request, "userSession"); if (userSession != null) { return new AccountForm(this.petStore.getAccount( userSession.getAccount().getUsername())); } else { return new AccountForm(); } }
Spring 为 Web 应用提供了几个过滤器和监听器,在适合的时间使用它们,能够解决一些常见的 Web 应用问题。
延迟加载过滤器
Hibernate 容许对关联对象、属性进行延迟加载,可是必须保证延迟加载的操做限于同一个 Hibernate Session 范围以内进行。若是 Service 层返回一个启用了延迟加载功能的领域对象给 Web 层,当 Web 层访问到那些须要延迟加载的数据时,因为加载领域对象的 Hibernate Session 已经关闭,这些致使延迟加载数据的访问异常。
Spring 为此专门提供了一个 OpenSessionInViewFilter 过滤器,它的主要功能是使每一个请求过程绑定一个 Hibernate Session,即便最初的事务已经完成了,也能够在 Web 层进行延迟加载的操做。
OpenSessionInViewFilter 过滤器将 Hibernate Session 绑定到请求线程中,它将自动被 Spring 的事务管理器探测到。因此 OpenSessionInViewFilter 适用于 Service 层使用 HibernateTransactionManager 或 JtaTransactionManager 进行事务管理的环境,也能够用于非事务只读的数据操做中。
要启用这个过滤器,必须在 web.xml 中对此进行配置:
… <filter> <filter-name>hibernateFilter</filter-name> <filter-class> org.springframework.orm.hibernate3.support.OpenSessionInViewFilter </filter-class> </filter> <filter-mapping> <filter-name>hibernateFilter</filter-name> <url-pattern>*.html</url-pattern> </filter-mapping> …
上面的配置,咱们假设使用 .html 的后缀做为 Web 框架的 URL 匹配模式,若是您使用 Struts 等 Web 框架,能够将其改成对应的“*.do”模型。
中文乱码过滤器
在您经过表单向服务器提交数据时,一个经典的问题就是中文乱码问题。虽然咱们全部的 JSP 文件和页面编码格式都采用 UTF-8,但这个问题仍是会出现。解决的办法很简单,咱们只须要在 web.xml 中配置一个 Spring 的编码转换过滤器就能够了:
<web-app> <!---listener 的配置 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class> org.springframework.web.filter.CharacterEncodingFilter ① Spring 编辑过滤器 </filter-class> <init-param> ② 编码方式 <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> ③ 强制进行编码转换 <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> ② 过滤器的匹配 URL <filter-name>encodingFilter</filter-name> <url-pattern>*.html</url-pattern> </filter-mapping> <!---servlet 的配置 --> </web-app>
这样全部以 .html 为后缀的 URL 请求的数据都会被转码为 UTF-8 编码格式,表单中文乱码的问题就能够解决了。
请求跟踪日志过滤器
除了以上两个经常使用的过滤器外,还有两个在程序调试时可能会用到的请求日志跟踪过滤器,它们会将请求的一些重要信息记录到日志中,方便程序的调试。这两个日志过滤器只有在日志级别为 DEBUG 时才会起做用:
方法 | 说明 |
---|---|
org.springframework.web.filter.ServletContextRequestLoggingFilter |
该过滤器将请求的 URI 记录到 Common 日志中(如经过 Log4J 指定的日志文件); |
org.springframework.web.filter.ServletContextRequestLoggingFilter |
该过滤器将请求的 URI 记录到 ServletContext 日志中。 |
如下是日志过滤器记录的请求跟踪日志的片段:
(JspServlet.java:224) - JspEngine --> /htmlTest.jsp (JspServlet.java:225) - ServletPath: /htmlTest.jsp (JspServlet.java:226) - PathInfo: null (JspServlet.java:227) - RealPath: D:\masterSpring\chapter23\webapp\htmlTest.jsp (JspServlet.java:228) - RequestURI: /baobaotao/htmlTest.jsp …
经过这个请求跟踪日志,程度调试者能够详细地查看到有哪些请求被调用,请求的参数是什么,请求是否正确返回等信息。虽然这两个请求跟踪日志过滤器通常在程序调试时使用,可是即便程序部署不将其从 web.xml 中移除也不会有大碍,由于只要将日志级别设置为 DEBUG 以上级别,它们就不会输出请求跟踪日志信息了。
转存 Web 应用根目录监听器和 Log4J 监听器
Spring 在 org.springframework.web.util 包中提供了几个特殊用途的 Servlet 监听器,正确地使用它们能够完成一些特定需求的功能。好比某些第三方工具支持经过 ${key} 的方式引用系统参数(便可以经过 System.getProperty() 获取的属性),WebAppRootListener 能够将 Web 应用根目录添加到系统参数中,对应的属性名能够经过名为“webAppRootKey”的 Servlet 上下文参数指定,默认为“webapp.root”。下面是该监听器的具体的配置:
… <context-param> <param-name>webAppRootKey</param-name> <param-value>baobaotao.root</param-value> ① Web 应用根目录以该属性名添加到系统参数中 </context-param> … ② 负责将 Web 应用根目录以 webAppRootKey 上下文参数指定的属性名添加到系统参数中 <listener> <listener-class> org.springframework.web.util.WebAppRootListener </listener-class> </listener> …
这样,您就能够在程序中经过 System.getProperty("baobaotao.root") 获取 Web 应用的根目录了。不过更常见的使用场景是在第三方工具的配置文件中经过 ${baobaotao.root} 引用 Web 应用的根目录。好比如下的 log4j.properties 配置文件就经过 ${baobaotao.root} 设置了日志文件的地址:
log4j.rootLogger=INFO,R log4j.appender.R=org.apache.log4j.RollingFileAppender log4j.appender.R.File=${baobaotao.root}/WEB-INF/logs/log4j.log ① 指定日志文件的地址 log4j.appender.R.MaxFileSize=100KB log4j.appender.R.MaxBackupIndex=1 log4j.appender.R.layout.ConversionPattern=%d %5p [%t] (%F:%L) - %m%n
另外一个专门用于 Log4J 的监听器是 Log4jConfigListener。通常状况下,您必须将 Log4J 日志配置文件以 log4j.properties 为文件名并保存在类路径下。Log4jConfigListener 容许您经过 log4jConfigLocation Servlet 上下文参数显式指定 Log4J 配置文件的地址,以下所示:
① 指定 Log4J 配置文件的地址 <context-param> <param-name>log4jConfigLocation</param-name> <param-value>/WEB-INF/log4j.properties</param-value> </context-param> … ② 使用该监听器初始化 Log4J 日志引擎 <listener> <listener-class> org.springframework.web.util.Log4jConfigListener </listener-class> </listener> …
Log4jConfigListener 监听器包括了 WebAppRootListener 的功能,也就是说,Log4jConfigListener 会自动完成将 Web 应用根目录以 webAppRootKey 上下文参数指定的属性名添加到系统参数中,因此当您使用 Log4jConfigListener 后,就没有必须再使用 WebAppRootListener 了。
Introspector 缓存清除监听器
Spring 还提供了一个名为 org.springframework.web.util.IntrospectorCleanupListener 的监听器。它主要负责处理由 JavaBean Introspector 功能而引发的缓存泄露。IntrospectorCleanupListener 监听器在 Web 应用关闭的时会负责清除 JavaBean Introspector 的缓存,在 web.xml 中注册这个监听器能够保证在 Web 应用关闭的时候释放与其相关的 ClassLoader 的缓存和类引用。若是您使用了 JavaBean Introspector 分析应用中的类,Introspector 缓存会保留这些类的引用,结果在应用关闭的时候,这些类以及 Web 应用相关的 ClassLoader 不能被垃圾回收。不幸的是,清除 Introspector 的惟一方式是刷新整个缓存,这是由于无法准确判断哪些是属于本 Web 应用的引用对象,哪些是属于其它 Web 应用的引用对象。因此删除被缓存的 Introspection 会致使将整个 JVM 全部应用的 Introspection 都删掉。须要注意的是,Spring 托管的 Bean 不须要使用这个监听器,由于 Spring 的 Introspection 所使用的缓存在分析完一个类以后会立刻从 javaBean Introspector 缓存中清除掉,并将缓存保存在应用程序特定的 ClassLoader 中,因此它们通常不会致使内存资源泄露。可是一些类库和框架每每会产生这个问题。例如 Struts 和 Quartz 的 Introspector 的内存泄漏会致使整个的 Web 应用的 ClassLoader 不能进行垃圾回收。在 Web 应用关闭以后,您还会看到此应用的全部静态类引用,这个错误固然不是由这个类自身引发的。解决这个问题的方法很简单,您仅需在 web.xml 中配置 IntrospectorCleanupListener 监听器就能够了:
<listener> <listener-class> org.springframework.web.util.IntrospectorCleanupListener </listener-class> </listener>
本文介绍了一些经常使用的 Spring 工具类,其中大部分 Spring 工具类不但能够在基于 Spring 的应用中使用,还能够在其它的应用中使用。使用 JDK 的文件操做类在访问类路径相关、Web 上下文相关的文件资源时,每每显得拖泥带水、拐弯抹角,Spring 的 Resource 实现类使这些工做变得轻松了许多。
在 Web 应用中,有时你但愿直接访问 Spring 容器,获取容器中的 Bean,这时使用 WebApplicationContextUtils 工具类从 ServletContext 中获取 WebApplicationContext 是很是方便的。WebUtils 为访问 Servlet API 提供了一套便捷的代理方法,您能够经过 WebUtils 更好的访问 HttpSession 或 ServletContext 的信息。
Spring 提供了几个 Servlet 过滤器和监听器,其中 ServletContextRequestLoggingFilter 和 ServletContextRequestLoggingFilter 能够记录请求访问的跟踪日志,你能够在程序调试时使用它们获取请求调用的详细信息。WebAppRootListener 能够将 Web 应用的根目录以特定属性名添加到系统参数中,以便第三方工具类经过 ${key} 的方式进行访问。Log4jConfigListener 容许你指定 Log4J 日志配置文件的地址,且能够在配置文件中经过 ${key} 的方式引用 Web 应用根目录,若是你须要在 Web 应用相关的目录建立日志文件,使用 Log4jConfigListener 能够很容易地达到这一目标。
Web 应用的内存泄漏是最让开发者头疼的问题,虽然不正确的程序编写多是这一问题的根源,也有多是一些第三方框架的 JavaBean Introspector 缓存得不到清除而致使的,Spring 专门为解决这一问题配备了 IntrospectorCleanupListener 监听器,它只要简单在 web.xml 中声明该监听器就能够了。
http://www.ibm.com/developerworks/cn/java/j-lo-spring-utils2/
因为 Web 应用程序须要联合使用到多种语言,每种语言都包含一些特殊的字符,对于动态语言或标签式的语言而言,若是须要动态构造语言的内容时,一个咱们常常会碰到的问题就是特殊字符转义的问题。下面是 Web 开发者最常面对须要转义的特殊字符类型:
若是不对这些特殊字符进行转义处理,则不但可能破坏文档结构,还能够引起潜在的安全问题。Spring 为 HTML 和 JavaScript 特殊字符提供了转义操做工具类,它们分别是 HtmlUtils 和 JavaScriptUtils。
HTML 中 <,>,& 等字符有特殊含义,它们是 HTML 语言的保留字,所以不能直接使用。使用这些个字符时,应使用它们的转义序列:
因为 HTML 网页自己就是一个文本型结构化文档,若是直接将这些包含了 HTML 特殊字符的内容输出到网页中,极有可能破坏整个 HTML 文档的结构。因此,通常状况下须要对动态数据进行转义处理,使用转义序列表示 HTML 特殊字符。下面的 JSP 网页将一些变量动态输出到 HTML 网页中:
<%@ page language="java" contentType="text/html; charset=utf-8"%> <%! String userName = "</td><tr></table>"; String address = " \" type=\"button"; %> <table border="1"> <tr> <td>姓名:</td><td><%=userName%></td> ① </tr> <tr> <td>年龄:</td><td>28</td> </tr> </table> <input value="<%=address%>" type="text" /> ②
在 ① 和 ② 处,咱们未经任何转义处理就直接将变量输出到 HTML 网页中,因为这些变量可能包含一些特殊的 HTML 的字符,它们将可能破坏整个 HTML 文档的结构。咱们能够从以上 JSP 页面的一个具体输出中了解这一问题:
<table border="1"> <tr> <td>姓名:</td><td></td><tr></table></td> ① 破坏了 <table> 的结构 </tr> <tr> <td>年龄:</td><td>28</td> </tr> </table> <input value=" " type="button" type="text" /> ② 将原本是输入框组件偷梁换柱为按钮组件
融合动态数据后的 HTML 网页已经面目全非,首先 ① 处的 <table> 结构被包含 HTML 特殊字符的 userName 变量截断了,形成其后的 <table> 代码变成无效的内容;其次,② 处 <input> 被动态数据改换为按钮类型的组件(type="button")。为了不这一问题,咱们须要事先对可能破坏 HTML 文档结构的动态数据进行转义处理。Spring 为咱们提供了一个简单适用的 HTML 特殊字符转义工具类,它就是 HtmlUtils。下面,咱们经过一个简单的例子了解 HtmlUtils 的具体用法:
package com.baobaotao.escape; import org.springframework.web.util.HtmlUtils; public class HtmpEscapeExample { public static void main(String[] args) { String specialStr = "<div id=\"testDiv\">test1;test2</div>"; String str1 = HtmlUtils.htmlEscape(specialStr); ①转换为HTML转义字符表示 System.out.println(str1); String str2 = HtmlUtils.htmlEscapeDecimal(specialStr); ②转换为数据转义表示 System.out.println(str2); String str3 = HtmlUtils.htmlEscapeHex(specialStr); ③转换为十六进制数据转义表示 System.out.println(str3); ④下面对转义后字符串进行反向操做 System.out.println(HtmlUtils.htmlUnescape(str1)); System.out.println(HtmlUtils.htmlUnescape(str2)); System.out.println(HtmlUtils.htmlUnescape(str3)); } }
HTML 不但可使用通用的转义序列表示 HTML 特殊字符,还可使用以 # 为前缀的数字序列表示 HTML 特殊字符,它们在最终的显示效果上是同样的。HtmlUtils 提供了三个转义方法:
方法 | 说明 |
---|---|
static String htmlEscape(String input) |
将 HTML 特殊字符转义为 HTML 通用转义序列; |
static String htmlEscapeDecimal(String input) |
将 HTML 特殊字符转义为带 # 的十进制数据转义序列; |
static String htmlEscapeHex(String input) |
将 HTML 特殊字符转义为带 # 的十六进制数据转义序列; |
此外,HtmlUtils 还提供了一个可以将通过转义内容还原的方法:htmlUnescape(String input),它能够还原以上三种转义序列的内容。运行以上代码,您将能够看到如下的输出:
str1:<div id="testDiv">test1;test2</div> str2:<div id="testDiv">test1;test2</div> str3:<div id="testDiv">test1;test2</div> <div id="testDiv">test1;test2</div> <div id="testDiv">test1;test2</div> <div id="testDiv">test1;test2</div>
您只要使用 HtmlUtils 对代码 清单 1 的 userName 和 address 进行转义处理,最终输出的 HTML 页面就不会遭受破坏了。
JavaScript 中也有一些须要特殊处理的字符,若是直接将它们嵌入 JavaScript 代码中,JavaScript 程序结构将会遭受破坏,甚至被嵌入一些恶意的程序。下面列出了须要转义的特殊 JavaScript 字符:
咱们经过一个具体例子演示动态变量是如何对 JavaScript 程序进行破坏的。假设咱们有一个 JavaScript 数组变量,其元素值经过一个 Java List 对象提供,下面是完成这一操做的 JSP 代码片段:
<%@ page language="java" contentType="text/html; charset=utf-8"%> <jsp:directive.page import="java.util.*"/> <% List textList = new ArrayList(); textList.add("\";alert();j=\""); %> <script> var txtList = new Array(); <% for ( int i = 0 ; i < textList.size() ; i++) { %> txtList[<%=i%>] = "<%=textList.get(i)%>"; ① 未对可能包含特殊 JavaScript 字符的变量进行处理 <% } %> </script>
当客户端调用这个 JSP 页面后,将获得如下的 HTML 输出页面:
<script> var txtList = new Array(); txtList[0] = "";alert();j=""; ① 原本是但愿接受一个字符串,结果被植入了一段JavaScript代码 </script>
因为包含 JavaScript 特殊字符的 Java 变量直接合并到 JavaScript 代码中,咱们原本指望 ① 处所示部分是一个普通的字符串,但结果变成了一段 JavaScript 代码,网页将弹出一个 alert 窗口。想像一下若是粗体部分的字符串是“";while(true)alert();j="”时会产生什么后果呢?
所以,若是网页中的 JavaScript 代码须要经过拼接 Java 变量动态产生时,通常须要对变量的内容进行转义处理,能够经过 Spring 的 JavaScriptUtils 完成这件工做。下面,咱们使用 JavaScriptUtils 对以上代码进行改造:
<%@ page language="java" contentType="text/html; charset=utf-8"%> <jsp:directive.page import="java.util.*"/> <jsp:directive.page import="org.springframework.web.util.JavaScriptUtils"/> <% List textList = new ArrayList(); textList.add("\";alert();j=\""); %> <script> var txtList = new Array(); <% for ( int i = 0 ; i < textList.size() ; i++) { %> ① 在输出动态内容前事先进行转义处理 txtList[<%=i%>] = "<%=JavaScriptUtils.javaScriptEscape(""+textList.get(i))%>"; <% } %> </script>
经过转义处理后,这个 JSP 页面输出的结果网页的 JavaScript 代码就不会产生问题了:
<script> var txtList = new Array(); txtList[0] = "\";alert();j=\""; ① 粗体部分仅是一个普通的字符串,而非一段 JavaScript 的语句了 </script>
应该说,您即便没有处理 HTML 或 JavaScript 的特殊字符,也不会带来灾难性的后果,可是若是不在动态构造 SQL 语句时对变量中特殊字符进行处理,将可能致使程序漏洞、数据盗取、数据破坏等严重的安全问题。网络中有大量讲解 SQL 注入的文章,感兴趣的读者能够搜索相关的资料深刻研究。
虽然 SQL 注入的后果很严重,可是只要对动态构造的 SQL 语句的变量进行特殊字符转义处理,就能够避免这一问题的发生了。来看一个存在安全漏洞的经典例子:
SELECT COUNT(userId) FROM t_user WHERE userName='"+userName+"' AND password ='"+password+"';
以上 SQL 语句根据返回的结果数判断用户提供的登陆信息是否正确,若是 userName 变量不通过特殊字符转义处理就直接合并到 SQL 语句中,黑客就能够经过将 userName 设置为 “1' or '1'='1”绕过用户名/密码的检查直接进入系统了。
因此除非必要,通常建议经过 PreparedStatement 参数绑定的方式构造动态 SQL 语句,由于这种方式能够避免 SQL 注入的潜在安全问题。可是每每很难在应用中彻底避免经过拼接字符串构造动态 SQL 语句的方式。为了防止他人使用特殊 SQL 字符破坏 SQL 的语句结构或植入恶意操做,必须在变量拼接到 SQL 语句以前对其中的特殊字符进行转义处理。Spring 并无提供相应的工具类,您能够经过 jakarta commons lang 通用类包中(spring/lib/jakarta-commons/commons-lang.jar)的 StringEscapeUtils 完成这一工做:
package com.baobaotao.escape; import org.apache.commons.lang.StringEscapeUtils; public class SqlEscapeExample { public static void main(String[] args) { String userName = "1' or '1'='1"; String password = "123456"; userName = StringEscapeUtils.escapeSql(userName); password = StringEscapeUtils.escapeSql(password); String sql = "SELECT COUNT(userId) FROM t_user WHERE userName='" + userName + "' AND password ='" + password + "'"; System.out.println(sql); } }
事实上,StringEscapeUtils 不但提供了 SQL 特殊字符转义处理的功能,还提供了 HTML、XML、JavaScript、Java 特殊字符的转义和还原的方法。若是您不介意引入 jakarta commons lang 类包,咱们更推荐您使用 StringEscapeUtils 工具类完成特殊字符转义处理的工做。
Web 应用在接受表单提交的数据后都须要对其进行合法性检查,若是表单数据不合法,请求将被驳回。相似的,当咱们在编写类的方法时,也经常须要对方法入参进行合法性检查,若是入参不符合要求,方法将经过抛出异常的方式拒绝后续处理。举一个例子:有一个根据文件名获取输入流的方法:InputStream getData(String file),为了使方法可以成功执行,必须保证 file 入参不能为 null 或空白字符,不然根本无须进行后继的处理。这时方法的编写者一般会在方法体的最前面编写一段对入参进行检测的代码,以下所示:
public InputStream getData(String file) { if (file == null || file.length() == 0|| file.replaceAll("\\s", "").length() == 0) { throw new IllegalArgumentException("file入参不是有效的文件地址"); } … }
相似以上检测方法入参的代码是很是常见,可是在每一个方法中都使用手工编写检测逻辑的方式并非一个好主意。阅读 Spring 源码,您会发现 Spring 采用一个 org.springframework.util.Assert 通用类完成这一任务。
Assert 翻译为中文为“断言”,使用过 JUnit 的读者都熟知这个概念,它判定某一个实际的运行值和预期想同样,不然就抛出异常。Spring 对方法入参的检测借用了这个概念,其提供的 Assert 类拥有众多按规则对方法入参进行断言的方法,能够知足大部分方法入参检测的要求。这些断言方法在入参不知足要求时就会抛出 IllegalArgumentException。下面,咱们来认识一下 Assert 类中的经常使用断言方法:
断言方法 | 说明 |
---|---|
notNull(Object object) |
当 object 不为 null 时抛出异常,notNull(Object object, String message) 方法容许您经过 message 定制异常信息。和 notNull() 方法断言规则相反的方法是 isNull(Object object)/isNull(Object object, String message),它要求入参必定是 null; |
isTrue(boolean expression) / isTrue(boolean expression, String message) |
当 expression 不为 true 抛出异常; |
notEmpty(Collection collection) / notEmpty(Collection collection, String message) |
当集合未包含元素时抛出异常。notEmpty(Map map) / notEmpty(Map map, String message) 和 notEmpty(Object[] array, String message) / notEmpty(Object[] array, String message) 分别对 Map 和 Object[] 类型的入参进行判断; |
hasLength(String text) / hasLength(String text, String message) |
当 text 为 null 或长度为 0 时抛出异常; |
hasText(String text) / hasText(String text, String message) |
text 不能为 null 且必须至少包含一个非空格的字符,不然抛出异常; |
isInstanceOf(Class clazz, Object obj) / isInstanceOf(Class type, Object obj, String message) |
若是 obj 不能被正确造型为 clazz 指定的类将抛出异常; |
isAssignable(Class superType, Class subType) / isAssignable(Class superType, Class subType, String message) |
subType 必须能够按类型匹配于 superType,不然将抛出异常; |
使用 Assert 断言类能够简化方法入参检测的代码,如 InputStream getData(String file) 在应用 Assert 断言类后,其代码能够简化为如下的形式:
public InputStream getData(String file){ Assert.hasText(file,"file入参不是有效的文件地址"); ① 使用 Spring 断言类进行方法入参检测 … }
可见使用 Spring 的 Assert 替代自编码实现的入参检测逻辑后,方法的简洁性获得了很多的提升。Assert 不依赖于 Spring 容器,您能够大胆地在本身的应用中使用这个工具类。
本文介绍了一些经常使用的 Spring 工具类,其中大部分 Spring 工具类不但能够在基于 Spring 的应用中使用,还能够在其它的应用中使用。
对于 Web 应用来讲,因为有不少关联的脚本代码,若是这些代码经过拼接字符串的方式动态产生,就须要对动态内容中特殊的字符进行转义处理,不然就有可能产生意想不到的后果。Spring 为此提供了 HtmlUtils 和 JavaScriptUtils 工具类,只要将动态内容在拼接以前使用工具类进行转义处理,就能够避免相似问题的发生了。若是您不介意引入一个第三方类包,那么 jakarta commons lang 通用类包中的 StringEscapeUtils 工具类可能更加适合,由于它提供了更加全面的转义功能。
最后咱们还介绍了 Spring 的 Assert 工具类,Assert 工具类是通用性很强的工具类,它使用面向对象的方式解决方法入参检测的问题,您能够在本身的应用中使用 Assert 对方法入参进行检查。