Shiro 之 入口:EnvironmentLoaderListener

自從那次與 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 源碼分析》的下一篇必定會更加精彩!

相關文章
相關標籤/搜索