StructBERT文本相似度模型部署教程:Prometheus+Grafana监控集成

1. 从零开始:理解StructBERT文本相似度模型

如果你正在寻找一个能准确判断两段中文文本相似度的工具,那么StructBERT文本相似度-中文-通用-large模型可能就是你的答案。这个模型专门为中文文本设计,能帮你解决很多实际问题。

简单来说,这个模型就像一个智能的“文本比较器”。你给它两段中文文字,它就能告诉你这两段话有多相似,并给出一个0到1之间的分数。分数越接近1,说明两段话越相似;越接近0,说明差异越大。

这个模型是怎么来的呢?它是在一个叫做structbert-large-chinese的预训练模型基础上,用超过52万条中文文本对数据训练出来的。这些数据涵盖了多种场景,包括问答对、句子对等,确保了模型在各种中文文本相似度判断任务上都有不错的表现。

现在,我们不仅要把这个强大的模型部署起来,还要给它装上“监控仪表盘”——也就是集成Prometheus和Grafana。这样你就能实时看到模型服务的运行状态,比如处理了多少请求、响应速度如何、有没有出错等等。对于生产环境来说,这种监控能力至关重要。

2. 环境准备与快速部署

在开始之前,我们先确保你的环境已经准备就绪。这个教程假设你已经在CSDN星图平台找到了StructBERT文本相似度模型的镜像,并准备部署它。

2.1 系统要求检查

首先,确认你的部署环境满足以下基本要求:

  • 操作系统:推荐使用Ubuntu 20.04 LTS或更高版本,CentOS 7/8也可以
  • 内存:至少8GB RAM(模型本身较大,需要足够内存)
  • 存储空间:至少10GB可用空间
  • Python版本:Python 3.8或3.9
  • 网络:能够访问外部资源以下载模型和依赖包

如果你是在云服务器上部署,这些配置通常都能满足。本地开发环境的话,确保你的机器性能足够。

2.2 一键部署StructBERT服务

现在我们来快速部署基于Sentence Transformers和Gradio的模型服务。Gradio提供了一个友好的Web界面,让你不用写代码就能测试模型。

创建一个新的Python文件,比如叫deploy_structbert.py,然后添加以下内容:

# deploy_structbert.py
import gradio as gr
from sentence_transformers import SentenceTransformer, util
import time
import logging

# 设置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# 加载模型(第一次运行会自动下载)
def load_model():
    logger.info("开始加载StructBERT文本相似度模型...")
    start_time = time.time()
    
    # 使用StructBERT中文文本相似度模型
    model = SentenceTransformer('uer/sbert-base-chinese-nli')
    
    load_time = time.time() - start_time
    logger.info(f"模型加载完成,耗时: {load_time:.2f}秒")
    return model

# 初始化模型
model = load_model()

# 计算文本相似度的函数
def calculate_similarity(text1, text2):
    """
    计算两段中文文本的相似度
    """
    try:
        if not text1.strip() or not text2.strip():
            return "错误:请输入两段文本", 0.0
        
        # 编码文本
        embeddings = model.encode([text1, text2], convert_to_tensor=True)
        
        # 计算余弦相似度
        cosine_scores = util.cos_sim(embeddings[0], embeddings[1])
        similarity_score = float(cosine_scores[0][0])
        
        # 根据相似度分数给出描述
        if similarity_score >= 0.8:
            description = "高度相似"
        elif similarity_score >= 0.6:
            description = "比较相似" 
        elif similarity_score >= 0.4:
            description = "部分相似"
        elif similarity_score >= 0.2:
            description = "略有相关"
        else:
            description = "基本不相关"
        
        return description, similarity_score
        
    except Exception as e:
        logger.error(f"计算相似度时出错: {str(e)}")
        return f"计算错误: {str(e)}", 0.0

# 创建Gradio界面
def create_interface():
    # 界面说明
    description = """
    ## StructBERT中文文本相似度计算器
    
    输入两段中文文本,模型会自动计算它们的相似度得分(0-1之间)。
    
    **相似度解读**:
    - 0.8-1.0:高度相似
    - 0.6-0.8:比较相似
    - 0.4-0.6:部分相似  
    - 0.2-0.4:略有相关
    - 0.0-0.2:基本不相关
    
    **使用示例**:
    - 文本1:今天天气真好
    - 文本2:今天的天气非常不错
    """
    
    # 创建界面
    iface = gr.Interface(
        fn=calculate_similarity,
        inputs=[
            gr.Textbox(label="第一段文本", placeholder="请输入第一段中文文本...", lines=3),
            gr.Textbox(label="第二段文本", placeholder="请输入第二段中文文本...", lines=3)
        ],
        outputs=[
            gr.Textbox(label="相似度描述"),
            gr.Number(label="相似度得分", precision=4)
        ],
        title="StructBERT中文文本相似度计算",
        description=description,
        examples=[
            ["今天天气真好", "今天的天气非常不错"],
            ["我喜欢吃苹果", "香蕉是我的最爱"],
            ["人工智能正在改变世界", "AI技术正在快速发展"]
        ],
        theme="soft"
    )
    
    return iface

# 主函数
if __name__ == "__main__":
    # 创建并启动界面
    iface = create_interface()
    
    # 启动服务,设置共享链接(如果需要外部访问)
    iface.launch(
        server_name="0.0.0.0",  # 允许外部访问
        server_port=7860,        # 端口号
        share=False              # 不创建公开链接
    )

保存这个文件后,安装必要的依赖:

# 安装必要的Python包
pip install sentence-transformers gradio torch

# 如果下载慢,可以使用国内镜像
pip install sentence-transformers gradio torch -i https://pypi.tuna.tsinghua.edu.cn/simple

然后运行服务:

python deploy_structbert.py

等待一会儿,你会看到类似这样的输出:

Running on local URL:  http://0.0.0.0:7860

在浏览器中打开 http://你的服务器IP:7860,就能看到模型的服务界面了。第一次运行时会自动下载模型文件,可能需要几分钟时间。

3. 添加Prometheus监控

现在模型服务已经跑起来了,但我们还不知道它运行得怎么样。接下来我们给服务加上Prometheus监控,这样就能收集各种运行指标了。

3.1 安装Prometheus客户端

首先,我们需要在Python服务中添加Prometheus客户端库:

pip install prometheus-client

3.2 创建监控中间件

创建一个新的文件monitoring.py,用来处理所有的监控逻辑:

# monitoring.py
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
from prometheus_client import start_http_server as start_prometheus_server
import time
from functools import wraps
import logging

# 设置日志
logger = logging.getLogger(__name__)

# 定义Prometheus指标
# 请求计数器
REQUEST_COUNT = Counter(
    'structbert_requests_total',
    'Total number of requests',
    ['method', 'endpoint', 'status']
)

# 请求延迟直方图
REQUEST_LATENCY = Histogram(
    'structbert_request_duration_seconds',
    'Request latency in seconds',
    ['method', 'endpoint']
)

# 活跃请求数
ACTIVE_REQUESTS = Gauge(
    'structbert_active_requests',
    'Number of active requests'
)

# 相似度分数分布
SIMILARITY_SCORE = Histogram(
    'structbert_similarity_score',
    'Distribution of similarity scores',
    buckets=[0, 0.2, 0.4, 0.6, 0.8, 1.0]
)

# 错误计数器
ERROR_COUNT = Counter(
    'structbert_errors_total',
    'Total number of errors',
    ['error_type']
)

# 模型加载时间
MODEL_LOAD_TIME = Gauge(
    'structbert_model_load_time_seconds',
    'Time taken to load the model'
)

# 监控装饰器
def monitor_request(endpoint_name=""):
    """
    监控请求的装饰器
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 记录活跃请求数
            ACTIVE_REQUESTS.inc()
            
            start_time = time.time()
            method = "POST" if endpoint_name else "GET"
            
            try:
                # 执行函数
                result = func(*args, **kwargs)
                
                # 记录成功请求
                REQUEST_COUNT.labels(
                    method=method,
                    endpoint=endpoint_name or func.__name__,
                    status="success"
                ).inc()
                
                # 记录延迟
                REQUEST_LATENCY.labels(
                    method=method,
                    endpoint=endpoint_name or func.__name__
                ).observe(time.time() - start_time)
                
                # 如果返回相似度分数,记录到直方图
                if isinstance(result, tuple) and len(result) > 1:
                    similarity_score = result[1]
                    if isinstance(similarity_score, (int, float)):
                        SIMILARITY_SCORE.observe(similarity_score)
                
                return result
                
            except Exception as e:
                # 记录错误
                error_type = type(e).__name__
                ERROR_COUNT.labels(error_type=error_type).inc()
                
                REQUEST_COUNT.labels(
                    method=method,
                    endpoint=endpoint_name or func.__name__,
                    status="error"
                ).inc()
                
                logger.error(f"请求处理出错: {str(e)}")
                raise
                
            finally:
                # 减少活跃请求数
                ACTIVE_REQUESTS.dec()
                
        return wrapper
    return decorator

# 启动Prometheus指标服务器
def start_metrics_server(port=8000):
    """
    启动Prometheus指标服务器
    """
    try:
        start_prometheus_server(port)
        logger.info(f"Prometheus指标服务器已启动,端口: {port}")
        logger.info(f"指标地址: http://localhost:{port}/metrics")
        return True
    except Exception as e:
        logger.error(f"启动Prometheus服务器失败: {str(e)}")
        return False

# 创建指标端点(供Gradio或其他框架使用)
def metrics_endpoint():
    """
    返回Prometheus格式的指标数据
    """
    return generate_latest(), 200, {'Content-Type': CONTENT_TYPE_LATEST}

3.3 集成监控到主服务

现在修改我们之前的主服务文件,集成监控功能。创建一个新的文件structbert_with_monitoring.py

# structbert_with_monitoring.py
import gradio as gr
from sentence_transformers import SentenceTransformer, util
import time
import logging
from monitoring import monitor_request, start_metrics_server, MODEL_LOAD_TIME

# 设置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# 加载模型(带监控)
@monitor_request("load_model")
def load_model():
    """
    加载StructBERT模型,并记录加载时间
    """
    logger.info("开始加载StructBERT文本相似度模型...")
    start_time = time.time()
    
    try:
        # 使用StructBERT中文文本相似度模型
        model = SentenceTransformer('uer/sbert-base-chinese-nli')
        
        load_time = time.time() - start_time
        MODEL_LOAD_TIME.set(load_time)
        
        logger.info(f"模型加载完成,耗时: {load_time:.2f}秒")
        return model
        
    except Exception as e:
        logger.error(f"模型加载失败: {str(e)}")
        raise

# 计算文本相似度(带监控)
@monitor_request("calculate_similarity")
def calculate_similarity(text1, text2):
    """
    计算两段中文文本的相似度
    """
    try:
        if not text1.strip() or not text2.strip():
            return "错误:请输入两段文本", 0.0
        
        # 编码文本
        embeddings = model.encode([text1, text2], convert_to_tensor=True)
        
        # 计算余弦相似度
        cosine_scores = util.cos_sim(embeddings[0], embeddings[1])
        similarity_score = float(cosine_scores[0][0])
        
        # 根据相似度分数给出描述
        if similarity_score >= 0.8:
            description = "高度相似"
        elif similarity_score >= 0.6:
            description = "比较相似" 
        elif similarity_score >= 0.4:
            description = "部分相似"
        elif similarity_score >= 0.2:
            description = "略有相关"
        else:
            description = "基本不相关"
        
        return description, similarity_score
        
    except Exception as e:
        logger.error(f"计算相似度时出错: {str(e)}")
        return f"计算错误: {str(e)}", 0.0

# 创建Gradio界面
def create_interface():
    description = """
    ## StructBERT中文文本相似度计算器(带监控版)
    
    输入两段中文文本,模型会自动计算它们的相似度得分。
    服务已集成Prometheus监控,可通过 http://localhost:8000/metrics 查看指标。
    
    **监控指标包括**:
    - 请求总数和成功率
    - 请求响应时间
    - 活跃请求数
    - 相似度分数分布
    - 错误统计
    
    **相似度解读**:
    - 0.8-1.0:高度相似
    - 0.6-0.8:比较相似
    - 0.4-0.6:部分相似  
    - 0.2-0.4:略有相关
    - 0.0-0.2:基本不相关
    """
    
    iface# 1. 概述

本文,我们来分享 MyBatis 的日志模块,对应 `logging` 包。如下图所示:

![`logging` 包](http://static2.iocoder.cn/images/MyBatis/2020_01_07/01.png)

在 [《精尽 MyBatis 源码解析 —— 项目结构一览》](http://svip.iocoder.cn/MyBatis/intro) 中,简单介绍了这个模块如下:

> 无论在开发测试环境中,还是在线上生产环境中,日志在整个系统中的地位都是非常重要的。良好的日志功能可以帮助开发人员和测试人员快速定位 Bug 代码,也可以帮助运维人员快速定位性能瓶颈等问题。目前的 Java 世界中存在很多优秀的日志框架,例如 Log4j、 Log4j2、Apache Commons Log、java.util.logging、slf4j 等。
>
> MyBatis 作为一个设计优良的框架,除了提供详细的日志输出信息,还要能够集成多种日志框架,其日志模块的一个主要功能就是**集成第三方日志框架**。

本文涉及的类如下图所示:

![类图](http://static2.iocoder.cn/images/MyBatis/2020_01_07/02.png)

下面,我们逐小节来分享。

# 2. LogFactory

`org.apache.ibatis.logging.LogFactory` ,Log 工厂类。

## 2.1 构造方法

```java
// LogFactory.java

/**
 * Marker to be used by logging implementations that support markers
 */
public static final String MARKER = "MYBATIS";

/**
 * 使用的 Log 的构造方法
 */
private static Constructor<? extends Log> logConstructor;

static {
    // <1> 逐个尝试,判断使用哪个 Log 的实现类,即初始化 logConstructor 属性
    tryImplementation(LogFactory::useSlf4jLogging);
    tryImplementation(LogFactory::useCommonsLogging);
    tryImplementation(LogFactory::useLog4J2Logging);
    tryImplementation(LogFactory::useLog4JLogging);
    tryImplementation(LogFactory::useJdkLogging);
    tryImplementation(LogFactory::useNoLogging);
}

logConstructor 静态属性,使用的 Log 的构造方法。在 <1> 处,会逐个尝试,判断使用哪个 Log 的实现类,即初始化 logConstructor 属性。

#tryImplementation(Runnable runnable) 方法,尝试调用方法,若失败,则继续下一个。代码如下:

// LogFactory.java

private static void tryImplementation(Runnable runnable) {
    // 若 logConstructor 为空,说明还未初始化成功,则运行 runnable
    if (logConstructor == null) {
        try {
            runnable.run();
        } catch (Throwable t) {
            // ignore
        }
    }
}

#useXXXLogging() 方法,尝试使用对应的 Log 组件。代码如下:

// LogFactory.java

public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}

public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}

public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}

public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}

public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}

public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}

public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}

#setImplementation(Class<? extends Log> implClass) 方法,设置 logConstructor 属性。代码如下:

// LogFactory.java

private static void setImplementation(Class<? extends Log> implClass) {
    try {
        // 获得参数为 String 的构造方法
        Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
        // 创建 Log 对象
        Log log = candidate.newInstance(LogFactory.class.getName());
        if (log.isDebugEnabled()) {
            log.debug("Logging initialized using '" + implClass + "' adapter.");
        }
        // 创建成功,意味着可以使用,设置为 logConstructor
        logConstructor = candidate;
    } catch (Throwable t) {
        throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
}

2.2 getLog

#getLog(...) 方法,获得 Log 对象。代码如下:

// LogFactory.java

public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
}

public static Log getLog(String logger) {
    try {
        return logConstructor.newInstance(logger);
    } catch (Throwable t) {
        throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
}

2.3 小结

因为艿艿使用 slf4j + Logback 的组合,所以最终使用的 Log 实现类是 org.apache.ibatis.logging.slf4j.Slf4jImpl 。下面,我们来看看具体的实现类。

3. Log

org.apache.ibatis.logging.Log ,MyBatis Log 接口。代码如下:

// Log.java

public interface Log {

    boolean isDebugEnabled();

    boolean isTraceEnabled();

    void error(String s, Throwable e);

    void error(String s);

    void debug(String s);

    void trace(String s);

    void warn(String s);

}

3.1 Slf4jImpl

org.apache.ibatis.logging.slf4j.Slf4jImpl ,实现 Log 接口,SLF4J 实现类。代码如下:

// Slf4jImpl.java

public class Slf4jImpl implements Log {

    /**
     * Log 对象
     */
    private Log log;

    public Slf4jImpl(String clazz) {
        // 使用 SLF4J 的 LoggerFactory 获得 SLF4J 的 Log 对象
        Logger logger = LoggerFactory.getLogger(clazz);

        // 如果是 LocationAwareLogger ,则创建 Slf4jLocationAwareLoggerImpl 对象
        if (logger instanceof LocationAwareLogger) {
            try {
                // check for slf4j >= 1.6 method signature
                logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class);
                log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger);
                return;
            } catch (SecurityException | NoSuchMethodException e) {
                // fail-back to Slf4jLoggerImpl
            }
        }

        // 否则,创建 Slf4jLoggerImpl 对象
        // Logger is not LocationAwareLogger or slf4j version < 1.6
        log = new Slf4jLoggerImpl(logger);
    }

    @Override
    public boolean isDebugEnabled() {
        return log.isDebugEnabled();
    }

    @Override
    public boolean isTraceEnabled() {
        return log.isTraceEnabled();
    }

    @Override
    public void error(String s, Throwable e) {
        log.error(s, e);
    }

    @Override
    public void error(String s) {
        log.error(s);
    }

    @Override
    public void debug(String s) {
        log.debug(s);
    }

    @Override
    public void trace(String s) {
        log.trace(s);
    }

    @Override
    public void warn(String s) {
        log.warn(s);
    }

}
  • 根据传入的 clazz 参数,获得 SLF4J 的 org.slf4j.Logger 对象。根据是否是 org.slf4j.spi.LocationAwareLogger 类型,创建对应的 Slf4jLocationAwareLoggerImpl 或 Slf4jLoggerImpl 对象。

3.1.1 Slf4jLoggerImpl

org.apache.ibatis.logging.slf4j.Slf4jLoggerImpl ,实现 Log 接口,普通 SLF4J 实现类。代码如下:

// Slf4jLoggerImpl.java

public class Slf4jLoggerImpl implements Log {

    /**
     * SLF4J Logger 对象
     */
    private Log log;

    public Slf4jLoggerImpl(Logger logger) {
        this.log = new Logger(logger);
    }

    // 省略 isDebugEnabled 等的方法。实现上,都是调用 log 属性的对应的方法
}
  • 在构造方法中,会创建 org.apache.ibatis.logging.slf4j.Slf4jLoggerImpl.Logger 对象。代码如下:

    // Slf4jLoggerImpl.java
    
    private static class Logger implements Log {
    
        /**
         * SLF4J Logger 对象
         */
        private org.slf4j.Logger logger;
    
        public Logger(org.slf4j.Logger logger) {
            this.logger = logger;
        }
    
        @Override
        public boolean isDebugEnabled() {
            return logger.isDebugEnabled();
        }
    
        @Override
        public boolean isTraceEnabled() {
            return logger.isTraceEnabled();
        }
    
        @Override
        public void error(String s, Throwable e) {
            logger.error(s, e);
        }
    
        @Override
        public void error(String s) {
            logger.error(s);
        }
    
        @Override
        public void debug(String s) {
            logger.debug(s);
        }
    
        @Override
        public void trace(String s) {
            logger.trace(s);
        }
    
        @Override
        public void warn(String s) {
            logger.warn(s);
        }
    
    }
    
    • 实际上,是 SLF4J Logger 的装饰器,在其基础上,实现 MyBatis 的 Log 接口。

3.1.2 Slf4jLocationAwareLoggerImpl

org.apache.ibatis.logging.slf4j.Slf4jLocationAwareLoggerImpl ,实现 Log 接口,支持 LocationAwareLogger SLF4J 实现类。代码如下:

// Slf4jLocationAwareLoggerImpl.java

public class Slf4jLocationAwareLoggerImpl implements Log {

    /**
     * SLF4J LocationAwareLogger 对象
     */
    private static final Marker MARKER = MarkerFactory.getMarker(LogFactory.MARKER);

    /**
     * LocationAwareLogger 对象
     */
    private static final String FQCN = Slf4jImpl.class.getName();

    /**
     * LocationAwareLogger 对象
     */
    private LocationAwareLogger logger;

    public Slf4jLocationAwareLoggerImpl(LocationAwareLogger logger) {
        this.logger = logger;
    }

    @Override
    public boolean isDebugEnabled() {
        return logger.isDebugEnabled();
    }

    @Override
    public boolean isTraceEnabled() {
        return logger.isTraceEnabled();
    }

    @Override
    public void error(String s, Throwable e) {
        logger.log(MARKER, FQCN, LocationAwareLogger.ERROR_INT, s, null, e);
    }

    @Override
    public void error(String s) {
        logger.log(MARKER, FQCN, LocationAwareLogger.ERROR_INT, s, null, null);
    }

    @Override
    public void debug(String s) {
        logger.log(MARKER, FQCN, LocationAwareLogger.DEBUG_INT, s, null, null);
    }

    @Override
    public void trace(String s) {
        logger.log(MARKER, FQCN, LocationAwareLogger.TRACE_INT, s, null, null);
    }

    @Override
    public void warn(String s) {
        logger.log(MARKER, FQCN, LocationAwareLogger.WARN_INT, s, null, null);
    }

}
  • 实际上,是 SLF4J LocationAwareLogger 的装饰器,在其基础上,实现 MyBatis 的 Log 接口。

3.2 JakartaCommonsLoggingImpl

org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl ,实现 Log 接口,Apache Commons Logging 实现类。代码如下:

// JakartaCommonsLoggingImpl.java

public class JakartaCommonsLoggingImpl implements Log {

    /**
     * Apache Common Logging Log 对象
     */
    private Log log;

    public JakartaCommonsLoggingImpl(String clazz) {
        // 获得 Apache Common Logging Log 对象
        log = LogFactory.getLog(clazz);
    }

    @Override
    public boolean isDebugEnabled() {
        return log.isDebugEnabled();
    }

    @Override
    public boolean isTraceEnabled() {
        return log.isTraceEnabled();
    }

    @Override
    public void error(String s, Throwable e) {
        log.error(s, e);
    }

    @Override
    public void error(String s) {
        log.error(s);
    }

    @Override
    public void debug(String s) {
        log.debug(s);
    }

    @Override
    public void trace(String s) {
        log.trace(s);
    }

    @Override
    public void warn(String s) {
        log.warn(s);
    }

}

3.3 Log4jImpl

org.apache.ibatis.logging.log4j.Log4jImpl ,实现 Log 接口,Log4J 实现类。代码如下:

// Log4jImpl.java

public class Log4jImpl implements Log {

    /**
     * Log4J Logger 对象
     */
    private Logger log;

    public Log4jImpl(String clazz) {
        // 获得 Log4J Logger 对象
        log = Logger.getLogger(clazz);
    }

    @Override
    public boolean isDebugEnabled() {
        return log.isDebugEnabled();
    }

    @Override
    public boolean isTraceEnabled() {
        return log.isTraceEnabled();
    }

    @Override
    public void error(String s, Throwable e) {
        log.error(s, e);
    }

    @Override
    public void error(String s) {
        log.error(s);
    }

    @Override
    public void debug(String s) {
        log.debug(s);
    }

    @Override
    public void trace(String s) {
        log.trace(s);
    }

    @Override
    public void warn(String s) {
        log.warn(s);
    }

}

3.4 Log4j2Impl

org.apache.ibatis.logging.log4j2.Log4j2Impl ,实现 Log 接口,Log4J2 实现类。代码如下:

// Log4j2Impl.java

public class Log4j2Impl implements Log {

    /**
     * Log4J2 Logger 对象
     */
    private Logger log;

    public Log4j2Impl(String clazz) {
        // 获得 Log4J2 Logger 对象
        log = LogManager.getLogger(clazz);
    }

    @Override
    public boolean isDebugEnabled() {
        return log.isDebugEnabled();
    }

    @Override
    public boolean isTraceEnabled() {
        return log.isTraceEnabled();
    }

    @Override
    public void error(String s, Throwable e) {
        log.error(s, e);
    }

    @Override
    public void error(String s) {
        log.error(s);
    }

    @Override
    public void debug(String s) {
        log.debug(s);
    }

    @Override
    public void trace(String s) {
        log.trace(s);
    }

    @Override
    public void warn(String s) {
        log.warn(s);
    }

}

3.5 Jdk14LoggingImpl

org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl ,实现 Log 接口,JDK Logging 实现类。代码如下:

// Jdk14LoggingImpl.java

public class Jdk14LoggingImpl implements Log {

    /**
     * JDK Logger 对象
     */
    private Logger log;

    public Jdk14LoggingImpl(String clazz) {
        // 获得 JDK Logger 对象
        log = Logger.getLogger(clazz);
    }

    @Override
    public boolean isDebugEnabled() {
        return log.isLoggable(Level.FINE);
    }

    @Override
    public boolean isTraceEnabled() {
        return log.isLoggable(Level.FINER);
    }

    @Override
    public void error(String s, Throwable e) {
        log.log(Level.SEVERE, s, e);
    }

    @Override
    public void error(String s) {
        log.log(Level.SEVERE, s);
    }

    @Override
    public void debug(String s) {
        log.log(Level.FINE, s);
    }

    @Override
    public void trace(String s) {
        log.log(Level.FINER, s);
    }

    @Override
    public void warn(String s) {
        log.log(Level.WARNING, s);
    }

}

3.6 StdOutImpl

org.apache.ibatis.logging.stdout.StdOutImpl ,实现 Log 接口,StdOut 实现类。代码如下:

// StdOutImpl.java

public class StdOutImpl implements Log {

    public StdOutImpl(String clazz) {
        // Do Nothing
    }

    @Override
    public boolean isDebugEnabled() {
        return true;
    }

    @Override
    public boolean isTraceEnabled() {
        return true;
    }

    @Override
    public void error(String s, Throwable e) {
        System.err.println(s);
        e.printStackTrace(System.err);
    }

    @Override
    public void error(String s) {
        System.err.println(s);
    }

    @Override
    public void debug(String s) {
        System.out.println(s);
    }

    @Override
    public void trace(String s) {
        System.out.println(s);
    }

    @Override
    public void warn(String s) {
        System.out.println(s);
    }

}

3.7 NoLoggingImpl

org.apache.ibatis.logging.nologging.NoLoggingImpl ,实现 Log 接口,空实现类。代码如下:

// NoLoggingImpl.java

public class NoLoggingImpl implements Log {

    public NoLoggingImpl(String clazz) {
        // Do Nothing
    }

    @Override
    public boolean isDebugEnabled() {
        return false;
    }

    @Override
    public boolean isTraceEnabled() {
        return false;
    }

    @Override
    public void error(String s, Throwable e) {
        // Do Nothing
    }

    @Override
    public void error(String s) {
        // Do Nothing
    }

    @Override
    public void debug(String s) {
        // Do Nothing
    }

    @Override
    public void trace(String s) {
        // Do Nothing
    }

    @Override
    public void warn(String s) {
        // Do Nothing
    }

}

4. BaseJdbcLogger

logging 包中,除了 org.apache.ibatis.logging.Log 接口和其相关的实现类,还有 org.apache.ibatis.logging.jdbc 包,它主要是将 JDBC 操作,记录成日志。而 BaseJdbcLogger 就是 jdbc 包下所有 Logger 的抽象基类。

4.1 构造方法

// BaseJdbcLogger.java

/**
 * 常用的 Set 的方法名集合
 */
private static final Set<String> SET_METHODS = new HashSet<>();
/**
 * 执行 SQL 的方法名集合
 */
private static final Set<String> EXECUTE_METHODS = new HashSet<>();

/**
 * {@link #SET_METHODS} 的初始化
 */
static {
    SET_METHODS.add("setString");
    SET_METHODS.add("setInt");
    SET_METHODS.add("setBoolean");
    SET_METHODS.add("setShort");
    SET_METHODS.add("setLong");
    SET_METHODS.add("setDouble");
    SET_METHODS.add("setFloat");
    SET_METHODS.add("setTimestamp");
    SET_METHODS.add("setDate");
    SET_METHODS.add("setTime");
    SET_METHODS.add("setArray");
    SET_METHODS.add("setBigDecimal");
    SET_METHODS.add("setAsciiStream");
    SET_METHODS.add("setBinaryStream");
    SET_METHODS.add("setBlob");
    SET_METHODS.add("setBytes");
    SET_METHODS.add("setCharacterStream");
    SET_METHODS.add("setNCharacterStream");
    SET_METHODS.add("setClob");
    SET_METHODS.add("setNClob");
    SET_METHODS.add("setObject");
    SET_METHODS.add("setNull");
}

/**
 * {@link #EXECUTE_METHODS} 的初始化
 */
static {
    EXECUTE_METHODS.add("execute");
    EXECUTE_METHODS.add("executeUpdate");
    EXECUTE_METHODS.add("executeQuery");
    EXECUTE_METHODS.add("addBatch");
}

/**
 * Connection 连接对象的日志
 */
protected final Log connectionLog;
/**
 * Statement 对象的日志
 */
protected final Log statementLog;
/**
 * ResultSet 结果集的日志
 */
protected final Log resultSetLog;
/**
 * 当前连接的 Connection 对象
 */
protected final Map<String, Object> columnValues = new HashMap<>();

/**
 * 连接的编号
 */
protected int columnIndex = 1;

public BaseJdbcLogger(Log connLog, Log statementLog, Log resultSetLog) {
    this.connectionLog = connLog;
    this.statementLog = statementLog;
    this.resultSetLog = resultSetLog;
}
  • 每个属性,看代码注释。

4.2 setColumn

#setColumn(String key, Object value) 方法,添加到 columnValues 中。代码如下:

// BaseJdbcLogger.java

protected void setColumn(String key, Object value) {
    columnValues.put(key, value);
}

4.3 getColumn

#getColumn(String key) 方法,获得指定 key 的值。代码如下:

// BaseJdbcLogger.java

protected Object getColumn(String key) {
    return columnValues.get(key);
}

4.4 其他方法

// BaseJdbcLogger.java

protected String getParameterValueString() {
    List<Object> typeList = new ArrayList<>(columnValues.values());
    return typeList.toString();
}

protected String getColumnString() {
    return columnValues.toString();
}

protected void clearColumnInfo() {
    columnIndex = 1;
    columnValues.clear();
}

protected boolean isDebugEnabled() {
    return statementLog.isDebugEnabled();
}

protected boolean isTraceEnabled() {
    return statementLog.isTraceEnabled();
}

protected void debug(String text, boolean input) {
    if (statementLog.isDebugEnabled()) {
        statementLog.debug(prefix(input) + text);
    }
}

protected void trace(String text, boolean input) {
    if (statementLog.isTraceEnabled()) {
        statementLog.trace(prefix(input) + text);
    }
}

private String prefix(boolean isInput) {
    if (isInput) {
        return "==> ";
    } else {
        return "<== ";
    }
}

4.5 静态方法

// BaseJdbcLogger.java

public static boolean isPmdKnown() {
    return PMD_KNOWN;
}

public static boolean isPmdEnabled() {
    return PMD_ENABLED;
}

public static void removeBreakingWhitespace(StringBuilder original) {
    int length = original.length();
    for (int index = 0; index < length; index++) {
        char current = original.charAt(index);
        if (current == '\n' || current == '\r' || current == '\t') {
            original.setCharAt(index, ' ');
        }
    }
}

public static String trim(String string) {
    return string == null ? null : string.trim();
}

5. ConnectionLogger

org.apache.ibatis.logging.jdbc.ConnectionLogger ,继承 BaseJdbcLogger 类,Connection 日志增强类,负责打印 Connection 的日志。

5.1 构造方法

// ConnectionLogger.java

/**
 * Connection 对象的代理
 */
private final Connection connection;

private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    super(statementLog, statementLog, statementLog);
    this.connection = conn;
    this.queryStack = queryStack;
}
  • connection 属性,Connection 对象的代理。

5.2 newInstance

#newInstance(Connection conn, Log statementLog, int queryStack) 静态方法,创建 Connection 的代理对象。代码如下:

// ConnectionLogger.java

public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
  • 创建 ConnectionLogger 对象,作为 InvocationHandler 对象,创建 Connection 的代理对象。

5.3 invoke

// ConnectionLogger.java

@Override
public Object invoke(Object proxy, Method method, Object[] params)
        throws Throwable {
    try {
        // 如果是调用从 Object 继承的方法,直接调用,不进行代理
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, params);
        }
        // 如果是调用 prepareStatement 方法,则打印要执行的 SQL 语句,并创建 PreparedStatement 的代理对象
        if ("prepareStatement".equals(method.getName())) {
            if (isDebugEnabled()) {
                debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
            }
            // 创建 PreparedStatement 对象
            PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
            // 创建 PreparedStatement 的代理对象
            stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
            return stmt;
        // 如果是调用 prepareCall 方法,则打印要执行的 SQL 语句,并创建 CallableStatement 的代理对象
        } else if ("prepareCall".equals(method.getName())) {
            if (isDebugEnabled()) {
                debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
            }
            // 创建 CallableStatement 对象
            PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
            // 创建 CallableStatement 的代理对象
            stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
            return stmt;
        // 如果是调用 createStatement 方法,则创建 Statement 的代理对象
        } else if ("createStatement".equals(method.getName())) {
            // 创建 Statement 对象
            Statement stmt = (Statement) method.invoke(connection, params);
            // 创建 Statement 的代理对象
            stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
            return stmt;
        } else {
            // 其他方法,直接调用
            return method.invoke(connection, params);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}
  • 根据不同的方法,进行不同的日志打印处理。

5.4 debug

#debug(String text, boolean input) 方法,代码如下:

// ConnectionLogger.java

@Override
protected void debug(String text, boolean input) {
    if (statementLog.isDebugEnabled()) {
        statementLog.debug(prefix(input) + text);
    }
}

@Override
protected void trace(String text, boolean input) {
    if (statementLog.isTraceEnabled()) {
        statementLog.trace(prefix(input) + text);
    }
}

6. PreparedStatementLogger

org.apache.ibatis.logging.jdbc.PreparedStatementLogger ,继承 BaseJdbcLogger 类,PreparedStatement 日志增强类,负责打印 PreparedStatement 的日志。

6.1 构造方法

// PreparedStatementLogger.java

/**
 * PreparedStatement 对象的代理
 */
private final PreparedStatement statement;

private PreparedStatementLogger(PreparedStatement stmt, Log statementLog, int queryStack) {
    super(statementLog, statementLog, statementLog);
    this.statement = stmt;
    this.queryStack = queryStack;
}
  • statement 属性,PreparedStatement 对象的代理。

6.2 newInstance

#newInstance(PreparedStatement stmt, Log statementLog, int queryStack) 静态方法,创建 PreparedStatement 的代理对象。代码如下:

// PreparedStatementLogger.java

public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
    InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
    ClassLoader cl = PreparedStatement.class.getClassLoader();
    return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
}
  • 创建 PreparedStatementLogger 对象,作为 InvocationHandler 对象,创建 PreparedStatement 的代理对象。

6.3 invoke

// PreparedStatementLogger.java

@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
        // 如果是调用从 Object 继承的方法,直接调用,不进行代理
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, params);
        }
        // 如果是调用 execute 系列方法,则打印日志
        if (EXECUTE_METHODS.contains(method.getName())) {
            if (isDebugEnabled()) {
                debug("Parameters: " + getParameterValueString(), true);
            }
            // 清空 columnValues
            clearColumnInfo();
            // 执行方法
            if ("executeQuery".equals(method.getName())) {
                // 如果是执行查询方法,则创建 ResultSet 的代理对象
                ResultSet rs = (ResultSet) method.invoke(statement, params);
                return ResultSetLogger.newInstance(rs, statementLog, queryStack);
            } else {
                return method.invoke(statement, params);
            }
        // 如果是调用 set 系列方法,则设置参数到 columnValues 中
        } else if (SET_METHODS.contains(method.getName())) {
            if ("setNull".equals(method.getName())) {
                setColumn(params[0], null);
            } else {
                setColumn(params[0], params[1]);
            }
            return method.invoke(statement, params);
        // 如果是调用 getResultSet 方法,则创建 ResultSet 的代理对象
        } else if ("getResultSet".equals(method.getName())) {
            ResultSet rs = (ResultSet) method.invoke(statement, params);
            return ResultSetLogger.newInstance(rs, statementLog, queryStack);
        // 如果是调用 getUpdateCount 方法,则打印日志
        } else if ("getUpdateCount".equals(method.getName())) {
            int updateCount = (Integer) method.invoke(statement, params);
            if (updateCount != -1) {
                debug("   Updates: " + updateCount, false);
            }
            return updateCount;
        } else {
            return method.invoke(statement, params);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}
  • 根据不同的方法,进行不同的日志打印处理。

6.4 debug

#debug(String text, boolean input) 方法,代码如下:

// PreparedStatementLogger.java

@Override
protected void debug(String text, boolean input) {
    if (statementLog.isDebugEnabled()) {
        statementLog.debug(prefix(input) + text);
    }
}

@Override
protected void trace(String text, boolean input) {
    if (statementLog.isTraceEnabled()) {
        statementLog.trace(prefix(input) + text);
    }
}

7. StatementLogger

org.apache.ibatis.logging.jdbc.StatementLogger ,继承 BaseJdbcLogger 类,Statement 日志增强类,负责打印 Statement 的日志。

7.1 构造方法

// StatementLogger.java

/**
 * Statement 对象的代理
 */
private final Statement statement;

private StatementLogger(Statement stmt, Log statementLog, int queryStack) {
    super(statementLog, statementLog, statementLog);
    this.statement = stmt;
    this.queryStack = queryStack;
}
  • statement 属性,Statement 对象的代理。

7.2 newInstance

#newInstance(Statement stmt, Log statementLog, int queryStack) 静态方法,创建 Statement 的代理对象。代码如下:

// StatementLogger.java

public static Statement newInstance(Statement stmt, Log statementLog, int queryStack) {
    InvocationHandler handler = new StatementLogger(stmt, statementLog, queryStack);
    ClassLoader cl = Statement.class.getClassLoader();
    return (Statement) Proxy.newProxyInstance(cl, new Class[]{Statement.class}, handler);
}
  • 创建 StatementLogger 对象,作为 InvocationHandler 对象,创建 Statement 的代理对象。

7.3 invoke

// StatementLogger.java

@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
        // 如果是调用从 Object 继承的方法,直接调用,不进行代理
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, params);
        }
        // 如果是调用 execute 系列方法,则打印日志
        if (EXECUTE_METHODS.contains(method.getName())) {
            if (isDebugEnabled()) {
                debug("Parameters: " + getParameterValueString(), true);
            }
            // 清空 columnValues
            clearColumnInfo();
            // 执行方法
            if ("executeQuery".equals(method.getName())) {
                // 如果是执行查询方法,则创建 ResultSet 的代理对象
                ResultSet rs = (ResultSet) method.invoke(statement, params);
                return ResultSetLogger.newInstance(rs, statementLog, queryStack);
            } else {
                return method.invoke(statement, params);
            }
        // 如果是调用 getResultSet 方法,则创建 ResultSet 的代理对象
        } else if ("getResultSet".equals(method.getName())) {
            ResultSet rs = (ResultSet) method.invoke(statement, params);
            return ResultSetLogger.newInstance(rs, statementLog, queryStack);
        // 如果是调用 getUpdateCount 方法,则打印日志
        } else if ("getUpdateCount".equals(method.getName())) {
            int updateCount = (Integer) method.invoke(statement, params);
            if (updateCount != -1) {
                debug("   Updates: " + updateCount, false);
            }
            return updateCount;
        } else {
            return method.invoke(statement, params);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}
  • 根据不同的方法,进行不同的日志打印处理。

7.4 debug

#debug(String text, boolean input) 方法,代码如下:

// StatementLogger.java

@Override
protected void debug(String text, boolean input) {
    if (statementLog.isDebugEnabled()) {
        statementLog.debug(prefix(input) + text);
    }
}

@Override
protected void trace(String text, boolean input) {
    if (statementLog.isTraceEnabled()) {
        statementLog.trace(prefix(input) + text);
    }
}

8. ResultSetLogger

org.apache.ibatis.logging.jdbc.ResultSetLogger ,继承 BaseJdbcLogger 类,ResultSet 日志增强类,负责打印 ResultSet 的日志。

8.1 构造方法

// ResultSetLogger.java

/**
 * ResultSet 对象的代理
 */
private final ResultSet rs;
/**
 * 记录已经打印日志的 ResultSet 的列的编号的集合
 */
private final Set<Integer> firstPrintedColumnIndexes = new HashSet<>();
/**
 * 记录已经打印日志的 ResultSet 的列名的集合
 */
private final Set<String> firstPrintedColumnNames = new HashSet<>();

private ResultSetLogger(ResultSet rs, Log statementLog, int queryStack) {
    super(statementLog, statementLog, statementLog);
    this.rs = rs;
    this.queryStack = queryStack;
}
  • firstPrintedColumnIndexes 属性,记录已经打印日志的 ResultSet 的列的编号的集合。
  • firstPrintedColumnNames 属性,记录已经打印日志的 ResultSet 的列名的集合。

8.2 newInstance

#newInstance(ResultSet rs, Log statementLog, int queryStack) 静态方法,创建 ResultSet 的代理对象。代码如下:

// ResultSetLogger.java

public static ResultSet newInstance(ResultSet rs, Log statementLog, int queryStack) {
    InvocationHandler handler = new ResultSetLogger(rs, statementLog, queryStack);
    ClassLoader cl = ResultSet.class.getClassLoader();
    return (ResultSet) Proxy.newProxyInstance(cl, new Class[]{ResultSet.class}, handler);
}
  • 创建 ResultSetLogger 对象,作为 InvocationHandler 对象,创建 ResultSet 的代理对象。

8.3 invoke

// ResultSetLogger.java

@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
        // 如果是调用从 Object 继承的方法,直接调用,不进行代理
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, params);
        }
        // 执行方法
        Object o = method.invoke(rs, params);
        // 如果是调用 next 方法,则打印日志
        if ("next".equals(method.getName())) {
            // 打印日志
            if ((Boolean) o) {
                // 增加 columnIndex
                columnIndex = 1;
                // 打印列名
                if (isTraceEnabled()) {
                    ResultSetMetaData rsmd = rs.getMetaData();
                    final int columnCount = rsmd.getColumnCount();
                    // 如果是第一次,则打印列名
                    if (firstPrintedColumnNames.isEmpty()) {
                        firstPrintedColumnNames.addAll(getColumnNames(rsmd, columnCount));
                    }
                    // 打印列值
                    if (columnCount != firstPrintedColumnIndexes.size()) {
                        firstPrintedColumnIndexes.clear();
                        for (int i = 1; i <= columnCount; i++) {
                            firstPrintedColumnIndexes.add(i);
                        }
                    }
                }
                // 打印列值
                if (isDebugEnabled()) {
                    debug("     Row: " + getColumnValueString(), false);
                }
            }
        }
        // 清空 columnValues
        clearColumnInfo();
        return o;
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}
  • 根据不同的方法,进行不同的日志打印处理。

8.4 getColumnNames

#getColumnNames(ResultSetMetaData rsmd, int columnCount) 方法,获得列名。代码如下:

// ResultSetLogger.java

private List<String> getColumnNames(ResultSetMetaData rsmd, int columnCount) throws SQLException {
    List<String> columnNames = new ArrayList<>();
    for (int i = 1; i <= columnCount; i++) {
        columnNames.add(rsmd.getColumnLabel(i));
    }
    return columnNames;
}

8.5 getColumnValueString

#getColumnValueString() 方法,获得列值。代码如下:

// ResultSetLogger.java

private String getColumnValueString() {
    List<Object> columnValues = new ArrayList<>();
    for (int i = 1; i <= columnIndex; i++) {
        Object value = getColumn(i);
        if (value != null) {
            columnValues.add(objectToString(value));
        } else {
            columnValues.add("null");
        }
    }
    return columnValues.toString();
}

8.6 debug

#debug(String text, boolean input) 方法,代码如下:

// ResultSetLogger.java

@Override
protected void debug(String text, boolean input) {
    if (statementLog.isDebugEnabled()) {
        statementLog.debug(prefix(input) + text);
    }
}

@Override
protected void trace(String text, boolean input) {
    if (statementLog.isTraceEnabled()) {
        statementLog.trace(prefix(input) + text);
    }
}

9. 其他

logging 包中,还有 commonslog4jlog4j2slf4jstdoutjdk 包,分别对应 org.apache.ibatis.logging.Log 的不同实现类。当然,jdbc 包,也对应 org.apache.ibatis.logging.Log 的不同实现类。所以,整体类图如下:

类图

另外,还有 org.apache.ibatis.logging.LogException ,是 MyBatis 封装的日志异常。代码如下:

// LogException.java

public class LogException extends RuntimeException {

    private static final long serialVersionUID = 1022924004852350942L;

    public LogException() {
        super();
    }

    public LogException(String message) {
        super(message);
    }

    public LogException(String message, Throwable cause) {
        super(message, cause);
    }

    public LogException(Throwable cause) {
        super(cause);
    }

}

666. 彩蛋

比较简单的一篇文章,主要是对 Log 的封装,以及 JDBC 的日志打印。

参考和推荐如下文章:

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐