作为 Java 后端开发,你是否曾经纠结过:查询用户信息时,要不要把用户关联的订单、地址一起查出来?全部查询性能肯定受影响,可不查又怕后面用到时反复访问数据库。这种"查不查"的两难抉择,其实可以通过 MyBatis 的延迟加载机制漂亮解决。那么问题来了,MyBatis 到底支持延迟加载吗?它背后的实现原理又是什么?

MyBatis 的延迟加载支持情况

MyBatis 确实支持延迟加载(Lazy Loading)功能,这是一种按需加载的策略,可以有效减轻系统负担,提高查询效率。

简单来说,当我们查询一个实体时,对于它的关联对象,不立即从数据库中加载,而是在第一次真正使用到关联对象时才去数据库查询。这样做可以避免一次性加载过多数据,尤其是在关联关系较多或数据量较大的情况下。

延迟加载的配置方式

MyBatis 提供了两个全局参数来控制延迟加载:

<settings>
    <!-- 开启延迟加载功能 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 设置激进延迟加载策略 -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>
  • lazyLoadingEnabled:设置为 true 时开启延迟加载功能
  • aggressiveLazyLoading:设置为 false 时,按需加载对象属性(只有当调用该属性的 getter 方法时才加载);设置为 true 时,任何对对象方法的调用都会触发所有标记为延迟加载的属性加载

举个简单例子,当aggressiveLazyLoading=true时:

User user = userMapper.getUserById(1);
user.getUsername(); // 仅想获取用户名,但会触发orderList等所有延迟加载属性的加载
// 或者
System.out.println(user); // 调用toString()方法,却触发了所有延迟属性的加载

因此,生产环境中通常建议保持aggressiveLazyLoading=false,避免不必要的性能损耗。

除了全局配置外,还可以在关联查询中单独设置:

<!-- association关联查询时使用延迟加载 -->
<association property="author" column="author_id" select="selectAuthor" fetchType="lazy"/>

<!-- collection集合查询时使用延迟加载 -->
<collection property="posts" ofType="Post" column="id" select="selectPostsForBlog" fetchType="lazy"/>

通过fetchType属性可以覆盖全局的延迟加载设置,值为lazy表示使用延迟加载,eager表示立即加载。

延迟加载的触发条件

延迟加载并非任何操作都会触发,具体的触发条件包括:

  1. 调用延迟属性的 getter 方法:如user.getOrderList()
  2. 对延迟集合属性进行操作:如orderList.size()orderList.isEmpty()、遍历操作等
  3. 仅获取代理对象引用不会触发加载:必须调用其方法才会触发
User user = userMapper.getUserById(1);
// 以下操作不会触发延迟加载
List<Order> orderList = null;
orderList = user.getOrderList(); // 仅获取引用,不会触发加载

// 以下操作会触发延迟加载
int size = user.getOrderList().size(); // 调用size()方法触发加载
boolean isEmpty = user.getOrderList().isEmpty(); // 调用isEmpty()方法触发加载
for (Order order : user.getOrderList()) { // 遍历触发加载
    // 处理订单
}

延迟加载的实现原理

MyBatis 的延迟加载主要是通过动态代理实现的。这里涉及两种代理模式:

  1. JDK 动态代理
  2. CGLIB 动态代理
graph TD
    A[查询主对象] --> B{是否配置延迟加载?}
    B -->|是| C[创建代理对象]
    B -->|否| D[直接关联查询]
    C --> E[返回代理对象包装真实对象]
    E --> F{是否访问关联属性getter方法?}
    F -->|是| G[触发SQL查询加载关联对象]
    F -->|否| H[不执行额外查询]
    D --> I[返回完整对象]

字节码层面的代理原理

理解代理选择的核心,需要了解底层实现原理:

  • JDK 动态代理:基于接口实现,通过java.lang.reflect.Proxy类在运行时生成接口的代理类。它要求目标类必须实现至少一个接口。
  • CGLIB 动态代理:基于字节码生成技术,通过创建目标类的子类来实现代理。CGLIB 在运行时动态修改字节码,重写目标类的方法以插入延迟加载逻辑。

简单理解:JDK 代理是"实现接口",CGLIB 代理是"继承类"。这就是为什么实现了接口的类优先使用 JDK 代理,而普通类只能用 CGLIB 代理。

代理机制的选择

MyBatis 会根据目标类是否实现接口选择使用不同的代理机制:

// MyBatis ProxyFactory选择逻辑(简化版)
public class ProxyFactory {
    private ProxyFactory() {
        // Prevent Instantiation
    }

    public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration,
                                    ObjectFactory objectFactory, List<Class<?>> constructorArgTypes,
                                    List<Object> constructorArgs) {
        // target: 真实对象(如User实例)
        // lazyLoader: 存储延迟加载任务的映射(属性名→加载器)

        // 判断目标类是否为接口或者代理类
        boolean isJdkProxy = target.getClass().getInterfaces().length > 0
            && !Proxy.isProxyClass(target.getClass());

        if (isJdkProxy) {
            // 使用JDK动态代理(优先选择,性能略优且符合Java标准)
            return JdkProxyFactory.createProxy(target, lazyLoader, configuration, objectFactory,
                                              constructorArgTypes, constructorArgs);
        } else {
            // 使用CGLIB动态代理(目标是非接口的普通类时)
            return CglibProxyFactory.createProxy(target, lazyLoader, configuration, objectFactory,
                                               constructorArgTypes, constructorArgs);
        }
    }
}
  • 如果目标类实现了接口,MyBatis 会优先使用 JDK 动态代理(性能更好且符合 Java 标准)
  • 如果目标类没有实现接口,则使用 CGLIB 动态代理
注意:MyBatis 3.2.8+完全支持 JDK/CGLIB 代理自动切换,早期版本可能需要手动配置代理工厂。MyBatis 自 3.3.0 起,若检测到 classpath 中无 CGLIB 依赖,会自动引入mybatis-cglib-proxy模块(基于 CGLIB 3.2.5),因此 Maven 项目通常无需额外配置。若使用 Gradle 或手动管理依赖,需确保相关 jar 包存在。

动态代理实现优化

JDK 和 CGLIB 代理处理逻辑中有很多相似部分,可以抽取公共方法处理:

// 公共方法处理逻辑
private Object handleSpecialMethods(Object target, Method method, Object[] args) throws Throwable {
    final String methodName = method.getName();

    if (methodName.equals("equals")) {
        return target.equals(args[0]);
    } else if (methodName.equals("hashCode")) {
        return target.hashCode();
    } else if (methodName.equals("toString")) {
        return target.toString();
    }
    return null; // 不是特殊方法,返回null
}

// 然后在代理处理器中调用
Object result = handleSpecialMethods(target, method, args);
if (result != null) {
    return result;
}
// 处理其他方法...

ResultLoaderMap:延迟加载的核心容器

ResultLoaderMap是 MyBatis 用于管理延迟加载任务的容器,它存储了属性名与对应的ResultLoader的映射关系。每个延迟属性对应一个ResultLoader,当属性被访问时,通过ResultLoader执行对应的子查询并填充数据。

ResultLoaderMap是会话级(SqlSession)容器,线程安全由SqlSession的线程隔离性保证,无需额外同步。在高并发场景下,每个请求使用独立SqlSession,避免线程间数据污染。

// ResultLoaderMap简化概念示意
public class ResultLoaderMap {
    // 存储属性名到ResultLoader的映射
    private final Map<String, LoadPair> loaderMap = new HashMap<>();

    // 检查是否有指定属性的加载器
    public boolean hasLoader(String property) {
        return loaderMap.containsKey(property);
    }

    // 触发指定属性的加载
    public void load(String property) throws SQLException {
        LoadPair pair = loaderMap.get(property);
        if (pair != null) {
            pair.load(); // 执行SQL查询并填充结果
            loaderMap.remove(property); // 加载后移除该加载器
        }
    }
}

// 加载器,包含了执行查询所需的全部信息
class LoadPair {
    private final String property;
    private final MetaObject metaResultObject;
    private final ResultLoader resultLoader;

    public void load() throws SQLException {
        // 执行SQL查询获取结果
        Object value = resultLoader.loadResult();
        // 将结果设置到目标对象的属性上
        metaResultObject.setValue(property, value);
    }
}

延迟加载的实际案例

让我们通过一个用户(User)和订单(Order)的例子来看看延迟加载如何工作:

实体类定义

public class User implements Serializable { // 实现Serializable接口避免序列化问题
    private Integer id;
    private String username;
    private List<Order> orderList;

    // getter和setter方法
}

public class Order implements Serializable {
    private Integer id;
    private String orderNo;
    private Double amount;
    private Integer userId;

    // getter和setter方法
}

MyBatis 配置

  1. 首先在 MyBatis 全局配置中启用延迟加载:
<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>
  1. 然后在 Mapper 文件中配置:
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 查询用户,延迟加载订单信息 -->
    <select id="getUserById" resultMap="userResultMap" parameterType="int">
        SELECT id, username FROM user WHERE id = #{id}
    </select>

    <!-- 根据用户ID查询订单列表 -->
    <select id="getOrdersByUserId" resultType="com.example.entity.Order" parameterType="int">
        SELECT id, order_no, amount, user_id FROM orders WHERE user_id = #{userId}
    </select>

    <resultMap id="userResultMap" type="com.example.entity.User">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <!-- 配置延迟加载 -->
        <collection property="orderList" ofType="com.example.entity.Order"
                    column="id" select="getOrdersByUserId" fetchType="lazy"/>
    </resultMap>
</mapper>

执行过程与事务

工具类及代码演示

首先,需要一个 MyBatis 工具类来获取 SqlSession:

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyBatisUtil {
    private static final Logger log = LoggerFactory.getLogger(MyBatisUtil.class);
    private static final SqlSessionFactory sqlSessionFactory;

    static {
        try (InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")) {
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            log.error("MyBatis配置加载失败", e);
            throw new RuntimeException("MyBatis配置加载失败", e);
        }
    }

    public static SqlSession getSqlSession() {
        return sqlSessionFactory.openSession();
    }
}
注意:需要在类路径下添加mybatis-config.xml配置文件,配置数据源和 Mapper 扫描。

然后,使用这个工具类编写延迟加载示例:

public class LazyLoadingDemo {
    public static void main(String[] args) {
        // 使用try-with-resources确保SqlSession正确关闭
        try (SqlSession sqlSession = MyBatisUtil.getSqlSession()) {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

            // 查询用户信息
            User user = userMapper.getUserById(1);
            System.out.println("用户名: " + user.getUsername());

            // 此时还没有执行订单查询的SQL
            System.out.println("=== 分割线,以上SQL不包含订单查询 ===");

            // 访问订单信息时,才会触发延迟加载,执行订单查询SQL
            // 注意:延迟加载依赖活动的SqlSession,建议在会话关闭前完成所有延迟属性的访问
            List<Order> orderList = user.getOrderList();
            System.out.println("订单数量: " + orderList.size());

            // 后续再次访问不会触发SQL查询,因为已缓存在一级缓存中
            System.out.println("再次访问订单: " + user.getOrderList().size());
        } // SqlSession自动关闭

        // 注意:在此处访问user.getOrderList()会抛出异常
        // 因为延迟加载依赖活动的SqlSession
    }
}

延迟加载的优缺点

优点

  1. 性能提升:避免一次性加载过多不必要的数据,减少内存占用
  2. 按需加载:只有真正需要使用关联数据时才会查询,减少不必要的 IO 操作
  3. 降低系统压力:特别是在复杂关联关系或大数据量场景下,可以显著降低系统负担

缺点

  1. N+1 问题:当需要遍历一个集合并访问每个元素的延迟加载属性时,会导致主查询 1 次+每个对象的延迟查询 N 次,总共 N+1 次查询
  2. 代理对象序列化问题:延迟加载的代理对象序列化时可能会出现问题,尤其是 CGLIB 代理对象
  3. 会话关闭后无法加载:延迟加载依赖活动的数据库会话,SqlSession 关闭后无法再加载
graph LR
    A[延迟加载] --> B[优点]
    A --> C[缺点]

    B --> D[性能提升]
    B --> E[按需加载]
    B --> F[降低系统压力]

    C --> G[N+1问题]
    C --> H[序列化问题]
    C --> I[会话依赖]

解决 N+1 问题的方法

延迟加载可能导致的 N+1 问题可以通过以下方式解决:

1. 使用显式即时加载

在明确需要关联数据的场景下,可以显式指定即时加载:

<collection property="orderList" ofType="Order" column="id"
           select="getOrdersByUserId" fetchType="eager"/>

需要注意的是,fetchType="eager"并不是在 SQL 层面使用 JOIN 查询,而是在主查询完成后立即执行关联查询。本质上是"分步加载",但不需要等到属性被访问时才加载。

2. 使用 MyBatis 的批量查询功能

MyBatis 提供了多种批量查询方式来解决 N+1 问题:

a) 使用 multiple column 参数传递多个值进行批量查询

<!-- 配置批量查询的映射 -->
<collection property="orders" ofType="Order"
           column="{userId=id, userName=username}" select="getOrdersByUserParams"/>

<!-- 批量查询方法接收多个参数 -->
<select id="getOrdersByUserParams" resultType="Order">
    SELECT * FROM orders
    WHERE user_id = #{userId}
    AND create_by = #{userName}
</select>

b) 手动批量查询优化

// 手动批量查询优化示例
List<User> users = userMapper.getAllUsers();
List<Integer> userIds = users.stream().map(User::getId).collect(Collectors.toList());
List<Order> allOrders = orderMapper.getOrdersByUserIds(userIds); // 1次批量查询

// 建立用户-订单映射关系
Map<Integer, List<Order>> orderMap = allOrders.stream()
    .collect(Collectors.groupingBy(Order::getUserId));

// 处理用户和订单
for (User user : users) {
    List<Order> userOrders = orderMap.getOrDefault(user.getId(), Collections.emptyList());
    System.out.println("用户" + user.getUsername() + "的订单数量: " + userOrders.size());
}
注意:虽然 MyBatis 提供了batchSize配置,但它主要用于优化批量插入/更新操作,对延迟加载的 N+1 问题没有直接帮助。延迟加载的子查询仍然是单条执行的,需要通过上述手动批量查询方式优化。

3. N+1 问题的监控与预防

可以通过以下方式监控和预防 N+1 问题:

// 配置SQL监控
@Aspect
@Component
public class LazyLoadingMonitor {
    private static final Logger log = LoggerFactory.getLogger(LazyLoadingMonitor.class);

    // 可通过配置调整阈值
    @Value("${mybatis.lazy.threshold:10}")
    private long threshold;

    @Around("execution(* com.example.entity.*.get*(..))")
    public Object monitorLazyLoading(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().getName();
        Object target = pjp.getTarget();

        // 判断是否可能触发延迟加载的getter方法
        if (methodName.startsWith("get") && !methodName.equals("getClass")) {
            // 记录方法调用前的时间
            long start = System.currentTimeMillis();
            Object result = pjp.proceed();
            long end = System.currentTimeMillis();

            // 如果执行时间过长,可能触发了延迟加载
            long duration = end - start;
            if (duration > threshold) {
                log.warn("可能的延迟加载: 类={}, 方法={}, 执行时间={}ms",
                         target.getClass().getSimpleName(),
                         methodName,
                         duration);
            }
            return result;
        }
        return pjp.proceed();
    }
}

也可以使用成熟的监控工具,如 MyBatis Plus 的性能分析插件来监控 SQL 执行。

代理对象序列化问题及解决方案

延迟加载使用的代理对象在序列化时可能会遇到问题,尤其是 CGLIB 代理类。CGLIB 生成的代理类名称类似$$EnhancerByCGLIB$$xxx,反序列化时需要相同的类路径和类定义。在分布式系统中(如微服务架构),这种代理类可能无法在不同节点间正确反序列化,导致ClassNotFoundException异常。

解决方案包括:

1. 确保实体类实现 Serializable 接口

所有实体类都应该实现java.io.Serializable接口,包括关联实体类。

2. 在序列化前触发延迟加载

确保在序列化前已经访问过延迟加载属性,将代理对象转换为真实对象:

// 引入Jackson依赖
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class SerializationHelper {
    private static final Logger log = LoggerFactory.getLogger(SerializationHelper.class);
    private static final ObjectMapper objectMapper = new ObjectMapper();

    public static String prepareForSerialization(User user) {
        try {
            // 在序列化前触发所有延迟加载
            if (user.getOrderList() != null) {
                user.getOrderList().size(); // 触发延迟加载
            }

            // 现在user中的orderList已经是真实数据,可以安全序列化
            return objectMapper.writeValueAsString(user);
        } catch (JsonProcessingException e) {
            log.error("序列化失败", e);
            throw new RuntimeException("序列化失败", e);
        }
    }
}

3. 使用自定义序列化策略

使用 Jackson 或其他序列化工具的自定义序列化功能:

// 使用Jackson注解忽略代理相关属性
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
public class User implements Serializable {
    // 实体类定义
}

延迟加载与事务的关系

延迟加载依赖的SqlSession需与事务作用域一致。如果事务提前提交或回滚,会导致后续的延迟加载无法执行:

// 正确示例:在同一事务中完成延迟加载
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    @Transactional
    public int getUserOrderCount(int userId) {
        User user = userMapper.getUserById(userId);
        // 在同一事务中访问延迟加载属性
        return user.getOrderList().size();
    }
}

在 Spring 环境中,可以使用OpenSessionInView模式延长会话生命周期,但这可能导致数据库连接长时间占用,高并发系统中要谨慎使用。

延迟加载与缓存结合使用

MyBatis 的延迟加载与缓存机制可以协同工作,进一步提升性能:

一级缓存(会话级)

  • 默认开启,作用域为 SqlSession
  • 延迟加载的结果会存入一级缓存,同一会话内重复访问不会触发数据库查询
  • 当执行 update、delete、insert 或调用 clearCache()时,一级缓存会被清空

二级缓存(全局)

  • 需手动配置<cache/><cache-ref/>
  • 延迟加载查询的结果也会被二级缓存缓存
  • 跨会话访问时可以直接从二级缓存获取
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 启用二级缓存 -->
    <cache eviction="LRU"
          flushInterval="60000"  <!-- 刷新间隔,单位毫秒 -->
          size="1024"           <!-- 引用数量 -->
          readOnly="true"/>     <!-- 只读设置 -->

    <!-- 映射器配置 -->
</mapper>

readOnly=true表示缓存对象不可变,MyBatis 会直接返回缓存对象引用,提升性能;readOnly=false则返回对象副本,保证线程安全。

二级缓存存储的是完整对象(包括延迟加载后的数据),因此需确保延迟加载触发后的数据会被正确序列化并缓存。建议在getUserById等主查询上配置缓存,延迟加载的子查询(如getOrdersByUserId)可通过flushCache="true"保证数据一致性。

延迟加载的适用场景

适合使用延迟加载的场景

  1. 关联数据使用频率低:如用户详情页的历史订单,只有用户点击"查看订单"时才需要加载
  2. 大数据量列表查询:只加载主数据,关联数据按需加载,避免一次性加载过多数据
  3. 层级数据结构:如树形结构,只需要加载当前节点数据,子节点按需加载
  4. 统计报表的明细数据:报表页面通常只展示汇总数据,详情数据按需加载

不适合使用延迟加载的场景

  1. 频繁访问关联数据:如订单详情页需同时展示用户和商品信息,此时即时加载更高效
  2. 批量数据处理:需要处理大量关联数据的场景,延迟加载会导致 N+1 问题
  3. 无状态服务:如 REST API,每个请求都会创建新的 Session,延迟加载可能导致会话关闭问题
  4. 高并发系统:延迟加载依赖会话,可能导致数据库连接长时间占用

复杂关联关系处理

多对多和嵌套加载处理

在处理复杂关联关系如多对多(用户-角色)或嵌套关系(用户-订单-商品)时,配置原理相似,但需要注意关联条件和层级结构:

<!-- 用户与角色的多对多关系 -->
<resultMap id="userWithRolesMap" type="com.example.entity.User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <!-- 通过中间表查询关联角色 -->
    <collection property="roles" ofType="com.example.entity.Role"
                column="id" select="getRolesByUserId" fetchType="lazy"/>
</resultMap>

<!-- 嵌套延迟加载:订单-商品 -->
<resultMap id="orderMap" type="com.example.entity.Order">
    <id property="id" column="id"/>
    <result property="orderNo" column="order_no"/>
    <!-- 嵌套层级的延迟加载 -->
    <collection property="products" ofType="com.example.entity.Product"
                column="id" select="getProductsByOrderId" fetchType="lazy"/>
</resultMap>

在处理复杂关系时要点:

  • 对于多对多关系:通常需要一个额外查询处理中间表连接
  • 对于嵌套层级:需确保每层都正确配置延迟加载,并且会话保持活动状态直到所有层级都访问完毕

MyBatis 与 Hibernate 延迟加载对比

对于熟悉 Hibernate 的开发者,了解两者差异有助于更好地使用 MyBatis 的延迟加载:

特性MyBatisHibernate
代理实现与性能基于动态代理(JDK/CGLIB),代理对象创建速度快,但功能相对简单基于字节码增强(Javassist/ByteBuddy),初始化较慢但运行性能好
加载方式通过单独的 select 查询(需手动配置)支持 JOIN 方式和单表查询两种延迟加载
会话管理需手动管理 SqlSession 生命周期通过 Session/EntityManager 自动处理
配置方式XML 或注解,需明确设置 fetchType通过映射关系直接控制(如@OneToMany(fetch=FetchType.LAZY))
N+1 解决需手动批量查询或配置关联查询提供批处理机制(batch fetching)自动优化

实际应用建议

  1. 选择性启用:不是所有场景都适合使用延迟加载,需要根据业务特点选择
  2. 合理设置全局配置
  • 开发环境可以设置lazyLoadingEnabled=true方便调试
  • 生产环境根据实际性能测试结果决定
  • 尽量保持aggressiveLazyLoading=false,避免非预期的性能问题
  1. 结合缓存机制:MyBatis 的一级缓存、二级缓存与延迟加载配合使用,可以进一步提升性能
  2. 在 Service 层管理好会话:确保访问延迟加载属性时 SqlSession 仍然处于打开状态,或考虑使用 Spring 的OpenSessionInView模式
  3. 性能测试:在生产环境部署前,对延迟加载的性能影响进行充分测试,包括高并发场景

总结

我们来用表格总结一下 MyBatis 的延迟加载特性:

特性描述
支持情况MyBatis 完全支持延迟加载功能
实现原理基于动态代理机制(JDK 代理或 CGLIB 代理)
延迟容器使用 ResultLoaderMap 存储延迟加载任务
全局配置lazyLoadingEnabledaggressiveLazyLoading控制
局部控制通过fetchType属性覆盖全局设置
触发条件调用 getter 方法、集合操作方法(size/isEmpty)、遍历等
会话依赖延迟加载依赖活动的 SqlSession 和事务
N+1 优化批量查询、multiple columns 传参
序列化处理实现 Serializable 接口、预先触发延迟加载、自定义序列化策略
与缓存结合延迟加载结果会进入一/二级缓存,提升后续访问性能
适用场景关联数据使用频率低、大数据量列表查询、层级数据结构

异常君
1 声望2 粉丝

在 Java 的世界里,永远有下一座技术高峰等着你。我愿做你登山路上的同频伙伴,陪你从看懂代码到写出让自己骄傲的代码。咱们,代码里见!