中国e网网站建设,南京建设网站多少钱,做网站游戏需要什么,wordpress顶部是什么Android移动端集成#xff1a;开发调用MogFace云服务的人脸检测App 你是不是也想过#xff0c;给自己的手机App加上人脸识别功能#xff1f;比如做个美颜相机、智能相册#xff0c;或者需要身份验证的应用。从头训练一个人脸检测模型#xff0c;对移动端开发者来说#x…Android移动端集成开发调用MogFace云服务的人脸检测App你是不是也想过给自己的手机App加上人脸识别功能比如做个美颜相机、智能相册或者需要身份验证的应用。从头训练一个人脸检测模型对移动端开发者来说门槛高、周期长而且模型优化和部署也是个大麻烦。现在有个更简单的办法把复杂的模型推理工作交给云端。MogFace是一个高性能的人脸检测模型我们可以把它部署在星图平台上变成一个随时可以调用的WebAPI服务。你的Android应用只需要负责拍照、上传图片、接收结果和展示核心的检测逻辑全在云端完成。这篇文章我就带你一步步实现一个Android应用它能调用部署好的MogFace服务完成拍照或选图、上传检测、并实时绘制出人脸框和关键点。整个过程你会接触到网络请求、图片处理、UI渲染这些移动端开发的核心技能但完全不用担心模型本身。让我们开始吧。1. 项目准备与环境搭建在动手写代码之前我们需要把“舞台”搭好。这包括创建一个Android项目以及准备好我们要调用的云端服务。1.1 创建Android项目与权限配置首先打开Android Studio新建一个项目。选择“Empty Views Activity”模板就行语言选Kotlin当然用Java也可以本文以Kotlin为例最低API级别建议选API 24Android 7.0或更高以兼容大多数现代特性。项目创建好后我们需要在AndroidManifest.xml文件中声明必要的权限。我们这个应用需要访问摄像头拍照也需要读取相册来选图。?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:toolshttp://schemas.android.com/tools !-- 网络权限用于调用API -- uses-permission android:nameandroid.permission.INTERNET / !-- 写入外部存储权限用于保存拍照的图片可选 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / !-- 在Android 10及以上作用域存储使该权限对媒体文件无效 -- !-- 相机权限 -- uses-permission android:nameandroid.permission.CAMERA / !-- 在Android 6.0上部分权限需要运行时申请 -- uses-feature android:nameandroid.hardware.camera android:requiredtrue / application ... activity ... ... /activity /application /manifest注意从Android 10API 29开始作用域存储改变了文件访问方式。对于拍照我们更推荐使用MediaStore API这样可能不需要WRITE_EXTERNAL_STORAGE权限。为了简化我们先按传统方式声明。1.2 引入必要的第三方库为了简化开发我们会使用一些优秀的第三方库。在项目根目录的build.gradle.kts(或build.gradle) 文件中确保已经添加了Google的Maven仓库。然后打开app模块下的build.gradle.kts(或build.gradle)在dependencies块中添加以下依赖dependencies { // Android核心库 implementation(androidx.core:core-ktx:1.12.0) implementation(androidx.appcompat:appcompat:1.6.1) implementation(com.google.android.material:material:1.11.0) implementation(androidx.constraintlayout:constraintlayout:2.1.4) // 网络请求Retrofit Gson implementation(com.squareup.retrofit2:retrofit:2.9.0) implementation(com.squareup.retrofit2:converter-gson:2.9.0) // 图片加载与显示Glide implementation(com.github.bumptech.glide:glide:4.16.0) // 权限请求简化运行时权限申请 implementation(com.guolindev.permissionx:permissionx:1.7.1) // 日志打印方便调试 implementation(com.jakewharton.timber:timber:5.0.1) // 单元测试 testImplementation(junit:junit:4.13.2) androidTestImplementation(androidx.test.ext:junit:1.1.5) androidTestImplementation(androidx.test.espresso:espresso-core:3.5.1) }添加后点击Sync Now同步项目。这些库能帮我们省去大量重复劳动Retrofit负责优雅地处理网络请求Glide让图片加载变得轻而易举PermissionX则让繁琐的权限申请流程变得清晰简单。1.3 确认MogFace WebAPI服务信息这是最关键的一步。你需要已经有一个部署在星图平台上的MogFace WebAPI服务。通常这个服务会提供一个访问地址URL和可能需要用到的认证信息如API Key。假设你的服务地址是https://your-mogface-service.example.com/predict请求方式通常是POST接收一张图片multipart/form-data格式返回一个包含人脸框和关键点坐标的JSON数据。为了在代码中方便管理我们在项目中创建一个单例对象来存放这些配置// 新建一个文件ApiConfig.kt object ApiConfig { // TODO: 替换成你实际的MogFace服务地址 const val BASE_URL https://your-mogface-service.example.com/ // 如果你的服务需要API Key可以在这里配置 const val API_KEY_HEADER X-API-Key const val API_KEY_VALUE your-api-key-here // 请妥善保管不要硬编码在正式应用中 }安全提示在实际生产环境中API Key等敏感信息绝对不应该直接硬编码在客户端代码中。更安全的做法是通过你自己的后端服务器进行中转或者使用移动端安全存储方案。这里为了演示流程我们先这样处理。2. 核心功能实现网络请求与图片处理基础打好了现在我们来构建应用的核心——如何把图片发送给云端并理解它返回的结果。2.1 定义数据模型与API接口首先我们需要定义和云端API对话的“语言”。根据MogFace服务的响应格式定义对应的数据类Data Class。假设返回的JSON结构如下{ code: 0, message: success, data: { faces: [ { bbox: [x1, y1, x2, y2], // 人脸框左上角和右下角坐标 score: 0.99, // 置信度 landmarks: [ // 关键点可能是5点或更多 [x1, y1], // 左眼 [x2, y2], // 右眼 [x3, y3], // 鼻子 [x4, y4], // 左嘴角 [x5, y5] // 右嘴角 ] } // ... 可能有多个人脸 ] } }我们在Kotlin中这样定义// 新建文件model/DetectionResponse.kt data class DetectionResponse( val code: Int, val message: String, val data: DetectionData? ) data class DetectionData( val faces: ListFace? ) data class Face( val bbox: ListFloat, // 或者可能是ListDouble根据实际API调整 val score: Float, val landmarks: ListListFloat? )接下来使用Retrofit定义网络请求接口。我们创建一个MogFaceService接口。// 新建文件network/MogFaceService.kt import okhttp3.MultipartBody import retrofit2.Call import retrofit2.http.* interface MogFaceService { /** * 上传图片进行人脸检测 * param imagePart 图片文件使用Multipart格式上传 */ Multipart POST(predict) // 对应API路径 fun detectFace( Part imagePart: MultipartBody.Part // 如果需要API Key可以添加Header // Header(ApiConfig.API_KEY_HEADER) apiKey: String ApiConfig.API_KEY_VALUE ): CallDetectionResponse }2.2 构建网络请求客户端有了接口定义我们需要创建Retrofit的实例。我们通常用一个单例的RetrofitClient来管理。// 新建文件network/RetrofitClient.kt import com.google.gson.GsonBuilder import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit object RetrofitClient { private const val TIMEOUT 30L // 超时时间30秒 val mogFaceService: MogFaceService by lazy { // 1. 创建OkHttpClient可以添加日志拦截器方便调试 val loggingInterceptor HttpLoggingInterceptor().apply { level HttpLoggingInterceptor.Level.BODY // 在调试时打印请求和响应日志 } val okHttpClient OkHttpClient.Builder() .connectTimeout(TIMEOUT, TimeUnit.SECONDS) .readTimeout(TIMEOUT, TimeUnit.SECONDS) .writeTimeout(TIMEOUT, TimeUnit.SECONDS) .addInterceptor(loggingInterceptor) // 可以在这里添加统一的Header比如API Key // .addInterceptor { chain - // val request chain.request().newBuilder() // .addHeader(ApiConfig.API_KEY_HEADER, ApiConfig.API_KEY_VALUE) // .build() // chain.proceed(request) // } .build() // 2. 创建Retrofit实例 val retrofit Retrofit.Builder() .baseUrl(ApiConfig.BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() // 3. 创建Service实例 retrofit.create(MogFaceService::class.java) } }这样我们在任何地方都可以通过RetrofitClient.mogFaceService来调用API了。2.3 实现图片选择与上传逻辑现在我们需要在Activity或Fragment中实现图片选择拍照或从相册选择和上传的逻辑。这里会涉及到权限申请和ActivityResult API的使用。首先设计一个简单的布局activity_main.xml?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp Button android:idid/btn_take_photo android:layout_widthmatch_parent android:layout_heightwrap_content android:text拍照 / Button android:idid/btn_pick_image android:layout_widthmatch_parent android:layout_heightwrap_content android:text从相册选择 android:layout_marginTop8dp/ ImageView android:idid/iv_preview android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:layout_marginTop16dp android:scaleTypefitCenter android:background#f0f0f0 android:contentDescription图片预览区域 / Button android:idid/btn_detect android:layout_widthmatch_parent android:layout_heightwrap_content android:text开始检测 android:layout_marginTop16dp android:enabledfalse/ ProgressBar android:idid/progress_bar android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter android:layout_marginTop16dp android:visibilitygone / TextView android:idid/tv_result android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop8dp android:text检测结果将显示在这里 android:textSize14sp / /LinearLayout然后在MainActivity.kt中实现核心逻辑// MainActivity.kt import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle import android.provider.MediaStore import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import com.bumptech.glide.Glide import com.guolindev.permissionx.PermissionX import kotlinx.android.synthetic.main.activity_main.* import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody import retrofit2.Call import retrofit2.Callback import retrofit2.Response import timber.log.Timber import java.io.File import java.io.FileOutputStream class MainActivity : AppCompatActivity() { private var currentImageUri: Uri? null // 当前选中图片的Uri private var currentImagePath: String? null // 当前图片的本地路径 // 用于保存拍照生成的临时文件 private lateinit var tempPhotoFile: File companion object { private const val REQUEST_TAKE_PHOTO 1 private const val REQUEST_PICK_IMAGE 2 } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 初始化Timber日志 if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } // 初始化临时文件 tempPhotoFile File(externalCacheDir, temp_photo.jpg) btn_take_photo.setOnClickListener { requestCameraPermission() } btn_pick_image.setOnClickListener { requestStoragePermission() } btn_detect.setOnClickListener { currentImagePath?.let { path - uploadImageForDetection(File(path)) } } } private fun requestCameraPermission() { PermissionX.init(this) .permissions(android.Manifest.permission.CAMERA) .onExplainRequestReason { scope, deniedList - // 向用户解释为什么需要权限 scope.showRequestReasonDialog(deniedList, 需要相机权限来拍照, 确定, 取消) } .request { allGranted, _, _ - if (allGranted) { takePhoto() } else { Timber.e(相机权限被拒绝) } } } private fun requestStoragePermission() { // Android 13 使用READ_MEDIA_IMAGES之前版本用READ_EXTERNAL_STORAGE val permission if (android.os.Build.VERSION.SDK_INT android.os.Build.VERSION_CODES.TIRAMISU) { android.Manifest.permission.READ_MEDIA_IMAGES } else { android.Manifest.permission.READ_EXTERNAL_STORAGE } PermissionX.init(this) .permissions(permission) .request { allGranted, _, _ - if (allGranted) { pickImageFromGallery() } else { Timber.e(存储权限被拒绝) } } } private fun takePhoto() { // 确保目录存在 tempPhotoFile.parentFile?.mkdirs() // 获取文件的Uri使用FileProvider避免FileUriExposedException val photoUri FileProvider.getUriForFile( this, ${packageName}.fileprovider, // 需要在Manifest中配置FileProvider tempPhotoFile ) val takePictureIntent Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, photoUri) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) } // 启动相机Activity startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO) } private fun pickImageFromGallery() { val intent Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply { type image/* } startActivityForResult(intent, REQUEST_PICK_IMAGE) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode ! RESULT_OK) return when (requestCode) { REQUEST_TAKE_PHOTO - { // 拍照返回图片已保存在tempPhotoFile中 currentImagePath tempPhotoFile.absolutePath currentImageUri Uri.fromFile(tempPhotoFile) displaySelectedImage(currentImageUri) btn_detect.isEnabled true } REQUEST_PICK_IMAGE - { // 从相册选择返回 data?.data?.let { uri - currentImageUri uri // 将Uri转换为文件路径简化处理实际生产环境需考虑更多情况 val path getRealPathFromUri(uri) currentImagePath path displaySelectedImage(uri) btn_detect.isEnabled true } } } } private fun displaySelectedImage(uri: Uri?) { uri?.let { Glide.with(this) .load(it) .into(iv_preview) } } // 一个简单的方法从Uri获取路径仅用于演示生产环境需更健壮的处理 private fun getRealPathFromUri(uri: Uri): String? { val projection arrayOf(MediaStore.Images.Media.DATA) contentResolver.query(uri, projection, null, null, null)?.use { cursor - val columnIndex cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) cursor.moveToFirst() return cursor.getString(columnIndex) } return null } // 核心方法上传图片进行检测 private fun uploadImageForDetection(imageFile: File) { progress_bar.visibility android.view.View.VISIBLE btn_detect.isEnabled false tv_result.text 检测中... // 1. 创建Multipart请求体 val requestFile imageFile.asRequestBody(image/jpeg.toMediaTypeOrNull()) val imagePart MultipartBody.Part.createFormData(image, imageFile.name, requestFile) // 2. 发起网络请求 val call RetrofitClient.mogFaceService.detectFace(imagePart) call.enqueue(object : CallbackDetectionResponse { override fun onResponse(call: CallDetectionResponse, response: ResponseDetectionResponse) { progress_bar.visibility android.view# 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、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) { if (logConstructor null) { try { runnable.run(); } catch (Throwable t) { // ignore } } }当logConstructor为空时执行runnable的方法。#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.); } // 初始化成功使用 candidate 构造方法 logConstructor candidate; } catch (Throwable t) { throw new LogException(Error setting Log implementation. Cause: t, t); } }在该方法中会创建指定implClass对应的 Log 对象若创建成功则意味着初始化logConstructor成功否则抛出 LogException 异常。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); } }通过logConstructor反射创建 Log 对象。2.3 小结因为日志框架众多所以 MyBatis 定义了自己的 Log 接口用于统一。而 LogFactory 工厂负责创建对应的 Log 对象。整个过程就是典型的生产者消费者模式。3. Logorg.apache.ibatis.logging.LogMyBatis 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 Log 的实现类Log 的实现类如下图每个实现类对应一种日志框架。以 Log4j2 举例子代码如下// Log4j2Impl.java public class Log4j2Impl implements Log { private final Log log; public Log4j2Impl(String clazz) { Logger logger LogManager.getLogger(clazz); if (logger instanceof AbstractLogger) { log new Log4j2AbstractLoggerImpl((AbstractLogger) logger); } else { log new Log4j2LoggerImpl(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); } }在构造方法中我们可以看到使用的org.apache.logging.log4j.Logger类也就是说使用 Log4j2 框架。其它实现类就不详细解析胖友可以自己看看。4. ConnectionLoggerorg.apache.ibatis.logging.jdbc.ConnectionLogger继承 BaseJdbcLogger 抽象类支持打印 Connection 的日志。即在调用java.sql.Connection的方法时打印对应调用的方法和参数以及耗时。4.1 构造方法// ConnectionLogger.java /** * Connection 对象 */ private final Connection connection; private ConnectionLogger(Connection conn, Log statementLog, int queryStack) { super(statementLog, queryStack); this.connection conn; }connection属性Connection 对象。4.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); }通过动态代理的方式创建 Connection 的代理对象。这样在调用 Connection 的方法时可以进行拦截进行日志的打印。4.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); } // 执行方法 if (prepareStatement.equals(method.getName())) { // 如果是调用 prepareStatement 方法则打印日志 if (isDebugEnabled()) { debug( Preparing: removeBreakingWhitespace((String) params[0]), true); } // 执行方法 PreparedStatement stmt (PreparedStatement) method.invoke(connection, params); // 创建 PreparedStatement 的代理对象 stmt PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } else if (prepareCall.equals(method.getName())) { // 如果是调用 prepareCall 方法则打印日志 if (isDebugEnabled()) { debug( Preparing: removeBreakingWhitespace((String) params[0]), true); } // 执行方法 PreparedStatement stmt (PreparedStatement) method.invoke(connection, params); // 创建 PreparedStatement 的代理对象 stmt PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } else if (createStatement.equals(method.getName())) { // 如果是调用 createStatement 方法则打印日志 // 执行方法 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); } }根据不同的方法进行不同的拦截处理。当然所有方法最终都会调用connection对应的方法。对于prepareStatement、prepareCall、createStatement方法会打印日志并且创建对应的 Statement 的代理对象。这样该 Statement 的代理对象后续的执行也会打印日志。对于其它方法不打印日志直接调用。另外#debug(String text, boolean input)方法代码如下// BaseJdbcLogger.java protected void debug(String text, boolean input) { // 判断是否开启 debug 日志级别 if (statementLog.isDebugEnabled()) { // 打印日志 statementLog.debug(prefix(text, input)); } } private String prefix(String text, boolean input) { // 拼接前缀 StringBuilder buf new StringBuilder(); // 需要的情况下拼接 [queryStack] 前缀。例如[1] for (int i 0; i queryStack; i) { buf.append( ); } // 拼接 input 前缀 if (input) { buf.append( ); } else { buf.append( ); } buf.append(text); return buf.toString(); }通过这样的方式打印出来的日志非常的美观。例如[2019-03-30 14:32:34,591][DEBUG][main][ Preparing: SELECT id, username, password FROM users WHERE id ?] [2019-03-30 14:32:34,648][DEBUG][main][ Parameters: 1(Integer)] [2019-03-30 14:32:34,678][DEBUG][main][ Total: 1]胖友可以看看org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl类打印的日志就是上述效果的。5. BaseJdbcLoggerorg.apache.ibatis.logging.jdbc.BaseJdbcLogger实现 Log 接口Jdbc 日志抽象基类。5.1 构造方法// BaseJdbcLogger.java /** * Log 对象用于打印日志 */ protected Log statementLog; /** * 查询的层数 */ protected int queryStack; public BaseJdbcLogger(Log statementLog, int queryStack) { this.statementLog statementLog; this.queryStack queryStack; }5.2 其它方法BaseJdbcLogger 还有其它方法比较简单胖友自己看看。6. StatementLoggerorg.apache.ibatis.logging.jdbc.StatementLogger继承 BaseJdbcLogger 抽象类支持打印 Statement 的日志。即在调用java.sql.Statement的方法时打印对应调用的方法和参数以及耗时。因为和 ConnectionLogger 比较类似所以本文就不详细解析了。胖友可以自己看看。7. PreparedStatementLoggerorg.apache.ibatis.logging.jdbc.PreparedStatementLogger继承 BaseJdbcLogger 抽象类支持打印 PreparedStatement 的日志。即在调用java.sql.PreparedStatement的方法时打印对应调用的方法和参数以及耗时。因为和 ConnectionLogger 比较类似所以本文就不详细解析了。胖友可以自己看看。8. ResultSetLoggerorg.apache.ibatis.logging.jdbc.ResultSetLogger继承 BaseJdbcLogger 抽象类支持打印 ResultSet 的日志。即在调用java.sql.ResultSet的方法时打印对应调用的方法和参数以及耗时。因为和 ConnectionLogger 比较类似所以本文就不详细解析了。胖友可以自己看看。9. 小结总的来说logging包相对简单胖友可以自己调试调试。