对于Shiro(v1.2+)的SecurityManager的创建,在普通的应用程序中一般可以在main方法中这么创建

Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();

该方法读取classpath路径下的shiro.ini文件来构建SecurityManager,然而在web应用程序中,其是怎么创建的我们接下来逐步分析。

在web环境中我们会使用以下的Listener,而SecurityManager的创建就在Listener的初始化过程中【该Listener在shrio-web.jar中】

<listener><listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>

EnvironmentLoaderListener的继承关系很简单,如下所示

EnvironmentLoader的作用是负责在应用程序启动的时候负责加载Shiro,同时将org.apache.shiro.web.mgt.WebSecurityManager设置到ServletContext中。

在初始化Shiro的过程中,在web.xml文件中配置的上下文参数“shiroEnvironmentClass”和“shiroConfigLocations”可以指导Shiro的初始化过程,当然,这两个参数不是必须配置的,有默认值。

shiroEnvironmentClass:制定继承自WebEnvironment的自定义类,默认对象为IniWebEnvironment。
shiroConfigLocations:制定shiro初始化时用的配置文件路径,默认会先查询/WEB-INF/shiro.ini,如果没找到再查找classpath:shiro.ini。

public class EnvironmentLoaderListener extends EnvironmentLoader implements ServletContextListener {/*** Initializes the Shiro {@code WebEnvironment} and binds it to the {@code ServletContext} at application* startup for future reference.** @param sce the ServletContextEvent triggered upon application startup*/public void contextInitialized(ServletContextEvent sce) {initEnvironment(sce.getServletContext());}/*** Destroys any previously created/bound {@code WebEnvironment} instance created by* the {@link #contextInitialized(javax.servlet.ServletContextEvent)} method.** @param sce the ServletContextEvent triggered upon application shutdown*/public void contextDestroyed(ServletContextEvent sce) {destroyEnvironment(sce.getServletContext());}
}
public class EnvironmentLoader {public WebEnvironment initEnvironment(ServletContext servletContext) throws IllegalStateException {if (servletContext.getAttribute(ENVIRONMENT_ATTRIBUTE_KEY) != null) {String msg = "There is already a Shiro environment associated with the current ServletContext.  " +"Check if you have multiple EnvironmentLoader* definitions in your web.xml!";throw new IllegalStateException(msg);}servletContext.log("Initializing Shiro environment");log.info("Starting Shiro environment initialization.");long startTime = System.currentTimeMillis();try {WebEnvironment environment = createEnvironment(servletContext);servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY,environment);log.debug("Published WebEnvironment as ServletContext attribute with name [{}]",ENVIRONMENT_ATTRIBUTE_KEY);if (log.isInfoEnabled()) {long elapsed = System.currentTimeMillis() - startTime;log.info("Shiro environment initialized in {} ms.", elapsed);}return environment;} catch (RuntimeException ex) {log.error("Shiro environment initialization failed", ex);servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, ex);throw ex;} catch (Error err) {log.error("Shiro environment initialization failed", err);servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, err);throw err;}}protected WebEnvironment createEnvironment(ServletContext sc) {//查找WebEnvironment对象,并将其实例化WebEnvironment webEnvironment = determineWebEnvironment(sc);if (!MutableWebEnvironment.class.isInstance(webEnvironment)) {throw new ConfigurationException("Custom WebEnvironment class [" + webEnvironment.getClass().getName() +"] is not of required type [" + MutableWebEnvironment.class.getName() + "]");}String configLocations = sc.getInitParameter(CONFIG_LOCATIONS_PARAM);boolean configSpecified = StringUtils.hasText(configLocations);if (configSpecified && !(ResourceConfigurable.class.isInstance(webEnvironment))) {String msg = "WebEnvironment class [" + webEnvironment.getClass().getName() + "] does not implement the " +ResourceConfigurable.class.getName() + "interface.  This is required to accept any " +"configured " + CONFIG_LOCATIONS_PARAM + "value(s).";throw new ConfigurationException(msg);}MutableWebEnvironment environment = (MutableWebEnvironment) webEnvironment;//保存当前的ServletContext对象environment.setServletContext(sc);//如果在web.xml设置了配置文件的路径,则在此设置到environment中if (configSpecified && (environment instanceof ResourceConfigurable)) {((ResourceConfigurable) environment).setConfigLocations(configLocations);}//构造方法,默认未实现customizeEnvironment(environment);//调用environment的init方法初始化environment对象LifecycleUtils.init(environment);return environment;}protected WebEnvironment determineWebEnvironment(ServletContext servletContext) {//从ServletContext的参数中获取WebEnvironment的配置--shiroEnvironmentClass,如果有则创建实例返回Class<? extends WebEnvironment> webEnvironmentClass = webEnvironmentClassFromServletContext(servletContext);WebEnvironment webEnvironment = null;// 尝试通过Java的ServiceLoader来查找WebEnvironment的实现类if (webEnvironmentClass == null) {webEnvironment = webEnvironmentFromServiceLoader();}// 如果上面的步骤都没找到,则使用默认的WebEnvironment实现类IniWebEnvironmentif (webEnvironmentClass == null && webEnvironment == null) {webEnvironmentClass = getDefaultWebEnvironmentClass();}// 创建WebEnvironment的实例if (webEnvironmentClass != null) {webEnvironment = (WebEnvironment) ClassUtils.newInstance(webEnvironmentClass);}return webEnvironment;}private WebEnvironment webEnvironmentFromServiceLoader() {WebEnvironment webEnvironment = null;/** 使用Java的ServiceLoader方式来查找WebEnvironment的实现类(查找jar包中META-INF下的services文件夹中的文件);* 例如在某个services文件夹中有个名为org.apache.shiro.web.env.WebEnvironment的文件,然后在文件里面保存WebEnvironment的实现类全路径;* 可见,文件名为接口的全路径,里面的内容为接口的实现类* */ServiceLoader<WebEnvironment> serviceLoader = ServiceLoader.load(WebEnvironment.class);Iterator<WebEnvironment> iterator = serviceLoader.iterator();// 如果找到则使用第一个if (iterator.hasNext()) {webEnvironment = iterator.next();}// 如果不止找到一个,则抛出异常if (iterator.hasNext()) {List<String> allWebEnvironments = new ArrayList<String>();allWebEnvironments.add(webEnvironment.getClass().getName());while (iterator.hasNext()) {allWebEnvironments.add(iterator.next().getClass().getName());}throw new ConfigurationException("ServiceLoader for class [" + WebEnvironment.class + "] returned more then one " +"result.  ServiceLoader must return zero or exactly one result for this class. Select one using the " +"servlet init parameter '"+ ENVIRONMENT_CLASS_PARAM +"'. Found: " + allWebEnvironments);}return webEnvironment;}
}

综上得知,查找WebEnvironment的实现类经历了三次查找
1)从ServletContext的初始化参数
2)从jar包查找实现类
3)使用默认的IniWebEnvironment

在得到WebEnvironment的实现类并创建好实例后,接着便会调用其init方法,这里假设得到的是默认的IniWebEnvironment。

public class IniWebEnvironment extends ResourceBasedWebEnvironment implements Initializable, Destroyable {public static final String DEFAULT_WEB_INI_RESOURCE_PATH = "/WEB-INF/shiro.ini";public static final String FILTER_CHAIN_RESOLVER_NAME = "filterChainResolver";private static final Logger log = LoggerFactory.getLogger(IniWebEnvironment.class);/*** The Ini that configures this WebEnvironment instance.*/private Ini ini;private WebIniSecurityManagerFactory factory;public IniWebEnvironment() {//实例化WebIniSecurityManagerFactory对象factory = new WebIniSecurityManagerFactory();}/*** 初始化本实例*/public void init() {//解析shiiro.ini配置文件并生成对应的Ini实例setIni(parseConfig());//使用Ini信息,通过WebIniSecurityManagerFactory创建WebSecurityManager实例configure();}
}protected Ini parseConfig() {//直接取,首次运行肯定为nullIni ini = getIni();//获取配置文件路径【该路径信息就是web.xml文件中配置的,实例化该类的时候已经在EnvironmentLoader中设置】String[] configLocations = getConfigLocations();if (log.isWarnEnabled() && !CollectionUtils.isEmpty(ini) &&configLocations != null && configLocations.length > 0) {//如果Ini对象不为空,并且configLocations也不为空,给出提示信息log.warn("Explicit INI instance has been provided, but configuration locations have also been " +"specified.  The {} implementation does not currently support multiple Ini config, but this may " +"be supported in the future. Only the INI instance will be used for configuration.",IniWebEnvironment.class.getName());}if (CollectionUtils.isEmpty(ini)) {log.debug("Checking any specified config locations.");//从指定路径下的配置文件中创建Ini实例ini = getSpecifiedIni(configLocations);}if (CollectionUtils.isEmpty(ini)) {log.debug("No INI instance or config locations specified.  Trying default config locations.");/** 如果没有在web.xml中配置,则从默认的路径下读取配置文件并创建实例* 1,/WEB-INF/shiro.ini* 2,classpath:shiro.ini* */ini = getDefaultIni();}/** 为了保持向后兼容而提供getFrameworkIni方法来创建Ini对象并与上面得到的Ini对象合并.* getFrameworkIni的默认实现返回null,经过合并处理后返回的还是上面的Ini对象* */ini = mergeIni(getFrameworkIni(), ini);if (CollectionUtils.isEmpty(ini)) {String msg = "Shiro INI configuration was either not found or discovered to be empty/unconfigured.";throw new ConfigurationException(msg);}return ini;}/*** 解析配置文件创建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)) {String msg = "Required configuration location '" + configLocation + "' does not exist or did not " +"contain any INI configuration.";throw new ConfigurationException(msg);}return ini;}/*** 加载制定路径的配置文件,然后将文件流作为参数调用Ini实例对象的load方法来初始化Ini对象* */private Ini convertPathToIni(String path, boolean required) {Ini ini = null;if (StringUtils.hasText(path)) {InputStream is = null;//SHIRO-178: Check for servlet context resource and not only resource paths:if (!ResourceUtils.hasResourcePrefix(path)) {is = getServletContextResourceStream(path);} else {try {is = ResourceUtils.getInputStreamForPath(path);} catch (IOException e) {if (required) {throw new ConfigurationException(e);} else {if (log.isDebugEnabled()) {log.debug("Unable to load optional path '" + path + "'.", e);}}}}if (is != null) {ini = new Ini();ini.load(is);} else {if (required) {throw new ConfigurationException("Unable to load resource path '" + path + "'");}}}return ini;}

再看看Ini对象的初始化过程

public class Ini implements Map<String, Ini.Section> {private static transient final Logger log = LoggerFactory.getLogger(Ini.class);public static final String DEFAULT_SECTION_NAME = ""; //empty string means the first unnamed sectionpublic static final String DEFAULT_CHARSET_NAME = "UTF-8";public static final String COMMENT_POUND = "#";public static final String COMMENT_SEMICOLON = ";";public static final String SECTION_PREFIX = "[";public static final String SECTION_SUFFIX = "]";protected static final char ESCAPE_TOKEN = '\\';private final Map<String, Section> sections;/*** Creates a new empty {@code Ini} instance.*/public Ini() {this.sections = new LinkedHashMap<String, Section>();}public void load(InputStream is) throws ConfigurationException {if (is == null) {throw new NullPointerException("InputStream argument cannot be null.");}InputStreamReader isr;try {isr = new InputStreamReader(is, DEFAULT_CHARSET_NAME);} catch (UnsupportedEncodingException e) {throw new ConfigurationException(e);}load(isr);}public void load(Reader reader) {Scanner scanner = new Scanner(reader);try {load(scanner);} finally {try {scanner.close();} catch (Exception e) {log.debug("Unable to cleanly close the InputStream scanner.  Non-critical - ignoring.", e);}}}public void load(Scanner scanner) {String sectionName = DEFAULT_SECTION_NAME;StringBuilder sectionContent = new StringBuilder();//循环读取每一行while (scanner.hasNextLine()) {String rawLine = scanner.nextLine();//取出两边的空格String line = StringUtils.clean(rawLine);if (line == null || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) {//忽略空行和注释continue;}//获取section名称,格式为 [main] 这种,此时返回“main”String newSectionName = getSectionName(line);if (newSectionName != null) {//前面section的配置信息收集完成,添加section配置addSection(sectionName, sectionContent);//为本次的section重置StringBuilder对象,用户存放该section的配置信息sectionContent = new StringBuilder();sectionName = newSectionName;if (log.isDebugEnabled()) {log.debug("Parsing " + SECTION_PREFIX + sectionName + SECTION_SUFFIX);}} else {//添加配置信息sectionContent.append(rawLine).append("\n");}}//添加Section的配置信息addSection(sectionName, sectionContent);}private void addSection(String name, StringBuilder content) {if (content.length() > 0) {String contentString = content.toString();String cleaned = StringUtils.clean(contentString);if (cleaned != null) {//构建Section对象【静态内部类】Section section = new Section(name, contentString);if (!section.isEmpty()) {//以键值对的方式保存Section对象sections.put(name, section);}}}}public static class Section implements Map<String, String> {private final String name;private final Map<String, String> props;/** 解析收集的配置信息,将配置信息保存到props对象中* */private Section(String name, String sectionContent) {if (name == null) {throw new NullPointerException("name");}this.name = name;Map<String,String> props;if (StringUtils.hasText(sectionContent) ) {props = toMapProps(sectionContent);} else {props = new LinkedHashMap<String,String>();}if ( props != null ) {this.props = props;} else {this.props = new LinkedHashMap<String,String>();}}}
}

到此,在IniWebEnvironment实例中通过解析配置文件得到了Ini对象,该对象里面保存了配置文件中的每个Section信息,那么接着就要使用该Ini对象来构建WebSecurityManager了,也就是调用IniWebEnvironment 的configure方法

public class IniWebEnvironment extends ResourceBasedWebEnvironment implements Initializable, Destroyable {protected void configure() {//Map<String, Object>对象this.objects.clear();WebSecurityManager securityManager = createWebSecurityManager();setWebSecurityManager(securityManager);FilterChainResolver resolver = createFilterChainResolver();if (resolver != null) {setFilterChainResolver(resolver);}}protected Map<String, Object> getDefaults() {Map<String, Object> defaults = new HashMap<String, Object>();defaults.put(FILTER_CHAIN_RESOLVER_NAME, new IniFilterChainResolverFactory());return defaults;}protected WebSecurityManager createWebSecurityManager() {//已经创建好的Ini对象Ini ini = getIni();if (!CollectionUtils.isEmpty(ini)) {factory.setIni(ini);}Map<String, Object> defaults = getDefaults();if (!CollectionUtils.isEmpty(defaults)) {factory.setDefaults(defaults);}//从WebIniSecurityManagerFactory实例中创建WebSecurityManagerWebSecurityManager wsm = (WebSecurityManager)factory.getInstance();//SHIRO-306 - get beans after they've been created (the call was before the factory.getInstance() call,//which always returned null.Map<String, ?> beans = factory.getBeans();if (!CollectionUtils.isEmpty(beans)) {this.objects.putAll(beans);}return wsm;}
}

接着看看WebIniSecurityManagerFactory的getInstance方法的实现

由图可见,在调用getInstance方法的时候,其实执行的是位于AbstractFactory中的getInstance方法

public abstract class AbstractFactory<T> implements Factory<T> {public T getInstance() {T instance;if (isSingleton()) {if (this.singletonInstance == null) {this.singletonInstance = createInstance();}instance = this.singletonInstance;} else {instance = createInstance();}if (instance == null) {String msg = "Factory 'createInstance' implementation returned a null object.";throw new IllegalStateException(msg);}return instance;}/** 子类(IniFactorySupport)实现创建实例的过程* */protected abstract T createInstance();
}
public abstract class IniFactorySupport<T> extends AbstractFactory<T> {public T createInstance() {/** 获取Ini对象,前面已经设置进来。* 如果ini对象不存在,还会从默认的路径来创建Ini对象* */Ini ini = resolveIni();T instance;if (CollectionUtils.isEmpty(ini)) { //如果Ini对象不存在,则调动子类(IniSecurityManagerFactory)使用默认的SecurityManager实例对象log.debug("No populated Ini available.  Creating a default instance.");instance = createDefaultInstance();if (instance == null) {String msg = getClass().getName() + " implementation did not return a default instance in " +"the event of a null/empty Ini configuration.  This is required to support the " +"Factory interface.  Please check your implementation.";throw new IllegalStateException(msg);}} else {log.debug("Creating instance from Ini [" + ini + "]");//调用子类(IniSecurityManagerFactory),根据Ini对象的信息来构建SecurityManager对象instance = createInstance(ini);if (instance == null) {String msg = getClass().getName() + " implementation did not return a constructed instance from " +"the createInstance(Ini) method implementation.";throw new IllegalStateException(msg);}}return instance;}protected abstract T createInstance(Ini ini);protected abstract T createDefaultInstance();
}
public class IniSecurityManagerFactory extends IniFactorySupport<SecurityManager> {public static final String MAIN_SECTION_NAME = "main";public static final String SECURITY_MANAGER_NAME = "securityManager";public static final String INI_REALM_NAME = "iniRealm";private ReflectionBuilder builder;public IniSecurityManagerFactory() {this.builder = new ReflectionBuilder();}//默认的SecurityManager对象【其实被WebIniSecurityManagerFactory复写,返回的是DefaultWebSecurityManager】protected SecurityManager createDefaultInstance() {return new DefaultSecurityManager();}//根据Ini来创建SecurityManager对象protected SecurityManager createInstance(Ini ini) {if (CollectionUtils.isEmpty(ini)) {throw new NullPointerException("Ini argument cannot be null or empty.");}SecurityManager securityManager = createSecurityManager(ini);if (securityManager == null) {String msg = SecurityManager.class + " instance cannot be null.";throw new ConfigurationException(msg);}return securityManager;}private SecurityManager createSecurityManager(Ini ini) {return createSecurityManager(ini, getConfigSection(ini));}//获取[main]的配置,如果没得到则获取默认的配置private Ini.Section getConfigSection(Ini ini) {Ini.Section mainSection = ini.getSection(MAIN_SECTION_NAME);if (CollectionUtils.isEmpty(mainSection)) {//try the default:mainSection = ini.getSection(Ini.DEFAULT_SECTION_NAME);}return mainSection;}@SuppressWarnings({"unchecked"})private SecurityManager createSecurityManager(Ini ini, Ini.Section mainSection) {/** 注意,createDefaults被子类WebIniSecurityManagerFactory复写,* 但其实也会首先调用本类的createDefaults方法,只是在结果中再添加了些默认的Filter实例。* * 然后将结果保存在ReflectionBuilder对象的objects【Map】属性中,此时里面包含了默认的SecurityManager、Realm以及各种默认Filter实例;* * 最后将createDefaults返回的Map全部加到ReflectionBuilder对象的objects【Map】中取缓存* */getReflectionBuilder().setObjects(createDefaults(ini, mainSection));//使用ReflectionBuilder构建对象【创建实例对象,加入到objects变量中,然后执行各个对象的init方法,同时返回objects对象】Map<String, ?> objects = buildInstances(mainSection);//直接从ReflectionBuilder对象中取出SecurityManager类型的对象SecurityManager securityManager = getSecurityManagerBean();/** 如果securityManager不为RealmSecurityManager类型则返回true;* 如果是RealmSecurityManager类型,但是里面没有Realm实例,返回为true;* 否则返回false* */boolean autoApplyRealms = isAutoApplyRealms(securityManager);if (autoApplyRealms) {//筛选其中的Realms对象【Realm或RealmFactory类型】Collection<Realm> realms = getRealms(objects);if (!CollectionUtils.isEmpty(realms)) {//如果securityManager不是RealmSecurityManager类型则抛出异常,否则给securityManager设置RealmsapplyRealmsToSecurityManager(realms, securityManager);}}return securityManager;}
}

到此,SecurityManager实例创建完成,并设置到IniWebEnvironment的属性objects[Map]中

public class IniWebEnvironment extends ResourceBasedWebEnvironment implements Initializable, Destroyable {protected void configure() {//Map<String, Object>对象this.objects.clear();WebSecurityManager securityManager = createWebSecurityManager();setWebSecurityManager(securityManager);//获取shiro.ini文件中配置的'filters' 或 'urls'项的Filter,加入objects对象中FilterChainResolver resolver = createFilterChainResolver();if (resolver != null) {setFilterChainResolver(resolver);}}
}

到此,Shiro的初始化过程完成,在EnvironmentLoaderListener 中将会把该IniWebEnvironment对象保存在ServletContext下供后面使用。

大致流程总结

系统启动的时候执行EnvironmentLoaderListener初始化方法并创建WebEnvironment实例,同时将实例对象保存到ServletContext中

1,创建WebEnvironment对象
1)读取web.xml中的上下文参数shiroEnvironmentClass
2)通过ServiceLoader方式查找jar包中的配置
3)是用默认的IniWebEnvironment类型

2,调用WebEnvironment的init方法初始化WebEnvironment实例
注:WebEnvironment构造犯法里面会创建WebIniSecurityManagerFactory实例factory。
1)从指定或默认的路径下解析shiro.ini文件生成Ini实例
2)将Ini实例设置给factory的ini属性
3)将默认的IniFilterChainResolverFactory设置给factory的defaultBeans(Map)属性
4)调用factory的getInstance方法创建SecurityManager对象
--解析Ini对象里面的信息,创建Realm等对象并设置给SecurityManager实例
5)将SecurityManager返回的objects(Map)添加到WebEnvironment的objects中。

默认的SecurityManager: DefaultWebSecurityManager

后面讲接着介绍Session和Realm的使用

转载于:https://blog.51cto.com/dengshuangfu/2362306

Apache Shiro源码解读之SecurityManager的创建相关推荐

  1. React源码解读之更新的创建

    React 的鲜活生命起源于 ReactDOM.render ,这个过程会为它的一生储备好很多必需品,我们顺着这个线索,一探婴儿般 React 应用诞生之初的悦然. 更新创建的操作我们总结为以下两种场 ...

  2. Shiro源码学习之二

    接上一篇 Shiro源码学习之一 3.subject.login 进入login public void login(AuthenticationToken token) throws Authent ...

  3. Shiro源码学习之一

    一.最基本的使用 1.Maven依赖 <dependency><groupId>org.apache.shiro</groupId><artifactId&g ...

  4. Android 开源框架之 Android-async-http 源码解读

    开源项目链接 Android-async-http仓库:https://github.com/loopj/android-async-http android-async-http主页:http:// ...

  5. tomcat源码解读(一)

    tomcat源码解读(一) 什么是 tomcat ? Tomcat是由Apache软件基金会下属的Jakarta项目开发的一个Servlet容器,按照Sun Microsystems提供的技术 规范, ...

  6. EasyExcel使用详解与源码解读

    EasyExcel使用详解 1.EasyExcel简单介绍 64M内存20秒读取75M(46W行25列)的Excel(3.0.2+版本) 2.EasyExcel和POI数据处理能力对比 3.使用Eas ...

  7. Apache Camel源码研究之Rest

    本文以Camel2.24.3 + SpringBoot2.x 为基础简单解读Camel中的Rest组件的源码级实现逻辑. 0. 目录 1. 前言 2. 源码解读 2.1 启动时 2.1.1 `Rest ...

  8. Apache Camel源码研究之Language

    Apache Camel通过Language将Expression和Predicate的构造操作合并在一起,减少了概念,也降低了扩展难度,是的整体架构更加清晰. 1. 概述 Apache Camel为 ...

  9. Activiti源码解读之TaskService

    activiti-5.17.0源码解读之TaskService 源码路径:activiti-5.17.0\modules\activiti-engine\src\main\java\org\activ ...

最新文章

  1. 如何用计算机求锐角三角比,9.3用计算器求锐角三角比教学案
  2. python代做收入-代写CSE205留学生程序 代做Python实验程序
  3. java网络编程与分布式计算_Java网络编程与分布式计算
  4. ICML2021 | Self-Tuning: 如何减少对标记数据的需求?
  5. Java包装类型对象比较相等性注意事项
  6. java设计一百亿的计算器_请设计一个一百亿的计算器
  7. multipartfile 获取音频时长_QQ音乐移动端加入倍速播放,蓄力长音频发展 | 产品观察...
  8. 关于OPENGL与OPENGL ES的区别
  9. php 压缩 解压文件,PHP 实现文件压缩解压操作的方法
  10. AIoT助力文旅产业,2020年5A景区数字化发展指数报告
  11. kx linux驱动下载,创新5.1声卡驱动kX Project Audio DriverV5.1免费版下载 - 下载吧
  12. 卡巴斯基破解版 KISV8.0.0.432 Beta 江南混混汉化特别版
  13. 机房火灾自动报警系统常见问题及解决方案
  14. 用计算机pol计算方位角,卡西欧计算方位角 计算器算方位角.doc
  15. 猿创征文|【实用工具tcping】ping tcping的区别,使用命令,超全超详细使用手册(建议收藏)
  16. 图说全球浏览器市场份额变迁史
  17. Android进阶之路 - 毛玻璃遮罩层
  18. 给力!低代码开发平台广州流辰信息科技助您增辉创价值!
  19. 全球前10大数据库产品厂家
  20. 独热编码(One-Hot Encoding)

热门文章

  1. Go实现Raft第四篇:持久化和调优
  2. 深入理解 PHP7 unset 真的会释放内存吗?
  3. SenchaTouch2.3.1 中使用listpaging以及pullrefresh插件 做的分页示例
  4. [Python] 函数lambda(), filter(), map(), reduce()
  5. 内核怎么帮程序建立连接的
  6. RabbitMQ学习笔记-RabbitMQ的运转流程
  7. Python精通-Python字典操作
  8. 通过QEMU-GuestAgent实现从外部注入写文件到KVM虚拟机内部
  9. 一文读懂 etcd 的 mvcc 实现
  10. 并发编程--用SingleFlight合并重复请求