Shiro 之 入口:EnvironmentLoaderListener

原创
2014/03/18 16:39
阅读数 2W

自从那次与 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 源码分析》的下一篇一定会更加精彩!

展开阅读全文
加载中
点击加入讨论🔥(30) 发布并加入讨论🔥
打赏
30 评论
96 收藏
20
分享
返回顶部
顶部