ibatis
2016-02-25 16:08
302 查看
配置
applicationContext.xml
sqlmapConfig.xml
SqlMapClientFactoryBean 初始化
因为他是个FactoryBean,所以真正的类型在get(sqlMapClient)方法中
他又实现InitializingBean
因此,实例化这个对象后,初始化afterPropertiesSet()方法
主要是创建 SqlMapClient 和事务(当没有指定什么事务时,就会创建一个空的事务,里面的方法都是空的)
TransactionAwareDataSourceProxy 这个主要是getConnection()中,创建代理对象
拦截的是
1) close() :DataSourceUtils.doReleaseConnection(con,dataSource);
2) getTargetConnection() : DataSourceUtils.doGetConnection(dataSource) ;
3) 别的方法: 这个方法执行后,DataSourceUtils. doReleaseConnection(con,dataSource)
以下是重要类的属性和流程图
![](https://img-blog.csdn.net/20160225163747882)
下面是解析的流程图
![](https://img-blog.csdn.net/20160225164933180)
介绍下解析类NodeletParser
这里主要存放一些处理节点的接口
这个process里面就是处理节点的
简介SqlMapConfigParser解析sqlConfig.xml文件
1) delegate = new SqlMapExecutorDelegate()
2)mapClient = new SqlMapClientImpl()
3) 处理xml中的各个节点
下面来简述下 ibatis的缓存
当在解析语句的时候insert, update sql等时,当这个语句是有缓存的时候就把他
1) GenernalStatement 如果增删改的时候,和这实体类有关的缓存都清除
2) CachingStatement :缓存语句:在查询的时候将查询结果放入进去
这个model放进去的时候有两种情况
总结: 在增删改时,会清理缓存 ; 查询的时候,插入到缓存中
观察者模式 ,即 语句是发布者,cachemodel是观察者。当语句被更改的时候,通知cachemodel来清理缓存中的数据(当这个实体类中的数据被修改时,对于这个实体类的缓存数据全部被删除;解析的时候语句封装成CachingStatement ,查询的时候直接从缓存中取出,如果没有数据再从数据库取出)
介绍下CacheModel:
从缓存中获取数据时,超过清理时间就清理缓存。对于要序列化对象,在获取和插入时都序列化下.具体的怎么插入什么的还是要看是什么策略
缓存策略:
1) FIFO – FifoCacheController :这个遵循先进去缓存先被删除(短时间内使用过就不再使用情况下,建议使用)
插入:判断是否已经达到缓存中的最大值,就删除最早放进去的;插入到缓存中
获取:直接从数据中获取
2) LRU – LruCacheController :这个遵循近期使用最少的被删除,一个对象总是被使用,被删除的几率很少(长期时间内,用户经常使用这个对象 ,比如查询关键字翻页)
插入:判断是否已经达到缓存中的最大值,就删除最早放进去的;插入到缓存中
获取:放在最近的位置上,再获取
3) MEMORY – MemoryController:
这个再等等 ,要看jvm java回收机制
根据reference-type来创建
SoftReference:内存不足时删除
WeakReference:java垃圾回收时,就删除
StrongReference
4) OSCACHE – OSCaheContoller :他是一个插件。以后再将
总结ibatis缓存: 1)在解析完xml后,把缓存
d790
放入到Statement的List中 2)如果这条语句是有缓存的话,那就包装成CachingStatement ,在查询后把结果放入到缓存中。增删改时,通知缓存去清理数据 3)缓存插入时,时会记录刷新的时间,如果这个是不可读时先序列化一下再放入缓存中。取出时,如果超过时间了,就清理缓存。是不可读时饭序列化一下 4) 缓存4大策略: I)fifo :先进先被删除 II) lru 最近很少被使用的就被删除 III) memory 引用策略 SoftReference:内存不足时删除,WeakReference:java垃圾回收时,就删除 IIII) 插件OSCache
数据源有3种情况:SIMPLE,DBCP,JNDI
解析xml时,是根据DataSourceFactory来获取的
基本接口
1)JNDI :这个是搜索上下文来寻找的
属性名也可以是DBIndiContext、DBFullJndiContext
2)DBCP :BasicDataSource
3) SimpleDataSourceFactory
这里的就大体上看下
这里的dataSource.getConnection() 返回的是代理对象(因为他只能确定基类是Connection,不能确定客户到底使用的是哪个连接(驱动)) 。只拦截Connection的方法
1)close() : datasource.pushConnection(this)将连接返回到连接池中
2)其余方法则放过
使用的时候从方法池中取出,关闭的时候放入到连接池中。
getConnection() 时(获取连接): 从连接池中取出
一) 当在连接池中有空闲连接时, 直接从连接池中取出
二) 创建连接
I) 当使用中的连接<最大数时(还可以再创建时) ,直接去创建新的连接
II) 取出最早的连接
a) 当这个连接被占用的时间超时时,就从使用的连接池中删除,并包装成新的连接
b) 线程等一会(等到有线程将连接放入到连接池中被唤醒)
当这个连接可以使用时,就放入到使用的线程池中;否则计入到错误数中
当这个错误数很大时,就抛异常。
把这个连接放入到连接池中
当这个连接可用
如果连接池中的连接数 < 允许最大空闲数 重新包装 放入连接池
否则 关闭连接
- 初始化
启动事务由2种
1) 是不用ibatis推荐的三种dataSource
直接在SqlMapClientFactoryBean中的afterProperties中
如果 dataSource不为空时
2) 用自带的
就在解析完sqlConfig.xml中中
在介绍事务之前,先讲下Throttle
就是达到一定数量之后线程就开始等待,直到有线程把他唤醒
TransactionManager :
简述下就是开始一个事务的时候(当事务数超出时,这个线程就一直在等待;直到有事务结束后将线程唤醒。事务结束时,当没有被提交,那么就回滚,关闭连接)
事务的三种方式:
1. jdbc – JdbcTransactionConfig: 就是在获取连接的时候将con.setAutoCommit(false);提交回滚都用con的
2. JTA – JtaTransactionConfig 用的是UserTransaction第三方插件
3. EXTERNAL –ExternalTransactionConfig (默认 ) 里面的提交回滚什么的都不操作
![](https://img-blog.csdn.net/20160226144804780)
总结: insert 流程 1. ThreadLocal 这个是一个线程一个变量 确保不会出现并发的行为 (只有在这个线程的SessionImpl被关闭了或者null才创建) 2.从线程池中取出Session ,然后开启一个事务 ,具体的和jdbc操作的交给别的 。提交事务 然后关闭事务,把这个session放入到线程池中,将这个线程的SessionImpl关闭
以下是简单的代码.
- SqlMapClientImpl
applicationContext.xml
<bean id = "sqlmapClientBuilder" class = "SqlMapClientFactoryBean"> <property name="dataSource" ref="mysqldbDataSource"/> <property name="sqlMapConfigResource" value="sqlmapConfig.xml"></property> </bean> //处理FactoryBean <bean id="mysqldbSqlMapClient" factory-bean="sqlmapClientBuilder" factory-method="get">
sqlmapConfig.xml
<sqlMapConfig> <setting cacheModelsEnabled="true" statementCachingEnabled = "true" classInfoCacheEnabled="true" /> <sqlMap resource="com/xianling/bus/id/entity/sqlmap/IdGen.xml"/> </sqlMapConfig>
SqlMapClientFactoryBean 初始化
因为他是个FactoryBean,所以真正的类型在get(sqlMapClient)方法中
他又实现InitializingBean
因此,实例化这个对象后,初始化afterPropertiesSet()方法
主要是创建 SqlMapClient 和事务(当没有指定什么事务时,就会创建一个空的事务,里面的方法都是空的)
SqlMapClient sqlMapClient; Class transactionConfigClass = ExternalTransactionConfig.class; public void afterPropertiesSet() { sqlMapClient = buildSqlMapClient(…);//解析xml. if(dataSource != null) { TransactionConfig config = transactionConfigClass.newInstance(); // DataSource toUse = new TransactionAwareDataSourceProxy (dataSource); config.setDataSource(toUse); config.initialize(transactionConfigProperties); Config.setMaximunConcurrentTransactions(sqlMapClient.getDelegate().getMaximunConcurrentTransactions()); sqlMapClient.getDelegate().setTxManager(new TransactionManager(config) ); } protected SqlMapClient buildSqlMapClient( Resource[] configLocations,Properties properties){ SqlMapClient client = null; SqlMapConfigParser configParser = new SqlMapConfigParser(); for (int i = 0; i < configLocations.length; i++) { InputStream is = configLocations[i].getInputStream(); client = configParser.parse(is, properties); } return 4000 client; }
TransactionAwareDataSourceProxy 这个主要是getConnection()中,创建代理对象
拦截的是
1) close() :DataSourceUtils.doReleaseConnection(con,dataSource);
2) getTargetConnection() : DataSourceUtils.doGetConnection(dataSource) ;
3) 别的方法: 这个方法执行后,DataSourceUtils. doReleaseConnection(con,dataSource)
以下是重要类的属性和流程图
下面是解析的流程图
介绍下解析类NodeletParser
这里主要存放一些处理节点的接口
这个process里面就是处理节点的
public interface Nodelet { void process (Node node) throws Exception; }
NodeletParser { Map<String,Nodelet> letMap = new HashMap(); public void addNodelet(String xpath, Nodelet nodelet) { letMap.put(xpath, nodelet); } public void parse(Reader reader) throws NodeletException { parse(createDocument(reader).getLastChild()); //遍历他的字节点,根据节点路径找到Nodelet来调用他的process(node) } } }
简介SqlMapConfigParser解析sqlConfig.xml文件
1) delegate = new SqlMapExecutorDelegate()
2)mapClient = new SqlMapClientImpl()
3) 处理xml中的各个节点
下面来简述下 ibatis的缓存
<cacheModel id="user-cache" type="LRU" readOnly="false" serialize="true"> <flushInterval hours="24" /> <flushOnExecute statement="getUser" /> </cacheModel>
当在解析语句的时候insert, update sql等时,当这个语句是有缓存的时候就把他
Public MappedStatement SqlStatement.parseGenernalStatement(Node,GeneralStatement) { cacheName = node.getAttribute(“cacheMode”); …. If(cacheName != null) Return new CachingStatement(statement, degate.getCacheMode(cacheName)); Return statement; }
ibatis缓存
下面来介绍下普通的1) GenernalStatement 如果增删改的时候,和这实体类有关的缓存都清除
Public int executeUpdate(RequestScope, Transaction, parameter) { Request.getSession().setCommitRequired(true);//这个事务是可以提交的。 //如果有一起批量更新的话,就加入到批量更新中;否则直接更新 If(request.getSession().isInBatch()) sqlExecutor.excuteUpdate(request,con,sql,para); else sqlExecutor.addBatch request,con,sql,para); sql.cleanup(request); notifyListeners();//通知清理缓存 } Public void notifyListeners(){ for(CacleModel model:executeListemers) { Model.onExecuteStatement(this); //缓存的清理cacheControll.flush() } }
2) CachingStatement :缓存语句:在查询的时候将查询结果放入进去
MappedStatement statement,CacheModel model; Public int executeUpdate(…) {return statement.executeUpdate(…);} Public executeQueryForList (…) { CacheKey cacheKey = get CacheKey(…); Object list = model.getOject(cacheKey); if(list == null) list = statement. executeQueryForList(…); model.putObject(cacheKey,list); return list; }
这个model放进去的时候有两种情况
//这里的type有FIFO、LRU、MEMORY、OSCACHE <cacheModel id="user-cache" type="LRU" readOnly="false" serialize="true"> <flushInterval hours="24" /> <flushOnExecute statement="getUser" /> <property value="600" name="size" /> </cacheModel〉 parser.addNodelet(“/sqlMapConfig/end()”,new Nodelet(){ Public void process(Node n){ For(CacheModel model : delegate.getCacheModes().valueSet()){ For(String key:model.getFlushTriggerStatementNames()) { MappedStatement statement = delegate.get MappedStatement(key); Statement.addExecuteListener(model); } }}}
总结: 在增删改时,会清理缓存 ; 查询的时候,插入到缓存中
下面重点来说下缓存
<sqlMap> <cacheModel id="user-cache" type ="LRU" readOnly="true" serialize="false"> <flushInterval hours="24"/> <flushOnExecute statement="updateUser"/> <flushOnExecute statement="insertUser"/> <flushOnExecute statement="deleteUser"/> <property value="500" name="size"/> </cacheModel> <statement id="getProductList" cacheModel=" user-cache " > select * from PRODUCT </statement> <statement id="getProduct" parameterClass="int" cacheModel=" user-cache" > select * from PRODUCT where id = #value# </statement> </sqlMap>
观察者模式 ,即 语句是发布者,cachemodel是观察者。当语句被更改的时候,通知cachemodel来清理缓存中的数据(当这个实体类中的数据被修改时,对于这个实体类的缓存数据全部被删除;解析的时候语句封装成CachingStatement ,查询的时候直接从缓存中取出,如果没有数据再从数据库取出)
介绍下CacheModel:
CacheModel implements ExecuteListener{ CacheController controller; //初始化的设置属性参数 Public void configure (props) { controller.configure(props);} //清理缓存 Public void flush (){ Controller.flush(this); lastFlush = System.currentTime();} Public Object getObject (CacheKey key) { Object value = null; Synchronized(this) { If(System.currentTime() – latFlush > flushInterval) flush() ; Value = controller.getObject(key); If(serialize && !readOnly) { Value = serializeOut(value) ; } } Return value; } Public void putObject (CacheKey key,Object value) { Synchronized(this) { If(serialize && !readOnly) { Value = serializeIn(value) ; } Controller.putObject(this,key,value); }} }
从缓存中获取数据时,超过清理时间就清理缓存。对于要序列化对象,在获取和插入时都序列化下.具体的怎么插入什么的还是要看是什么策略
缓存策略:
1) FIFO – FifoCacheController :这个遵循先进去缓存先被删除(短时间内使用过就不再使用情况下,建议使用)
插入:判断是否已经达到缓存中的最大值,就删除最早放进去的;插入到缓存中
获取:直接从数据中获取
Map cache = Collections.synchronizedMap(new HashMap(); int cacheSize = 100; List keys = Collections.syn..(new ..()); Public void configure (props) { String size = props.getProperty(“cache-szie”); Size = size != null? Size : props.getProperty(“size”); cacheSize = size != null? Size : cacheSize; } Public void putObject(CacheModel cachemodel, key, value) { Cache.put(key, value); keys.add(key); if(cache.szie() > cacheSize) { String old = key.remove(key); cache.remove(old); } } Public Object getObject(CacheModel cachemodel, key) { return cache.get(key); }
2) LRU – LruCacheController :这个遵循近期使用最少的被删除,一个对象总是被使用,被删除的几率很少(长期时间内,用户经常使用这个对象 ,比如查询关键字翻页)
插入:判断是否已经达到缓存中的最大值,就删除最早放进去的;插入到缓存中
获取:放在最近的位置上,再获取
LruCacheController .. { Configure和putObject 和FIFO的一样.. Public Object getObject(CacheModel cachemodel, key) { Object value = cache.get(key); keyList.remove(key); key.add(key); return value; } }
3) MEMORY – MemoryController:
这个再等等 ,要看jvm java回收机制
根据reference-type来创建
SoftReference:内存不足时删除
WeakReference:java垃圾回收时,就删除
StrongReference
4) OSCACHE – OSCaheContoller :他是一个插件。以后再将
总结ibatis缓存: 1)在解析完xml后,把缓存
d790
放入到Statement的List中 2)如果这条语句是有缓存的话,那就包装成CachingStatement ,在查询后把结果放入到缓存中。增删改时,通知缓存去清理数据 3)缓存插入时,时会记录刷新的时间,如果这个是不可读时先序列化一下再放入缓存中。取出时,如果超过时间了,就清理缓存。是不可读时饭序列化一下 4) 缓存4大策略: I)fifo :先进先被删除 II) lru 最近很少被使用的就被删除 III) memory 引用策略 SoftReference:内存不足时删除,WeakReference:java垃圾回收时,就删除 IIII) 插件OSCache
数据源 ## 当然也可以不用这个自带的,直接在SqlMapClientFactoryBean的属性dataSource上
< transactionManager type ="JDBC" ><!-- 定义了ibatis的事务管理器有3中(JDBC,JTA,EXTERNAL) --> < dataSource type ="SIMPLE" ><!-- type属性指定了数据源的链接类型,也有3种类型(SIMPLE,DBCP,JNDI) --> < propertyname ="JDBC.Driver" value="" .... "/> <!-- Pool.MaximumActiveConnections 连接池维持的最大容量 Pool.MaximumIdleConnections 连接 池允许挂起的最大连接 Pool.MaximumCheckoutTime 连接被某个任务所允许占用的最大时间 TimeToWait 线程允许等待的最大时间--> </ dataSource > </ transactionManager >
数据源有3种情况:SIMPLE,DBCP,JNDI
解析xml时,是根据DataSourceFactory来获取的
基本接口
DataSourceFactory{ Public void initialize(Map) ; public DataSource getDataSource(); }
1)JNDI :这个是搜索上下文来寻找的
属性名也可以是DBIndiContext、DBFullJndiContext
JndiDataSourceFactory { DataSource dataSource; public void initialize(Map){ IntialContxt c = new IntialContxt(map); dataSource = c.lookup(map.get(“DataSource”)) ; } … }
2)DBCP :BasicDataSource
DbcpDataSourceFactory{ DataSource dataSource; public void initialize(Map){ DbcpConfiguration dbcp = new DbcpConfiguration (map); dataSource = dbcp.getDataSource(); } // 就是这个 //BasicDataSource bds = new ..(); //.. 设置属性 //Reurn dbs; }
3) SimpleDataSourceFactory
SimpleDataSourceFactory { DataSource dataSource; public void initialize(Map){ dataSource = new SimpleDataSource(map); } }
这里的就大体上看下
这里的dataSource.getConnection() 返回的是代理对象(因为他只能确定基类是Connection,不能确定客户到底使用的是哪个连接(驱动)) 。只拦截Connection的方法
1)close() : datasource.pushConnection(this)将连接返回到连接池中
2)其余方法则放过
使用的时候从方法池中取出,关闭的时候放入到连接池中。
getConnection() 时(获取连接): 从连接池中取出
一) 当在连接池中有空闲连接时, 直接从连接池中取出
二) 创建连接
I) 当使用中的连接<最大数时(还可以再创建时) ,直接去创建新的连接
II) 取出最早的连接
a) 当这个连接被占用的时间超时时,就从使用的连接池中删除,并包装成新的连接
b) 线程等一会(等到有线程将连接放入到连接池中被唤醒)
当这个连接可以使用时,就放入到使用的线程池中;否则计入到错误数中
当这个错误数很大时,就抛异常。
把这个连接放入到连接池中
当这个连接可用
如果连接池中的连接数 < 允许最大空闲数 重新包装 放入连接池
否则 关闭连接
ibatis 事务
一般不用这个事务,因为这边的是一个insert一个事务,他没办法保证多个insert是在一个事务里。一般都用spring 事务DataSourceTransactionManager
- 初始化
启动事务由2种
1) 是不用ibatis推荐的三种dataSource
直接在SqlMapClientFactoryBean中的afterProperties中
如果 dataSource不为空时
2) 用自带的
就在解析完sqlConfig.xml中中
TransactionConfig config = Class.forName(type ).newInstance(); Config.setDataSource(dataSource);config.setMaxImumConcurrentTransactions(..); Config.initialize(txtProps); TransactionManager m = new TransactionManager(config); m.setForceCommit(…)) Delegate.setTxManager(m)
在介绍事务之前,先讲下Throttle
就是达到一定数量之后线程就开始等待,直到有线程把他唤醒
Throttle { Int count,limit; final Object LOCK = new Object(); public void increment() { synchronized(LOCK) { if(count >= limit) LOCK.wait(); Count++; } } Public void decrement() { synchronized(LOCK) { LOCK.notify();Count --; } }}
TransactionManager :
简述下就是开始一个事务的时候(当事务数超出时,这个线程就一直在等待;直到有事务结束后将线程唤醒。事务结束时,当没有被提交,那么就回滚,关闭连接)
TransactionManager { TransactionConfig config,Throttle t, Boolean forceCommit; Public TransactionManager(config) { …,t = new Throttle (config.getMaximumConcurrentTransactions()); } // 开启事务 Public void begin (SessionScope session,int isolation) { If(session.getTransactionState() == started || == user_provided) throw new .. Transaction tran = null; t.increment(); try{ tran = config.newTransaction(isolation); session.setCommitRequired(false); }catch(…){ T.decrement(); } Session.setTransaction(tran); Session.setTransactionState(started); } // 提交事务 Public void commit(SessionScope session) { If(state != started && state != committed) throw new .. If(session.isCommitRequired() || forceCommit){ Session.getTransaction().commit(); session.setCommitRequired(false); } Session.setTransactionState(committed); } //结束事务 Public void end (SessionScope session) { Transaction tran = session.getTransaction(); Try{ if(tran != null) { If(state != committed) { If(session.isCommitRequired() || forceCommiit) tran.rollback(); session.setCommitRequired(false); } } }finally { Tran.close(); if(state != ended) t.decrement(); } Session.setTransaction(null); Session.setTransactionState(ended); }
事务的三种方式:
1. jdbc – JdbcTransactionConfig: 就是在获取连接的时候将con.setAutoCommit(false);提交回滚都用con的
2. JTA – JtaTransactionConfig 用的是UserTransaction第三方插件
3. EXTERNAL –ExternalTransactionConfig (默认 ) 里面的提交回滚什么的都不操作
具体实现执行sql
以下是执行sql示意图总结: insert 流程 1. ThreadLocal 这个是一个线程一个变量 确保不会出现并发的行为 (只有在这个线程的SessionImpl被关闭了或者null才创建) 2.从线程池中取出Session ,然后开启一个事务 ,具体的和jdbc操作的交给别的 。提交事务 然后关闭事务,把这个session放入到线程池中,将这个线程的SessionImpl关闭
以下是简单的代码.
- SqlMapClientImpl
SqlMapClientImpl { ThreadLocal<SqlMapSessionImpl> session = new ..(); Public Object insert (String id,Object param) { Return getLocalSqlMapSession() .insert(id,param); } Public int update(String id,Object param) { Return getLocalSqlMapSession().update(id,param); }…….所有的都交给了SqlMapSessionImpl处理 Public void endTransaction() { getLocalSqlMapSession().endTransaction(); getLocalSqlMapSession().close(); -- session回到池中 } //这个是每个线程一个session变量,线程安全 。 当sessionImpl==null或者关闭时,创建 SqlMapSessionImpl getLocalSqlMapSession() { SqlMapSessionImpl sessionImpl = session.get(); if(sessionImpl == null || sessionImpl.isClosed()) { sessionImpl = new .. (this); session.set(sessionImpl); } return sessionImpl; } } }
SqlMapSessionImpl ..{ SessionScope session ,SqlMapExecutorDelegate delegate,Boolean closed; Public SqlMapSessionImpl(SqlMapClient client) { Delegate = client.getDelegate(); session = delegate.popSession(); .. } Public Object insert(String id, param) { Return delegate.insert(session,id,param); } Public void close() { delegate.pushSession(session); Session = null; delefate = null; closed = true; } Public void startBatch(){ delegate.startBatch();} } }
SqlMapExecutorDelegate { ThrottledPool sessionPool = new ..(RequestScope.class, 128); ThrottledPool requestPool = new ..(SessuinScope.class,512); Public Object insert(SessionScope session,id ,param) { Boolean autoStart = false; Transanction tran = getTransaction(session, autoStart); MappedStatement ms = mappedStatements.get(id); RequestScope request = popReuest (session,ms); Ms.executeUpdate (request,tran,param); pushRequest(request); if(autoStart ) { session.getSqlMapClient().commitTransaction(); session.getSqlMapClient().endTransaction(); } } Public void startBatch(SessionScope ) { session.setInBatch(true); } Public void commitTransaction (SessionScope) { If(session.isInBatch()) executeBatch(session); sqlExecutor.cleanup(session); txManager.commit(session); } Public void endTransaction (SessionScope) { sqlExecutor.cleanup(session); txManager.end(session); } Public void executeBatch(SessionScope) { Session.setInBatch(false); sqlExecutor.executeBatch(session); }
相关文章推荐
- android 深入理解LayoutInflater.inflate()
- h5新特性
- 16年寒假随笔(3)
- jqGrid 排序和treeGird
- hdu 4027 Can you answer these queries?
- 如何理解Hibernate的延迟加载机制?在实际应用中,延迟加载与Session关闭的矛盾是如何处理的?
- vc6.0 GDI+
- MFC 进制转换
- 自动化前端项目构建
- CoreData
- android中用户头像设置的一点心得——关于bitmap被回收的问题
- js javascript:void(0) 真正含义
- NavigationBar底部分割线隐藏
- 使用maven给工程打包
- iOS initWithFrame:frame] 与 [UIButton buttonWithType] 对比
- Android-->反编译初探(mac环境配置)
- 【分享】LOVELY×CATION 1+2【汉化硬盘版】带全CG存档+日文&中文攻略]
- Codeforces Round #339 (Div. 1) B. Skills 暴力 二分
- Hibernate常见优化策略
- C 进制转换