文档章节

Shiro 之 入口:EnvironmentLoaderListener

黄勇
 黄勇
发布于 2014/03/18 16:39
字数 2228
阅读 10000
收藏 94

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

© 著作权归作者所有

共有 人打赏支持
黄勇

黄勇

粉丝 6384
博文 121
码字总数 216155
作品 1
浦东
CTO(技术副总裁)
私信 提问
加载中

评论(30)

吃饼干的小黄人
请勇哥您有时间把这个shiro源码分析做完吧,看了一下只有两篇文章,非常期待
一波流
一波流
厉害了 老哥
杨延庆
杨延庆
Shiro和Spring结合时,读取的是xml文件,但是我在shiro-spring项目源码里没有看到对应的WebEnvironment的实现类,请问黄工,在Spring集成Shiro时,生成的WebEnvironment是什么子类实例?
唐家V
唐家V
写得太棒了,关键地方都说得很清楚,感谢分享。
黄勇
黄勇

引用来自“yanick”的评论

当权限或资源过多时,运行速度下降很严重,有多少个权限就遍历多少次~

private boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}

引用来自“黄勇”的评论

权限可以配置缓存的,问题应该不提大。

引用来自“yanick”的评论

缓存只是获取方便快速获取授权数据。 但上面代码是表决器,是判断是否有权限的,比如我有1000个权限,他就要遍历1000次。

引用来自“黄勇”的评论

将数据加载在内存中,循环 1000 次,不会有性能问题。

引用来自“yanick”的评论

假设: 一个系统有10000个url,当一个页面有10个按钮需要用标签控制,假设当前subject拥有10000个权限, 打开一个页面需要的时间:遍历10000次资源匹配的时间+(10个按钮*10000)遍历权限,这个时间需要1s以上吧 这个不能接受!
你有证据证明这个结论吗?
yanick
yanick

引用来自“yanick”的评论

当权限或资源过多时,运行速度下降很严重,有多少个权限就遍历多少次~

private boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}

引用来自“黄勇”的评论

权限可以配置缓存的,问题应该不提大。

引用来自“yanick”的评论

缓存只是获取方便快速获取授权数据。 但上面代码是表决器,是判断是否有权限的,比如我有1000个权限,他就要遍历1000次。

引用来自“黄勇”的评论

将数据加载在内存中,循环 1000 次,不会有性能问题。
假设: 一个系统有10000个url,当一个页面有10个按钮需要用标签控制,假设当前subject拥有10000个权限, 打开一个页面需要的时间:遍历10000次资源匹配的时间+(10个按钮*10000)遍历权限,这个时间需要1s以上吧 这个不能接受!
黄勇
黄勇

引用来自“yanick”的评论

当权限或资源过多时,运行速度下降很严重,有多少个权限就遍历多少次~

private boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}

引用来自“黄勇”的评论

权限可以配置缓存的,问题应该不提大。

引用来自“yanick”的评论

缓存只是获取方便快速获取授权数据。 但上面代码是表决器,是判断是否有权限的,比如我有1000个权限,他就要遍历1000次。
将数据加载在内存中,循环 1000 次,不会有性能问题。
yanick
yanick
还有另一个问题,当对url资源判断的时候,如果url资源写得过多的话,也影响速度的。

protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {

if (log.isTraceEnabled()) {
log.trace("appliedPaths property is null or empty. This Filter will passthrough immediately.");
}
return true;
}
for (String path : this.appliedPaths.keySet()) {
// If the path does match, then pass on to the subclass implementation for specific checks
//(first match 'wins'):
if (pathsMatch(path, request)) {
log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path);
Object config = this.appliedPaths.get(path);
return isFilterChainContinued(request, response, path, config);
}
}
//no path matched, allow the request to go through:
yanick
yanick

引用来自“yanick”的评论

当权限或资源过多时,运行速度下降很严重,有多少个权限就遍历多少次~

private boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}

引用来自“黄勇”的评论

权限可以配置缓存的,问题应该不提大。
缓存只是获取方便快速获取授权数据。 但上面代码是表决器,是判断是否有权限的,比如我有1000个权限,他就要遍历1000次。
黄勇
黄勇

引用来自“yanick”的评论

当权限或资源过多时,运行速度下降很严重,有多少个权限就遍历多少次~

private boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}
权限可以配置缓存的,问题应该不提大。
SpringBoot集成Shiro三个渐进式项目以及Shiro功能介绍

版权声明:本文为谙忆原创文章,转载请附上本文链接,谢谢。 https://blog.csdn.net/qq_26525215/article/details/82499114 首先,本篇博客的目的的重点是这里介绍的三个SpringBoot集成Shiro...

谙忆
09/07
0
0
shiro与spring整合

shiro与spring整合 Apache shiro 是一个强大并且灵活的java安全框架,他的几个核心功能包括:身份认证、权限管理、加密、session管理。 下面总结一下shiro和spring的整合。 相关jar包 我一般...

似水流年0_0
2016/07/12
321
1
shiro 使用

在使用Shiro标签库前,首先需要在JSP引入shiro标签: <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> 1、介绍Shiro的标签guest标签 :验证当前用户是否为“访客”,即未认...

_______-
2016/09/04
368
1
轻松带你走进shiro的世界

1.10分钟带你轻松入门shiro Shiro是apache旗下的一款轻量级的Java安全框架,它可以提供如下服务: Authentication(认证) Authorization(授权) Session Management(会话管理) Cryptography(加密...

陈小扁
2016/04/15
138
0
JFinal 整合 Shiro

(例子+源码 http://my.oschina.net/smile622/blog/203459) 最近整合JFinal和Shiro遇到的问题,希望能给你们提示与帮助。 首先,JFinal和Shiro本人都是刚刚接触,JFinal上手很快,但Shiro上手...

浮躁的码农
2015/12/03
41
0

没有更多内容

加载失败,请刷新页面

加载更多

让哲学照亮我们的人生——读《医务工作者需要学点哲学》有感2600字

让哲学照亮我们的人生——读《医务工作者需要学点哲学》有感2600字: 作者:孙冬梅;以前读韩国前总统朴槿惠的著作《绝望锻炼了我》时,里面有一句话令我印象深刻,她说“在我最困难的时期,...

原创小博客
26分钟前
0
0
JAVA-四元数类

public class Quaternion { private final double x0, x1, x2, x3; // 四元数构造函数 public Quaternion(double x0, double x1, double x2, double x3) { this.x0 = ......

Pulsar-V
44分钟前
13
0
Xshell利用Xftp传输文件,使用pure-ftpd搭建ftp服务

Xftp传输文件 如果已经通过Xshell登录到服务器,此时可以使用快捷键ctrl+alt+f 打开Xftp并展示Xshell当前的目录,之后直接拖拽传输文件即可。 pure-ftpd搭建ftp服务 pure-ftpd要比vsftp简单,...

野雪球
45分钟前
1
0
Confluence 6 文档主题合并问答

在 Confluence 官方 前期发布的消息 中,文档主题在 Confluence 6.0 及其后续版本中已经不可用。我们知道你可能对这个有很多好好奇的问题,因此我们在这里设置了一个问答用于帮助你将这个主题...

honeymose
今天
2
0
java框架学习日志-2

上篇文章(java框架学习日志-1)虽然跟着写了例子,也理解为什么这么写,但是有个疑问,为什么叫控制反转?控制的是什么?反转又是什么? 控制其实就是控制对象的创建。 反转与正转对应,正转...

白话
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部