MyBatis主键#

不支持对象列表存储时对自增id字段的赋值(至少包括3.2.6和3.3.0版本),如果id不是采用底层DB自增主键赋值,不必考虑此问题

温馨提示:分布式DB环境下,DB主键一般会采用统一的Id生成器生成Id,因此不必考虑由数据库自增策略填充主键值。

解决方案#

参考源码##

1)mybatis-batch-insert项目,请为原作者点赞,支持他开源

备注:实际代码有少量修改,会在下文列出,本文依据实现方案代码细节反推分析源码处理逻辑过程

批量插入对象列表自增主键赋值分析##

1)在获取数据库返回的主键值后填充到中间存储结构。

2)在构造具体返回对象结构过程中(其实insert语句并不需要),从中间存储结构将多个主键值填充到具体的对象实例当中。

备注:实际上这种解决方案还是来源于代码分析的结果,接下来简单列述Mybatis主键处理的核心代码及配置

MyBatis处理主键处理流程#

备注:主键填充的处理方法实际是populateKeys。

代码呈上#

测试示例

package org.wit.ff.jdbc;

import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
import org.wit.ff.jdbc.dao.HomeTownDao;
import org.wit.ff.jdbc.id.BatchInsertEntities;
import org.wit.ff.jdbc.model.HomeTown;
import org.wit.ff.jdbc.query.Criteria; import java.util.ArrayList;
import java.util.List; /**
* Created by F.Fang on 2015/11/17.
* Version :2015/11/17
*/
@ContextConfiguration(locations = {"classpath:applicationContext-batch.xml"})
public class HomeTownDaoBatchTest extends AbstractJUnit4SpringContextTests { @Autowired
private HomeTownDao homeTownDao; @Test
public void testBatchInsert(){
HomeTown ht1 = new HomeTown();
ht1.setName("hb");
ht1.setLocation("hubei");
HomeTown ht2 = new HomeTown();
ht2.setName("js");
ht2.setLocation("jiangsu"); List<HomeTown> list = new ArrayList<>();
list.add(ht1);
list.add(ht2); BatchInsertEntities<HomeTown> batchEntities = new BatchInsertEntities<>(list); homeTownDao.batchInsert(batchEntities);
System.out.println(batchEntities.getEntities());
} }

控制台输出

[3,hb,hubei, 4,js,jiangsu]

模型HomeTown

package org.wit.ff.jdbc.model;

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.wit.ff.jdbc.id.IdGenerator; /**
* Created by F.Fang on 2015/11/17.
* Version :2015/11/17
*/
public class HomeTown implements IdGenerator {
private int id; private String name; private String location; public int getId() {
return id;
} public void setId(int id) {
this.id = id;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public String getLocation() {
return location;
} public void setLocation(String location) {
this.location = location;
} public String toString() {
return ReflectionToStringBuilder.toString(this, ToStringStyle.SIMPLE_STYLE);
} @Override
public void parseGenKey(Object[] value) {
if(value!=null && value.length == 1){
this.id = Integer.valueOf(value[0].toString());
}
}
}

HomeTownDao

package org.wit.ff.jdbc.dao;

import org.wit.ff.jdbc.id.BatchInsertEntities;
import org.wit.ff.jdbc.model.HomeTown; import java.util.List; /**
* Created by F.Fang on 2015/11/17.
* Version :2015/11/17
*/
public interface HomeTownDao { void batchInsert(BatchInsertEntities<HomeTown> batchEntities);
}

Mapper配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.wit.ff.jdbc.dao.HomeTownDao"> <insert id="batchInsert" parameterType="org.wit.ff.jdbc.id.BatchInsertEntities" useGeneratedKeys="true" keyProperty="id"
keyColumn="ID"> insert into hometown
(name,location)
values
<foreach item="item" collection="entities" separator=",">
( #{item.name},#{item.location})
</foreach>
</insert> </mapper>

Spring配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd"> <!-- 数据源 -->
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${db.driverClass}"/>
<property name="url" value="${db.jdbcUrl}"/>
<property name="username" value="${db.user}"/>
<property name="password" value="${db.password}"/>
</bean> <!-- 配置 SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 制定路径自动加载mapper配置文件 -->
<property name="mapperLocations" value="classpath:mappers/*Dao.xml"/> <!-- 配置myibatis的settings http://mybatis.github.io/mybatis-3/zh/configuration.html#settings -->
<property name="configurationProperties">
<props>
<prop key="cacheEnabled">true</prop>
</props>
</property> <property name="typeHandlers">
<list>
<bean class="org.wit.ff.jdbc.id.BatchInsertEntitiesTypeHandler"/>
</list>
</property> <property name="objectWrapperFactory" ref="batchObjectWrapperFactory"/> <!-- 类型别名是为 Java 类型命名一个短的名字。 它只和 XML 配置有关, 只用来减少类完全 限定名的多余部分 -->
<property name="typeAliasesPackage" value="org.wit.ff.jdbc.model"/> </bean> <bean id="batchObjectWrapperFactory" class="org.wit.ff.jdbc.id.BatchInsertObjectWrapperFactory"/> <mybatis:scan base-package="org.wit.ff.jdbc.dao"/> </beans>

存储主键值的结构

package org.wit.ff.jdbc.id;

import java.util.List;

public class BatchInsertEntityPrimaryKeys {
private final List<String> primaryKeys; public BatchInsertEntityPrimaryKeys(List<String> pks) {
this.primaryKeys = pks;
} public List<String> getPrimaryKeys() {
return primaryKeys;
}
}

批量对象列表包装

package org.wit.ff.jdbc.id;

import java.util.List;

public class BatchInsertEntities<T extends IdGenerator> {
private final List<T> entities; public BatchInsertEntities(List<T> entities) {
this.entities = entities;
} /**
* <p>
* The entities will be batch inserted into DB. The entities are also the
* parameters of the
* {@link org.apache.ibatis.binding.MapperMethod.SqlCommand}.
*/
public List<T> getEntities() {
return entities;
}
}

自定义TypeHandler

package org.wit.ff.jdbc.id;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List; import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType; public class BatchInsertEntitiesTypeHandler extends BaseTypeHandler<BatchInsertEntityPrimaryKeys> { public BatchInsertEntityPrimaryKeys getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
// Read the primary key values from result set. It is believed that
// there is 1 primary key column.
List<String> pks = new LinkedList<>();
do {
// rs.next is called before.
pks.add(rs.getString(columnIndex));
} while (rs.next()); return new BatchInsertEntityPrimaryKeys(pks);
} @Override
public void setNonNullParameter(PreparedStatement ps, int i, BatchInsertEntityPrimaryKeys parameter,
JdbcType jdbcType) throws SQLException {
// TODO Auto-generated method stub
//System.out.println(" BatchInsertEntitiesTypeHandler#setNonNullParameter got called. ");
} @Override
public BatchInsertEntityPrimaryKeys getNullableResult(ResultSet rs, String columnName) throws SQLException {
// TODO Auto-generated method stub
//System.out.println(" BatchInsertEntitiesTypeHandler#getNullableResult got called. ");
return null;
} @Override
public BatchInsertEntityPrimaryKeys getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
// TODO Auto-generated method stub
//System.out.println(" BatchInsertEntitiesTypeHandler#getNullableResult got called. ");
return null;
} }

自定义ObjectWrapper

package org.wit.ff.jdbc.id;

import java.util.Iterator;
import java.util.List; import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.reflection.wrapper.ObjectWrapper; /**
* Wrap the collection object for batch insert.
* https://github.com/jactive/java
*/
public class BatchInsertObjectWrapper implements ObjectWrapper { private final BatchInsertEntities<IdGenerator> entity; public BatchInsertObjectWrapper(MetaObject metaObject, BatchInsertEntities<IdGenerator> object) {
this.entity = object;
} @Override
public void set(PropertyTokenizer prop, Object value) {
// check the primary key type existed or not when setting PK by reflection.
BatchInsertEntityPrimaryKeys pks = (BatchInsertEntityPrimaryKeys) value;
if (pks.getPrimaryKeys().size() == entity.getEntities().size()) { Iterator<String> iterPks = pks.getPrimaryKeys().iterator();
Iterator<IdGenerator> iterEntities = entity.getEntities().iterator(); while (iterPks.hasNext()) {
String id = iterPks.next();
IdGenerator entity = iterEntities.next();
//System.out.println(id + "|" + entity);
entity.parseGenKey(new Object[]{id});
}
}
} @Override
public Object get(PropertyTokenizer prop) {
// Only the entities or parameters property of BatchInsertEntities
// can be accessed by mapper.
// 这一段是决定最终返回数据结果.
if ("entities".equals(prop.getName()) ||
"parameters".equals(prop.getName())) {
return entity.getEntities();
} return null;
} @Override
public String findProperty(String name, boolean useCamelCaseMapping) {
return null;
} @Override
public String[] getGetterNames() {
return null;
} @Override
public String[] getSetterNames() {
return null;
} /**
* 此函数返回类型和BatchInsertEntitiesTypeHandler的泛型类型一致.
* Jdbc3KeyGenerator.
* Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);
* TypeHandler<?> th = typeHandlerRegistry.getTypeHandler(keyPropertyType);
*
* @param name
* @return
* @see org.apache.ibatis.reflection.wrapper.ObjectWrapper#getSetterType(java.lang.String)
*/
@Override
public Class<?> getSetterType(String name) {
// Return the primary key setter type.
// Here, we return the BatchInsertEntityPrimaryKeys because
// there are several primary keys in the result set of
// INSERT statement.
return BatchInsertEntityPrimaryKeys.class;
} @Override
public Class<?> getGetterType(String name) {
return null;
} @Override
public boolean hasSetter(String name) {
// In BatchInsertObjectWrapper, name is the primary key property name.
// Always return true here without checking if there is such property
// in BatchInsertEntities#getEntities().get(0) . The verification be
// postphone until setting the PK value at this.set method.
return true;
} @Override
public boolean hasGetter(String name) {
return false;
} @Override
public MetaObject instantiatePropertyValue(String name, PropertyTokenizer prop, ObjectFactory objectFactory) {
return null;
} @Override
public boolean isCollection() {
return false;
} @Override
public void add(Object element) { } @Override
public <E> void addAll(List<E> element) {
}
}

自定义ObjectWrapperFactory

package org.wit.ff.jdbc.id;

import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.wrapper.ObjectWrapper;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory; public class BatchInsertObjectWrapperFactory implements ObjectWrapperFactory {
public boolean hasWrapperFor(Object object) {
return null != object && BatchInsertEntities.class.isAssignableFrom(object.getClass());
} public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {
return new BatchInsertObjectWrapper(metaObject, (BatchInsertEntities<IdGenerator>)object);
} }

源代码分析#

  • 为什么定义一个BatchInsertEntities而不直接使用List
  • 自定义TypeHandler的目的
  • 自定义ObjectWrapper(factory)

此事要回到源码当中找答案。

1)上文的时序图定位主键核心处理代码起始方法:org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator.populateAfter

**注意mapper配置文件配置 useGeneratedKeys="true" keyProperty="id" keyColumn="ID" **


public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
List<Object> parameters = new ArrayList<Object>();
parameters.add(parameter);
processBatch(ms, stmt, parameters);
} public void processBatch(MappedStatement ms, Statement stmt, List<Object> parameters) {
ResultSet rs = null;
try {
rs = stmt.getGeneratedKeys();
final Configuration configuration = ms.getConfiguration();
final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
final String[] keyProperties = ms.getKeyProperties();
final ResultSetMetaData rsmd = rs.getMetaData();
TypeHandler<?>[] typeHandlers = null;
if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {
for (Object parameter : parameters) {
if (!rs.next()) break; // there should be one row for each statement (also one for each parameter)
final MetaObject metaParam = configuration.newMetaObject(parameter);
// 1,找typeHandlers的逻辑为关键.
if (typeHandlers == null) typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties);
// 2, 填充键值.
populateKeys(rs, metaParam, keyProperties, typeHandlers);
}
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
} finally {
if (rs != null) {
try {
rs.close();
} catch (Exception e) {
// ignore
}
}
}
}

步骤1 查找TypeHandler

BatchInsertEntitiesTypeHandler负责处理BatchInsertEntityPrimaryKeys类型,并定义了getNull(rs,int index)方法,后面可以看到这个方法在主键填充时被调用.

private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties) {
TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];
for (int i = 0; i < keyProperties.length; i++) {
if (metaParam.hasSetter(keyProperties[i])) {
// metaParam getSetterType --> BatchInsertObjectWrapper定义了getSetterType,参考MetaObject中获取getSetterType的源码,实际是从自定义的ObjectWrapper中获取
Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);
// 从spring xml 配置中找TypeHandler的配置.
TypeHandler<?> th = typeHandlerRegistry.getTypeHandler(keyPropertyType);
typeHandlers[i] = th;
}
}
return typeHandlers;
}

小结:获取TypeHandler的过程实际是依据ObjectWrapper指定的SetterType拿到KeyPropertyType(主键类型),再通过主键类型从用户配置的SessionFactory当中获取(上文SessionFactory中配置BatchInsertEntitiesTypeHandler)

步骤2 填充主键

请参考BaseTypehandler中的getResult方法,实际调用了getNullResult方法,此方法BatchInsertEntitiesTypeHandler已有实现,经过调试发现它依据配置的主键名称"id"从resultset中获取了一列id值。

  private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
for (int i = 0; i < keyProperties.length; i++) {
TypeHandler<?> th = typeHandlers[i];
if (th != null) {
// 即调用BatchInsertEntitiesTypeHandler的public BatchInsertEntityPrimaryKeys getNullableResult(ResultSet rs, int columnIndex)的方法
// 将主键值记录在BatchInsertEntityPrimaryKeys的对象当中,此时value的类型是BatchInsertEntityPrimaryKeys.
Object value = th.getResult(rs, i + 1);
// 调用 org.apache.ibatis.reflection.MetaObject
metaParam.setValue(keyProperties[i], value);
}
}
} public void setValue(String name, Object value) {
// 被设置的属性是不含有"." , 目前是id, 而不是(item.id)这样的字符串 因此会执行else.
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
MetaObject metaValue = metaObjectForProperty(prop.getIndexedName());
if (metaValue == SystemMetaObject.NULL_META_OBJECT) {
if (value == null && prop.getChildren() != null) {
return; // don't instantiate child path if value is null
} else {
metaValue = objectWrapper.instantiatePropertyValue(name, prop, objectFactory);
}
}
metaValue.setValue(prop.getChildren(), value);
} else {
// 核心执行逻辑.
// 此处的objectWrapper 对应我们自定义的org.wit.ff.BatchInsertObjectWrapper
objectWrapper.set(prop, value);
}
}

小结:依据目标TypeHandler,调用getNullResult处理主键,从ResultSet当中拿到一列主键值,并包装成BatchInsertEntityPrimaryKeys返回,作为参数执行目标ObjectWrapper的set方法,请参考上文BatchInsertObjectWrapper。

至此,从ResultSet返回的主键值列表已经被我们自定义的ObjectWrapper截获。

步骤3 BatchInsertObjectWrapper填充主键值到原始对象列表

HowTown类型实现了IdGenerator接口, 调用parseGenKey即可填充主键到目标对象上,详情参考上文的HownTown源码。

    @Override
public void set(PropertyTokenizer prop, Object value) {
// check the primary key type existed or not when setting PK by reflection.
BatchInsertEntityPrimaryKeys pks = (BatchInsertEntityPrimaryKeys) value;
if (pks.getPrimaryKeys().size() == entity.getEntities().size()) { Iterator<String> iterPks = pks.getPrimaryKeys().iterator();
Iterator<IdGenerator> iterEntities = entity.getEntities().iterator(); while (iterPks.hasNext()) {
String id = iterPks.next();
IdGenerator entity = iterEntities.next();
//System.out.println(id + "|" + entity);
entity.parseGenKey(new Object[]{id});
}
}
}

三个问题的答案##

1)自定义存储对象列表的结构的原因在于MyBatis处理主键时始终将对象作为"一个"来看待,并且要绑定主键类型,而List是集合类型,类型是List

2)由于1)中自定义了存储结构(BatchInsertEntities)需要处理主键,因此需要定义一个新的主键类型BatchInsertEntityPrimaryKeys 并绑定一个TypeHandler才可以处理此类型

3) ObjectWrapper和TypeHandler实际上是相辅相成的关系,有了类型处理器将ResultSet中的主键数据转换为目标对象可接受的类型,那么填充目标对象主键的工作就由ObjectWrapper来完成了,它们是相互协作的关系。

总结#

如果仍然对上述过程有疑问,请务必调试代码,作者不太聪明,调试了n次才读懂了完整过程。

最后给个提示,一个普通Bean是如何填充主键的,请查看org.apache.ibatis.reflection.wrapper.BeanWrapper及org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator,想必必有收获。

QA#

太累了,这一篇。。。

轻量级封装DbUtils&Mybatis之四MyBatis主键的更多相关文章

  1. 轻量级封装DbUtils&Mybatis之三MyBatis分页

    MyBatis假分页 参考DefaultResultSetHandler的skipRows方法. 温馨提示:部分代码请参考轻量级封装DbUtils&Mybatis之一概要 解决方案 1)之前公 ...

  2. MyBatis insert返回主键(sqlserver2008)

    mybatis insert返回主键(sqlserver2008)   MyBatisXML配置,下面两种方式都行 方式1: <insert id="insert" para ...

  3. MyBatis自动获取主键,MyBatis使用Oracle返回主键,Oracle获取主键

    MyBatis自动获取主键,MyBatis使用Oracle返回主键,Oracle获取主键 >>>>>>>>>>>>>> ...

  4. 【转】mybatis 自增主键配置

    mybatis自增主键配置(?) mybatis进行插入操作时,如果表的主键是自增的,针对不同的数据库相应的操作也不同.基本上经常会遇到的就是Oracle Sequece 和 MySQL 自增主键,至 ...

  5. 轻量级封装DbUtils&Mybatis之一概要

    Why 一时兴起,自以为是的对Jdbc访问框架做了一个简单的摸底,近期主要采用Mybatis,之前也有不少采用Dbutils,因此希望能让这两个框架折腾的更好用. DbUtils:非常简单的Jdbc访 ...

  6. 轻量级封装DbUtils&Mybatis之二Dbutils

    DbUtils入门 Apache出品的极为轻量级的Jdbc访问框架,核心类只有两个:QueryRunner和ResultSetHandler. 各类ResultSetHandler: ArrayHan ...

  7. mybatis insert 返回主键

    分享牛,分享牛原创.ssm整合的时候,我们操作mybatis insert 的时候,需要返回插入的主键,因为主键是自增的,这个时候怎么办呢?很简单看一下下面的代码示例: 1.1.1. 代码定义 pub ...

  8. jdbc、Mybatis插入数据主键回显的实现方法

    插入数据的时候,往往需要获取主键值.但是有时候主键是自增长的那么,就不太适用手动添加主键值了,此时需要一种可以回显主键参数的方法, 下面以jdbc.mybatis的实现举例 此时使用的是jdbc的话或 ...

  9. mybatis insertUseGeneratedKeys 返回主键为null

    package tk.mybatis.mapper.common.special; import org.apache.ibatis.annotations.InsertProvider; impor ...

随机推荐

  1. 013PHP基础知识——流程控制(一)

    <?php /** * 13 流程控制(一) * if语句: if(表达式){ 表达式 }elseif(表达式){ 代码段 } * if语句中,一个条件成立,其他分支不执行. * if中的表达式 ...

  2. C# 常用字符串处理办法

    再基础的东西不常用的话就得记下来...不然就忘记了. C#字符串中特殊字符的转义 一个是双引号",另一个就是转义符\ 对于同样一个字符串:地址:"C:\Users\E.txt&qu ...

  3. vue单独给页面设置body属性

    因项目需求:用户个人详细信息页面设置背景色,之前在这个页面设置最外层div发现不行.因为app.vue影响了它.后来直接在页面上用body设置样式,发现影响了其他页面. 后来想了通过vue的生命周期来 ...

  4. CI框架CodeIgniter伪静态各种服务器设置

    Apache服务器.htaccess伪静态设置 RewriteEngine on RewriteCond $1 !^(index\\.php|system\\.php|images|skin|js|l ...

  5. Composer 安装东西遇到github需要token怎么办

    安装yii2遇到这样的提示: Could not fetch https://api.github.com/repos/jquery/sizzle/contents/bower.json?ref=91 ...

  6. 201621123005《Java程序设计》第四周学习总结

    201621123005<Java程序设计>第四周学习总结 标签(空格分隔): 1.本章学习总结 1. 面向对象设计 1.1 写出你认为本周学习中比较重要的知识点关键词 继承.多态.覆盖. ...

  7. 报错:java.lang.IllegalArgumentException: object is not an instance of declaring class

    反射的报错信息如下: java.lang.IllegalArgumentException: object is not an instance of declaring class at sun.r ...

  8. SQL 测验

    1.SQL 指的是? 您的回答:Structured Query Language 2.哪个 SQL 语句用于从数据库中提取数据? 您的回答:SELECT 3.哪条 SQL 语句用于更新数据库中的数据 ...

  9. 自己WIN7旗舰版安装 SQLServer2005/2008的一些总结

    准备工作:下载安装包,当然要保证安装包能用: 安装:1.设置setup.exe文件 右键属性选择 --兼容,兼容下面选择---以管理员方式运行,---兼容模式选择windows xp或者windows ...

  10. 11.Linux启动过程详解

    目录: 本文转载自:http://blog.csdn.net/miss_acha/article/details/50004717 经过对Linux系统有了一定了解和熟悉后,想对其更深层次的东西做进一 ...