自從那次與 Shiro 邂逅,我就深深地愛上了她,很想走進她的心裏世界,看看她爲什麼如此迷人?java
咱們打算將 Shiro 放在 Web 應用中使用,只需在 web.xml 中作以下配置: web
<?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。 apache
Shiro 的 EnvironmentLoaderListener 就是一個典型的 ServletContextListener,它也是整個 Shiro Web 應用的入口,不妨先來看看它的靜態結構吧: app
1. EventListener 是一個標誌接口,裏面沒有任何的方法,Servlet 容器中全部的 Listener 都要繼承這個接口(這是 Servlet 規範)。框架
2. ServletContextListener 是一個 ServletContext 的監聽器,用於監聽容器的啓動與關閉事件,包括以下兩個方法:
- void contextInitialized(ServletContextEvent sce); // 當容器啓動時調用
- void contextDestroyed(ServletContextEvent sce); // 當容器關閉時調用
能夠從 ServletContextEvent 中直接獲取 ServletContext 對象。源碼分析
3. EnvironmentLoaderListener 不只實現了 ServletContextListener 接口,也擴展了 EnvironmentLoader 類,看來它是想在 Servlet 容器中調用 EnvironmentLoader 對象的生命週期方法。ui
毫無疑問,咱們首先從 EnvironmentLoaderListener 開始: this
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): url
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 就是爲了: spa
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 源碼分析》的下一篇必定會更加精彩!