自从那次与 Shiro 邂逅,我就深深地爱上了她,很想走进她的内心世界,看看她为何如此迷人?
我们打算将 Shiro 放在 Web 应用中使用,只需在 web.xml 中做如下配置:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener> <filter> <filter-name>ShiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>ShiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
大家知道 web.xml 才是整个 Web 应用的核心所在,Web 容器(例如:Tomcat)会提供一些监听器,用于监听 Web 应用的生命周期事件,有两个重要的点可以监听,一个是出生,另一个是死亡,具备这类特性的监听器就是 ServletContextListener。
Shiro 的 EnvironmentLoaderListener 就是一个典型的 ServletContextListener,它也是整个 Shiro Web 应用的入口,不妨先来看看它的静态结构吧:
1. EventListener 是一个标志接口,里面没有任何的方法,Servlet 容器中所有的 Listener 都要继承这个接口(这是 Servlet 规范)。
2. ServletContextListener 是一个 ServletContext 的监听器,用于监听容器的启动与关闭事件,包括如下两个方法:
- void contextInitialized(ServletContextEvent sce); // 当容器启动时调用
- void contextDestroyed(ServletContextEvent sce); // 当容器关闭时调用
可以从 ServletContextEvent 中直接获取 ServletContext 对象。
3. EnvironmentLoaderListener 不仅实现了 ServletContextListener 接口,也扩展了 EnvironmentLoader 类,看来它是想在 Servlet 容器中调用 EnvironmentLoader 对象的生命周期方法。
毫无疑问,我们首先从 EnvironmentLoaderListener 开始:
public class EnvironmentLoaderListener extends EnvironmentLoader implements ServletContextListener { // 容器启动时调用 public void contextInitialized(ServletContextEvent sce) { initEnvironment(sce.getServletContext()); } // 当容器关闭时调用 public void contextDestroyed(ServletContextEvent sce) { destroyEnvironment(sce.getServletContext()); } }
看来 EnvironmentLoaderListener 只是一个空架子而已,真正干活的人是它“爹”(EnvironmentLoader):
public class EnvironmentLoader { // 可在 web.xml 的 context-param 中定义 WebEnvironment 接口的实现类(默认为 IniWebEnvironment) public static final String ENVIRONMENT_CLASS_PARAM = "shiroEnvironmentClass"; // 可在 web.xml 的 context-param 中定义 Shiro 配置文件的位置 public static final String CONFIG_LOCATIONS_PARAM = "shiroConfigLocations"; // 在 ServletContext 中存放 WebEnvironment 的 key public static final String ENVIRONMENT_ATTRIBUTE_KEY = EnvironmentLoader.class.getName() + ".ENVIRONMENT_ATTRIBUTE_KEY"; // 从 ServletContext 中获取相关信息,并创建 WebEnvironment 实例 public WebEnvironment initEnvironment(ServletContext servletContext) throws IllegalStateException { // 确保 WebEnvironment 只能创建一次 if (servletContext.getAttribute(ENVIRONMENT_ATTRIBUTE_KEY) != null) { throw new IllegalStateException(); } try { // 创建 WebEnvironment 实例 WebEnvironment environment = createEnvironment(servletContext); // 将 WebEnvironment 实例放入 ServletContext 中 servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, environment); return environment; } catch (RuntimeException ex) { // 将异常对象放入 ServletContext 中 servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, ex); throw ex; } catch (Error err) { // 将错误对象放入 ServletContext 中 servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, err); throw err; } } protected WebEnvironment createEnvironment(ServletContext sc) { // 确定 WebEnvironment 接口的实现类 Class<?> clazz = determineWebEnvironmentClass(sc); // 确保该实现类实现了 MutableWebEnvironment 接口 if (!MutableWebEnvironment.class.isAssignableFrom(clazz)) { throw new ConfigurationException(); } // 从 ServletContext 中获取 Shiro 配置文件的位置参数,并判断该参数是否已定义 String configLocations = sc.getInitParameter(CONFIG_LOCATIONS_PARAM); boolean configSpecified = StringUtils.hasText(configLocations); // 若配置文件位置参数已定义,则需确保该实现类实现了 ResourceConfigurable 接口 if (configSpecified && !(ResourceConfigurable.class.isAssignableFrom(clazz))) { throw new ConfigurationException(); } // 通过反射创建 WebEnvironment 实例,将其转型为 MutableWebEnvironment 类型,并将 ServletContext 放入该实例中 MutableWebEnvironment environment = (MutableWebEnvironment) ClassUtils.newInstance(clazz); environment.setServletContext(sc); // 若配置文件位置参数已定义,且该实例是 ResourceConfigurable 接口的实例(实现了该接口),则将此参数放入该实例中 if (configSpecified && (environment instanceof ResourceConfigurable)) { ((ResourceConfigurable) environment).setConfigLocations(configLocations); } // 可进一步定制 WebEnvironment 实例(在子类中扩展) customizeEnvironment(environment); // 调用 WebEnvironment 实例的 init 方法 LifecycleUtils.init(environment); // 返回 WebEnvironment 实例 return environment; } protected Class<?> determineWebEnvironmentClass(ServletContext servletContext) { // 从初始化参数(context-param)中获取 WebEnvironment 接口的实现类 String className = servletContext.getInitParameter(ENVIRONMENT_CLASS_PARAM); // 若该参数已定义,则加载该实现类 if (className != null) { try { return ClassUtils.forName(className); } catch (UnknownClassException ex) { throw new ConfigurationException(ex); } } else { // 否则使用默认的实现类 return IniWebEnvironment.class; } } protected void customizeEnvironment(WebEnvironment environment) { } // 销毁 WebEnvironment 实例 public void destroyEnvironment(ServletContext servletContext) { try { // 从 ServletContext 中获取 WebEnvironment 实例 Object environment = servletContext.getAttribute(ENVIRONMENT_ATTRIBUTE_KEY); // 调用 WebEnvironment 实例的 destroy 方法 LifecycleUtils.destroy(environment); } finally { // 移除 ServletContext 中存放的 WebEnvironment 实例 servletContext.removeAttribute(ENVIRONMENT_ATTRIBUTE_KEY); } } }
看来 EnvironmentLoader 就是为了:
1. 当容器启动时,读取 web.xml 文件,从中获取 WebEnvironment 接口的实现类(默认是 IniWebEnvironment),初始化该实例,并将其加载到 ServletContext 中。
2. 当容器关闭时,销毁 WebEnvironment 实例,并从 ServletContext 将其移除。
这里有两个配置项可以在 web.xml 中进行配置:
<context-param> <param-name>shiroEnvironmentClass</param-name> <param-value>WebEnvironment 接口的实现类</param-value> </context-param> <context-param> <param-name>shiroConfigLocations</param-name> <param-value>shiro.ini 配置文件的位置</param-value> </context-param>
在 EnvironmentLoader 中仅用于创建 WebEnvironment 接口的实现类,随后将由这个实现类来加载并解析 shiro.ini 配置文件。
既然 WebEnvironment 如此重要,那么很有必要了解一下它的静态结构:
1. 可以认为这是一个较复杂的体系结构,有一系列的功能性接口。
2. 最底层的 IniWebEnvironment 是 WebEnvironment 接口的默认实现类,它将读取 ini 配置文件,并创建 WebEnvironment 实例。
3. 可以断言,如果需要将 Shiro 配置定义在 XML 或 Properties 配置文件中,那就需要自定义一些 WebEnvironment 实现类了。
4. WebEnvironment 的实现类不仅需要实现最顶层的 Environment 接口,还需要实现具有生命周期功能的 Initializable 与 Destroyable 接口。
那么 IniWebEnvironment 这个默认的实现类到底做了写什么呢?最后来看看它的代码吧:
public class IniWebEnvironment extends ResourceBasedWebEnvironment implements Initializable, Destroyable { // 默认 shiro.ini 路径 public static final String DEFAULT_WEB_INI_RESOURCE_PATH = "/WEB-INF/shiro.ini"; // 定义一个 Ini 对象,用于封装 ini 配置项 private Ini ini; public Ini getIni() { return this.ini; } public void setIni(Ini ini) { this.ini = ini; } // 当初始化时调用 public void init() { // 从成员变量中获取 Ini 对象 Ini ini = getIni(); // 从 web.xml 中获取配置文件位置(在 EnvironmentLoader 中已设置) String[] configLocations = getConfigLocations(); // 若成员变量中不存在,则从已定义的配置文件位置获取 if (CollectionUtils.isEmpty(ini)) { ini = getSpecifiedIni(configLocations); } // 若已定义的配置文件中仍然不存在,则从默认的位置获取 if (CollectionUtils.isEmpty(ini)) { ini = getDefaultIni(); } // 若还不存在,则抛出异常 if (CollectionUtils.isEmpty(ini)) { throw new ConfigurationException(); } // 初始化成员变量 setIni(ini); // 解析配置文件,完成初始化工作 configure(); } protected Ini getSpecifiedIni(String[] configLocations) throws ConfigurationException { Ini ini = null; if (configLocations != null && configLocations.length > 0) { // 只能通过第一个配置文件的位置来创建 Ini 对象,且必须有一个配置文件,否则就会报错 ini = createIni(configLocations[0], true); } return ini; } protected Ini createIni(String configLocation, boolean required) throws ConfigurationException { Ini ini = null; if (configLocation != null) { // 从指定路径下读取配置文件 ini = convertPathToIni(configLocation, required); } if (required && CollectionUtils.isEmpty(ini)) { throw new ConfigurationException(); } return ini; } private Ini convertPathToIni(String path, boolean required) { Ini ini = null; if (StringUtils.hasText(path)) { InputStream is = null; // 若路径不包括资源前缀(classpath:、url:、file:),则从 ServletContext 中读取,否则从这些资源路径下读取 if (!ResourceUtils.hasResourcePrefix(path)) { is = getServletContextResourceStream(path); } else { try { is = ResourceUtils.getInputStreamForPath(path); } catch (IOException e) { if (required) { throw new ConfigurationException(e); } } } // 将流中的数据加载到 Ini 对象中 if (is != null) { ini = new Ini(); ini.load(is); } else { if (required) { throw new ConfigurationException(); } } } return ini; } private InputStream getServletContextResourceStream(String path) { InputStream is = null; // 需要将路径进行标准化 path = WebUtils.normalize(path); ServletContext sc = getServletContext(); if (sc != null) { is = sc.getResourceAsStream(path); } return is; } protected Ini getDefaultIni() { Ini ini = null; String[] configLocations = getDefaultConfigLocations(); if (configLocations != null) { // 先找到的先使用,后面的无需使用 for (String location : configLocations) { ini = createIni(location, false); if (!CollectionUtils.isEmpty(ini)) { break; } } } return ini; } protected String[] getDefaultConfigLocations() { return new String[]{ DEFAULT_WEB_INI_RESOURCE_PATH, // /WEB-INF/shiro.ini IniFactorySupport.DEFAULT_INI_RESOURCE_PATH // classpath:shiro.ini }; } protected void configure() { // 清空这个 Bean 容器(一个 Map<String, Object> 对象,在 DefaultEnvironment 中定义) this.objects.clear(); // 创建基于 Web 的 SecurityManager 对象(WebSecurityManager) WebSecurityManager securityManager = createWebSecurityManager(); setWebSecurityManager(securityManager); // 初始化 Filter Chain 解析器(用于解析 Filter 规则) FilterChainResolver resolver = createFilterChainResolver(); if (resolver != null) { setFilterChainResolver(resolver); } } protected WebSecurityManager createWebSecurityManager() { // 通过工厂对象来创建 WebSecurityManager 实例 WebIniSecurityManagerFactory factory; Ini ini = getIni(); if (CollectionUtils.isEmpty(ini)) { factory = new WebIniSecurityManagerFactory(); } else { factory = new WebIniSecurityManagerFactory(ini); } WebSecurityManager wsm = (WebSecurityManager) factory.getInstance(); // 从工厂中获取 Bean Map 并将其放入 Bean 容器中 Map<String, ?> beans = factory.getBeans(); if (!CollectionUtils.isEmpty(beans)) { this.objects.putAll(beans); } return wsm; } protected FilterChainResolver createFilterChainResolver() { FilterChainResolver resolver = null; Ini ini = getIni(); if (!CollectionUtils.isEmpty(ini)) { // Filter 可以从 [urls] 或 [filters] 片段中读取 Ini.Section urls = ini.getSection(IniFilterChainResolverFactory.URLS); Ini.Section filters = ini.getSection(IniFilterChainResolverFactory.FILTERS); if (!CollectionUtils.isEmpty(urls) || !CollectionUtils.isEmpty(filters)) { // 通过工厂对象创建 FilterChainResolver 实例 IniFilterChainResolverFactory factory = new IniFilterChainResolverFactory(ini, this.objects); resolver = factory.getInstance(); } } return resolver; } }
看来 IniWebEnvironment 就是为了:
1. 查找并加载 shiro.ini 配置文件,首先从自身成员变量里查找,然后从 web.xml 中查找,然后从 /WEB-INF 下查找,然后从 classpath 下查找,若均未找到,则直接报错。
2. 当找到了 ini 配置文件后就开始解析,此时构造了一个 Bean 容器(相当于一个轻量级的 IOC 容器),最终的目标是为了创建 WebSecurityManager 对象与 FilterChainResolver 对象,创建过程使用了 Abstract Factory 模式:
其中有两个 Factory 需要关注:
- WebIniSecurityManagerFactory 用于创建 WebSecurityManager。
- IniFilterChainResolverFactory 用于创建 FilterChainResolver。
通过以上分析,相信 EnvironmentLoaderListener 已经不再神秘了,无非就是在容器启动时创建 WebEnvironment 对象,并由该对象来读取 Shiro 配置文件,创建WebSecurityManager 与 FilterChainResolver 对象,它们都在后面将要出现的 ShiroFilter 中起到了重要作用。
从 web.xml 中同样可以得知,ShiroFilter 是整个 Shiro 框架的门面,因为它拦截了所有的请求,后面是需要 Authentication(认证)还是需要 Authorization(授权)都由它说了算。
相信《Shiro 源码分析》的下一篇一定会更加精彩!