【MyBatis源码分析】Configuration加载(上篇)
config.xml解析为org.w3c.dom.Document
本文首先来简单看一下MyBatis中将config.xml解析为org.w3c.dom.Document的流程,代码为上文的这部分:
static {
try {
reader = Resources.getResourceAsReader("mybatis/config.xml");
ssf = new SqlSessionFactoryBuilder().build(reader);
}
catch (IOException e) {
e.printStackTrace();
}
}
第3行的代码实现为:
public static Reader getResourceAsReader(String resource) throws IOException {
Reader reader;
if (charset == null) {
reader = new InputStreamReader(getResourceAsStream(resource));
} else {
reader = new InputStreamReader(getResourceAsStream(resource), charset);
}
return reader;
}
相当于就是将输入的路径转换为一个字符输入流并返回。
接着继续看静态块第4行的代码,new SqlSessionFactoryBuilder().build(reader),把代码定位到SqlSessionFactoryBuilder类的builder方法,这里使用了多态,直接跟到build方法:
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
解析config.xml的代码在第3行XMLConfigBuilder类的构造方法中,看一下XMLConfigBuilder类的构造方法做了什么:
public XMLConfigBuilder(Reader reader, String environment, Properties props) {
this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}
这里的关键是第二行代码的第一个参数XPathParser,看一下实例化XPathParser类的代码:
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(reader));
}
第2行的代码commonConstructor方法没什么好看的,将validation、variables、entityResolver设置到XPathParser类的参数中而已,顺便再实例化一个javax.xml.xpath.XPath出来,XPath用于在XML文档中通过元素和属性进行导航,并对元素和属性进行遍历。
接着看第3行的createDocument方法:
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(validation); factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true); DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(entityResolver);
builder.setErrorHandler(new ErrorHandler() {
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
} @Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
} @Override
public void warning(SAXParseException exception) throws SAXException {
}
});
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
看一下第5行~第11行的代码设置DocumentBuilderFactory中参数的含义:
- setValidating表示是否验证xml文件,这个验证是DTD验证
- setNamespaceAware表示是否支持xml命名空间
- setIgnoringComments表示是否忽略注释
- setIgnoringElementContentWhitespace表示是否忽略元素中的空白
- setCoalescing表示是否将CDATA节点转换为Text节点,并将其附加到相邻(如果有)的Text节点
- setExpandEntityReferences表示是否扩展实体引用节点
第13行的代码由设置的参数从DocumentBuilderFactory中获取一个DocumentBuilder实例DocumentBuilderImpl,并由第14行的代码设置一个实体解析器,由第15行~第29行的代码设置一个错误处理器。
最后看一下第30行的代码parse方法:
public Document parse(InputSource is) throws SAXException, IOException {
if (is == null) {
throw new IllegalArgumentException(
DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN,
"jaxp-null-input-source", null));
}
if (fSchemaValidator != null) {
if (fSchemaValidationManager != null) {
fSchemaValidationManager.reset();
fUnparsedEntityHandler.reset();
}
resetSchemaValidator();
}
domParser.parse(is);
Document doc = domParser.getDocument();
domParser.dropDocumentReferences();
return doc;
}
看过Spring配置文件解析源码的朋友应该对这一段代码比较熟悉,一样的,使用DocumentBuilder将解析InputSource成org.w3c.dom.Document并将Document存储到XPathParser中。
Document转换为Configuration
前面的代码将config.xml转换为了org.w3c.dom.Document,下一步就是将org.w3c.dom.Document中的内容转换为Java对象了,其中最主要的一个对象就是org.apache.ibatis.session.Configuration,还是回到之前的SqlSessionFactoryBuilder的build方法:
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
先看一下第4行的parse方法,parse方法是XMLConfigBuilder中的,之前重点分析了它的属性XPathParser,看一下XMLConfigBuilder的parse方法是如何实现的:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
这里看一下第6行,可以使用XPathParser的evalNode方法解析标签,后面解析标签会大量用到此方法,此方法将标签解析为XNode,像config.xml(可见上一篇文章的示例)解析完之后的XNode,toString()方法输出的内容是这样的:
<configuration>
<properties resource="properties/db.properties"/>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="useGeneratedKeys" value="true"/>
</settings>
<typeAliases>
<typeAlias alias="Mail" type="org.xrq.mybatis.pojo.Mail"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driveClass}"/>
<property name="url" value="${url}"/>
<property name="username" value="${userName}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mybatis/mail.xml"/>
</mappers>
</configuration>
可见xml文件中<configuration>中所有内容都已经被成功解析并放在XNode中了,剩下的只要调用XNode的方法获取自己想要的内容即可。
最后扫一眼parseConfiguration方法,之所以说扫一眼,因为之后要分析里面的一些常用的和重点的内容,这里只是列一下代码而已:
private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.evalNode("settings"));
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
这里就是逐个解析<configuration>标签下的子标签,并将数据设置到对应的属性中,这里要一个一个看一下。
settings解析
首先看settingsAsPropertiess(root.evalNode("settings"))这句代码,显而易见这句话获取了<configuration>下的<settings>节点。跟一下代码的实现:
private Properties settingsAsPropertiess(XNode context) {
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}
第5行将节点解析成键值对的形式(Properties是Hashtable的子类),看一下props的toString方法打印的内容:
{useGeneratedKeys=true, lazyLoadingEnabled=true, cacheEnabled=true}
可见settings里面的数据已经被解析成了Properties了。之后还有一步,<settings>标签下的每个<setting>中的name属性不是随便填写的,都是MyBatis支持的配置,因此需要对Properties里面的Key做一个校验,校验的代码就是第7行至第12行的代码,其中有一个name不是MyBatis支持的就会抛出异常,MyBatis初始化整体失败。
至于具体校验的是哪些Key,这就要跟一下第7行的代码了,首先是MetaClass.forClass(Configuration.class, localReflectorFactory),第二个实参是XMLConfigBuilder里面直接new出来的,它的实际类型为DefaultReflectorFactory,看一下forClass方法实现:
public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
return new MetaClass(type, reflectorFactory);
}
看一下new MetaClass做了什么事:
private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
this.reflectorFactory = reflectorFactory;
this.reflector = reflectorFactory.findForClass(type);
}
显而易见,继续跟一下第3行的代码DefaultRelectorFactory的findForClass方法:
public Reflector findForClass(Class<?> type) {
if (classCacheEnabled) {
// synchronized (type) removed see issue #461
Reflector cached = reflectorMap.get(type);
if (cached == null) {
cached = new Reflector(type);
reflectorMap.put(type, cached);
}
return cached;
} else {
return new Reflector(type);
}
}
不管怎么样都会执行new Reflector(type)这一句代码,看一下此时做了什么事,注意传入的参数是Configuration的class对象:
public Reflector(Class<?> clazz) {
type = clazz;
addDefaultConstructor(clazz);
addGetMethods(clazz);
addSetMethods(clazz);
addFields(clazz);
readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writeablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
这么多方法至于具体要看哪个,要注意的是之前XMLConfigBuilder里面对于Key的判断是"!metaConfig.hasSetter(String.valueOf(key))",代码的意思是判断的是否Key有set方法,那么显而易见这里要继续跟第5行的addSetMethods方法:
private void addSetMethods(Class<?> cls) {
Map<String, List<Method>> conflictingSetters = new HashMap<String, List<Method>>();
Method[] methods = getClassMethods(cls);
for (Method method : methods) {
String name = method.getName();
if (name.startsWith("set") && name.length() > 3) {
if (method.getParameterTypes().length == 1) {
name = PropertyNamer.methodToProperty(name);
addMethodConflict(conflictingSetters, name, method);
}
}
}
resolveSetterConflicts(conflictingSetters);
}
到这里应该很明显了,结论就是:<setting>的name属性对应的值,必须在Configuration类有相应的Setter,比如设置了一个属性useGenerateKeys方法,那么必须在Configuration类中有setUseGenerateKeys方法才行。
顺便说一下第13行有一个resolveSetterConflicts方法,其作用是:Setter有可能在类中被重载导致有多个,此时取Setter中方法参数只有一个且参数类型与Getter一致的Setter。
properties解析
接着看一下propertiesElement(root.evalNode("properties"))方法,这句读取的是<configuration>下的<properties>节点,代码实现为:
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
Properties defaults = context.getChildrenAsProperties();
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}
看到第4行~第7行的代码指定了MyBatis的<properties>标签下不能同时指定"resource"属性和"url"属性。
接着第9行~第13行的代码将.properties资源解析为Properties类,最后将Properties类设置到XPathParser和Configuration的variables属性中,variables是一个Propreties变量。
类型别名解析
跳过loadCustomVfs(settings)直接看typeAliasesElement(root.evalNode("typeAliases"))这行,因为前者我也没看懂干什么用的,后者是用于定义类型的别名的,解析的是<configuration>下的<typeAliases>标签,用过MyBatis的应该很熟悉。看一下源码实现:
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
从源码实现中我们可以知道两点,<typeAliases>标签下可以定义<package>和<typeAlias>两种标签,但是看第4行和第7行的判断,这是一段if...else...,因此可以知道<package>标签和<typeAlias>标签只能定义其中的一种。首先看一下解析<package>标签的代码,第6行的registerAliases方法:
public void registerAliases(String packageName, Class<?> superType){
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for(Class<?> type : typeSet){
// Ignore inner classes and interfaces (including package-info.java)
// Skip also inner classes. See issue #6
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}
第3行根据路径packageName寻找它下面的".class"文件拿到所有的".class"文件对应的类的Class,然后遍历所有的Class,做了三层判断
- 必须不是匿名类
- 必须不是接口
- 必须不是成员类
此时此Class对应的类符合条件,会进行注册,通过registerAlias方法进行注册,看一下方法实现:
public void registerAlias(Class<?> type) {
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}
第2行获取Class的simpleName,simpleName指的是移除了包名的名称,比如aa.bb.cc.Mail,getSimpleName()获取的就是Mail。
第3行获取类上面的注解Alias,如果Alias注解中有定义value属性且指定了值,那么第4行~第6行的判断优先取这个值作为Class的别名。
第7行注册别名:
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
String key = alias.toLowerCase(Locale.ENGLISH);
if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
}
TYPE_ALIASES.put(key, value);
}
其实就做了两步操作:
- 将alias全部小写
- 将alias以及Class对象放到TYPE_ALIASES中,TYPE_ALIASES是一个HashMap
这样一个流程,就将<package>标签name属性路径下的Class(如果符合要求),全部放到了HashMap中以供使用。
接着看一下<typeAlias>标签的解析,也就是前面说的else部分:
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
这里先解析<typeAlias>中的alias属性,再解析<typeAlias>中的type属性,当然alias也可以不定义,不定义走的就是第6行的registerAlias方法,定义走的就是第8行的registerAlias方法,这两个重载的registerAlias方法前面也都说过了,就不说了。
默认typeAlias
上面说的是自定义typeAlias,MyBatis本身也默认提供给开发者了一些typeAlias定义,在两处地方。第一处地方在Configuration的构造方法中:
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class); typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); ...
}
第二处地方是在TypeAliasRegistry的构造方法中:
public TypeAliasRegistry() {
registerAlias("string", String.class); registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class); registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
registerAlias("short[]", Short[].class);
registerAlias("int[]", Integer[].class);
registerAlias("integer[]", Integer[].class);
registerAlias("double[]", Double[].class);
registerAlias("float[]", Float[].class);
registerAlias("boolean[]", Boolean[].class); registerAlias("_byte", byte.class);
registerAlias("_long", long.class);
registerAlias("_short", short.class);
registerAlias("_int", int.class);
registerAlias("_integer", int.class);
registerAlias("_double", double.class);
registerAlias("_float", float.class);
registerAlias("_boolean", boolean.class); registerAlias("_byte[]", byte[].class);
registerAlias("_long[]", long[].class);
registerAlias("_short[]", short[].class);
registerAlias("_int[]", int[].class);
registerAlias("_integer[]", int[].class);
registerAlias("_double[]", double[].class);
registerAlias("_float[]", float[].class);
registerAlias("_boolean[]", boolean[].class); registerAlias("date", Date.class);
registerAlias("decimal", BigDecimal.class);
registerAlias("bigdecimal", BigDecimal.class);
registerAlias("biginteger", BigInteger.class);
registerAlias("object", Object.class); registerAlias("date[]", Date[].class);
registerAlias("decimal[]", BigDecimal[].class);
registerAlias("bigdecimal[]", BigDecimal[].class);
registerAlias("biginteger[]", BigInteger[].class);
registerAlias("object[]", Object[].class); registerAlias("map", Map.class);
registerAlias("hashmap", HashMap.class);
registerAlias("list", List.class);
registerAlias("arraylist", ArrayList.class);
registerAlias("collection", Collection.class);
registerAlias("iterator", Iterator.class); registerAlias("ResultSet", ResultSet.class);
}
对于这些数据,我们可以直接使用registerAlias方法的第一个参数对应的字符串而不需要定义这些typeAlias。
【MyBatis源码分析】Configuration加载(上篇)的更多相关文章
- mybatis源码分析--如何加载配置及初始化
简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ...
- springboot集成mybatis源码分析-启动加载mybatis过程(二)
1.springboot项目最核心的就是自动加载配置,该功能则依赖的是一个注解@SpringBootApplication中的@EnableAutoConfiguration 2.EnableAuto ...
- Mybatis源码解析(二) —— 加载 Configuration
Mybatis源码解析(二) -- 加载 Configuration 正如上文所看到的 Configuration 对象保存了所有Mybatis的配置信息,也就是说mybatis-config. ...
- MyBatis 源码篇-资源加载
本章主要描述 MyBatis 资源加载模块中的 ClassLoaderWrapper 类和 Java 加载配置文件的三种方式. ClassLoaderWrapper 上一章的案例,使用 org.apa ...
- Spring Boot源码分析-配置文件加载原理
在Spring Boot源码分析-启动过程中我们进行了启动源码的分析,大致了解了整个Spring Boot的启动过程,具体细节这里不再赘述,感兴趣的同学可以自行阅读.今天让我们继续阅读源码,了解配置文 ...
- 精尽Spring Boot源码分析 - 配置加载
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
- Mybatis 源码分析--Configuration.xml配置文件加载到内存
(补充知识点: 1 byte(字节)=8 bit(位) 通常一个标准英文字母占一个字节位置,一个标准汉字占两个字节位置:字符的例子有:字母.数字系统或标点符号) 1.创建SqlSessionFacto ...
- Mybatis源码解读-配置加载和Mapper的生成
问题 Mybatis四大对象的创建顺序? Mybatis插件的执行顺序? 工程创建 环境:Mybatis(3.5.9) mybatis-demo,参考官方文档 简单示例 这里只放出main方法的示例, ...
- Spring源码分析之-加载IOC容器
本文接上一篇文章 SpringIOC 源码,控制反转前的处理(https://mp.weixin.qq.com/s/9RbVP2ZQVx9-vKngqndW1w) 继续进行下面的分析 首先贴出 Spr ...
- 第一次源码分析: 图片加载框架Picasso源码分析
使用: Picasso.with(this) .load("http://imgstore.cdn.sogou.com/app/a/100540002/467502.jpg") . ...
随机推荐
- 【Unity3D技术文档翻译】第1.5篇 本地使用 AssetBundles
上一章:[Unity3D技术文档翻译]第1.4篇 AssetBundle 依赖关系 本章原文所在章节:[Unity Manual]→[Working in Unity]→[Advanced Devel ...
- Python自动化--语言基础8--接口请求及封装
基于http协议,最常用的是GET和POST两种方法. 接口文档需要包含哪些信息: 接口名称接口功能接口地址支持格式 json/xml请求方式请求示例请求参数(是否必填.数据类型.传递参数格式)返回参 ...
- Java经典编程题50道之七
输入一行字符,分别统计出其中英文字母.空格.数字和其它字符的个数. public class Example07 { public static void main(String[] args) ...
- css渲染(一) 字体和文本
一.字体属性 1.默认字体系列 chrome/opera:"宋体" firefox:"微软雅黑" safari/IE:Times,"宋体" ...
- Sphinx下载、安装、配置、Hello World、文档阅读
sphinx下载.安装.配置.Hello World.查看文档
- C#标识符
- hdu4825 01字典树+贪心
从高位向低位构造字典树,因为高位得到的数更大. AC代码: #include<cstdio> using namespace std; typedef long long LL; cons ...
- [Code] 递归函数在函数式 Java 中的实现
这里以阶乘函数为例,对于阶乘函数 fact :: Integer -> Integer fact 0 = 1 fact n = n * fact (n - 1) 在函数式 Java 中可以使用 ...
- HI3531例子程序说明
Hisilicon Hi35xx 样例程序使用说明 1. 样例程序文件结构说明 sample # MPP 样例程序 |-- common # hi35x ...
- 【Thinkphp】入口文件和配置文件
一.入口文件 ThinkPHP采用单一入口模式进行项目部署和访问 入口文件代码 <?php define('APP_DEBUG',TRUE);//打开调试模式 在生产环境中应该关闭 define ...