持续创作,加速成长!这是我参与「日新计划 6 月更文挑战」的第16天,点击查看活动详情

使用拦截器模式重构Log框架

本来很早就像做这一篇文http 404章,但是由于重构该功能需要很多前置技能。该文章涉及到一下知识点。所以在此篇文章之前,我先把前置技能点亮。

我们得先接口和抽象类的区别知道拦截器模式如何定义。

实战使用简单Demo通过拦截模式来展示弹窗。

我们还需要学会Kotlin高阶扩展函数的使用。

我们得知道Android文件路径的管理。

知道指定了路径之https和http的区别后我们通过IO流操线程作文件。

可惜我认为很重要的知识点,平台没给推荐,Anyway,在此基础上我们jvm垃圾回收机制就能重构一个拦截器模式的日志打印保存的框架。

一、重构前的痛点

首先我们之前的Log框架,只能打印日志,美化日志的功线程撕裂者能和打印功能耦合,没有保存日志信息的能力,无jvm垃圾回收机制法保存指定的日志到文件,无法上传给服务端,做精准的Log分析。

开箱即用-Android设计模式实战-拦截实现Log的打印与保存

熟悉我的观众都知道,我的YYLogUtils打印的http 302日志如上图,功能都是耦合在一起的,只能暴力的开关选项,没有其他的配置。

改版之后会将不同的功能模块隔离。使用拦截接口器模式按需加载,每一个拦截器配置单独的开关。实现功能的引擎化,可替代jvm原理化。

二、拦截器的分类与使用

在之前的设计模式中有讲到,其实拦截器模式分为http协议简单拦截和拦截转发两种。

2.1 使用哪一种拦截器模式

如大名鼎鼎的OkHttp中的拦截器就是拦截转发的一种,它可以把上一个拦截器处理过的数据,转发到下一个拦截器中。那么通过这线程池面试题种值传递的方式,就可以得到处理过的数jvm调优据。

而我之前的文章,通过拦截模式来展示弹窗。http 404那是普通的拦截器,并没有拦截转发的功能,是我不想这么用吗?不是,是没有必要,业务场景的需求如此,我们的变量控制是由接口返回,是由外部控制的,不需要传递值。

那么如果设计一个Log的打印保http代理存框架,我们java编译器如果使用拦截器模式来实现,我们应该使用哪一种拦截器模式?

比如我们定义三个拦截实现,一个是Log美化 一个是线程的几种状态Log打印 一个是Log保存,那么Log美化之后的结果需要传递给 Log打印拦截服http 302务来接口和抽象类的区别打印 ,然后传递给Log保存拦截服务来保存到本地。所以我们需要接口文档拦截转发的那一种拦截器模式。

2.2 拦截转发方式的定义

如何定义一个拦截转发的拦截器?设计模http 500java编译器中有讲到过。

1.拦截器请求接口

public abstract class InterceptChain<T> {
    public InterceptChain next;
    public void intercept(T data) {
        if (next != null) {
            next.intercept(data);
        }
    }
}

2.拦截器处理器

public class InterceptChainHandler<T> {
    InterceptChain _interceptFirst;
    public void add(InterceptChain interceptChain) {
        if (_interceptFirst == null) {
            _interceptFirst = interceptChain;
            return;
        }
        InterceptChain node = _interceptFirst;
        while (true) {
            if (node.next == null) {
                node.next = interceptChain;
                break;
            }
            node = node.next;
        }
    }
    public void intercept(T data) {
        _interceptFirst.intercept(data);
    }
}

可以看到之前的拦截器是通过一个内置泛型对象接口是什么当做值来传递的,我们的Log框架https和http的区别其实不需要这么复杂,无需这么复杂,我们直接把对象展平,当做参数传递是一样的。我们需要的参数为日志级http 500接口测试priority 日志Tag 与日志的消息message。(当然我们把三个参数封装为一个对象使用上面的泛型传递也是一样样的)

修改之后我们Log框架的真正拦截器定义如下:

1.日志拦截http 404器的定义

abstract class LogInterceptChain {
    var next: LogInterceptChain? = null
    open fun intercept(priority: Int, tag: String, logMsg: String?) {
        next?.intercept(priority, tag, logMsg)
    }
}

2.日志拦截器的处理

class LogInterceptChainHandler {
    private var _interceptFirst: LogInterceptChain? = null
    fun add(interceptChain: LogInterceptChain?) {
        if (_interceptFirst == null) {
            _interceptFirst = interceptChain
            return
        }
        var node: LogInterceptChain? = _interceptFirst
        while (true) {
            if (node?.next == null) {
                node?.next = interceptChain
                break
            }
            node = node.next
        }
    }
    fun intercept(priority: Int, tag: String, logMsg: String?) {
        _interceptFirst?.intercept(priority, tag, logMsg)
    }
}

然后javascript我们就能管理拦截器来打印我们的日志

object YYLogUtils {
    var LINE_SEPARATOR = System.getProperty("line.separator")
    private const val DEBUG = 3
    private const val INFO = 4
    private const val WARN = 5
    private const val ERROR = 6
    val intercepts = LogInterceptChainHandler()
    @JvmStatic
    fun d(message: String, tag: String = "/", vararg args: Any) {
        log(DEBUG, message, tag, *args)
    }
    @JvmStatic
    fun d(message: String) {
        d(message, "/")
    }
    @JvmStatic
    fun e(message: String, tag: String = "/", vararg args: Any, throwable: Throwable? = null) {
        log(ERROR, message, tag, *args, throwable = throwable)
    }
    @JvmStatic
    fun e(message: String) {
        e(message, "/")
    }
    @JvmStatic
    fun w(message: String, tag: String = "/", vararg args: Any) {
        log(WARN, message, tag, *args)
    }
    @JvmStatic
    fun w(message: String) {
        w(message, "/")
    }
    @JvmStatic
    fun i(message: String, tag: String = "/", vararg args: Any) {
        log(INFO, message, tag, *args)
    }
    @JvmStatic
    fun i(message: String) {
        i(message, "/")
    }
    @JvmStatic
    fun json(json: String) {
        if (TextUtils.isEmpty(json)) {
            e("json 数据为空!")
            return
        }
        try {
            var message = ""
            if (json.startsWith("{")) {
                val jo = JSONObject(json)
                message = jo.toString(4)
            } else if (json.startsWith("[")) {
                val ja = JSONArray(json)
                message = ja.toString(4)
            }
            e(message)
        } catch (e: Exception) {
            e(e.cause!!.message + LINE_SEPARATOR + json)
        }
    }
    @JvmStatic
    fun xml(xml: String) {
        if (TextUtils.isEmpty(xml)) {
            e("xml 数据为空!")
            return
        }
        try {
            val xmlInput: Source = StreamSource(StringReader(xml))
            val xmlOutput = StreamResult(StringWriter())
            val transformer = TransformerFactory.newInstance().newTransformer()
            transformer.setOutputProperty(OutputKeys.INDENT, "yes")
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
            transformer.transform(xmlInput, xmlOutput)
            val message =
                xmlOutput.writer.toString().replaceFirst(">".toRegex(), ">$LINE_SEPARATOR")
            e(message)
        } catch (e: TransformerException) {
            e(e.cause!!.message + LINE_SEPARATOR + xml)
        }
    }
    fun addInterceptor(interceptor: LogInterceptChain) {
        intercepts.add(interceptor)
    }
    @Synchronized
    private fun log(
        priority: Int,
        message: String,
        tag: String,
        vararg args: Any,
        throwable: Throwable? = null
    ) {
        var logMessage = message.format(*args)
        if (throwable != null) {
            logMessage += getStackTraceString(throwable)
        }
        intercepts.intercept(priority, tag, logMessage)
    }
    fun String.format(vararg args: Any) =
        if (args.isNullOrEmpty()) this else String.format(this, *args)
    private fun getStackTraceString(tr: Throwable?): String {
        if (tr == null) {
            return ""
        }
        var t = tr
        while (t != null) {
            if (t is UnknownHostException) {
                return ""
            }
            t = t.cause
        }
        val sw = StringWriter()
        val pw = PrintWriter(sw)
        tr.printStackTrace(pw)
        pw.flush()
        return sw.toString()
    }
}

三、具体拦截器的实现

上面定义了拦截器之后,我们需要定义几个具体的拦截器实现,来线程和进程的区别是什么实现Log的拦截与功能的实现。

之前有分析,我们常用的三个拦截器分别为Log日志美化,Log日志打印,与Log日志线程池的七个参数的保存。下面看看我们分别如何实现。

3.1 日志美化

我们常用的日志美化就是把日志信息做一层封装,打印出当前的线程,打印日志的堆栈和方法调用层数,以及具体的日志,这里我线程池面试题就把之前的Log框架美化功能Copyg过来,实现和之前java模拟器一模一样的美化。

/**
 * 对Log进行装饰,美化打印效果
 * 打印Log之前 封装打印的线程-打印的位置
 * 具体的打印输出由其他拦截器负责
 */
class LogDecorateInterceptor(private val isEnable: Boolean) : LogInterceptChain() {
    companion object {
        private const val TOP_LEFT_CORNER = '┏'
        private const val BOTTOM_LEFT_CORNER = '┗'
        private const val MIDDLE_CORNER = '┠'
        private const val LEFT_BORDER = '┃'
        private const val DOUBLE_DIVIDER = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
        private const val SINGLE_DIVIDER = "──────────────────────────────────────────────────────"
        private const val TOP_BORDER = TOP_LEFT_CORNER.toString() + DOUBLE_DIVIDER
        private const val BOTTOM_BORDER = BOTTOM_LEFT_CORNER.toString() + DOUBLE_DIVIDER
        private const val MIDDLE_BORDER = MIDDLE_CORNER.toString() + SINGLE_DIVIDER
        private val blackList = listOf(
            LogDecorateInterceptor::class.java.name,
            YYLogUtils::class.java.name,
            LogPrintInterceptor::class.java.name,
            Log2FileInterceptor::class.java.name,
            LogInterceptChain::class.java.name,
            LogInterceptChainHandler::class.java.name,
        )
    }
    override fun intercept(priority: Int, tag: String, logMsg: String?) {
        if (isEnable) {
            super.intercept(priority, tag, TOP_BORDER)
            super.intercept(priority, tag, "$LEFT_BORDER [Thread] → " + Thread.currentThread().name)
            super.intercept(priority, tag, MIDDLE_BORDER)
            printStackInfo(priority, tag)
            super.intercept(priority, tag, MIDDLE_BORDER)
            super.intercept(priority, tag, "$LEFT_BORDER $logMsg")
            super.intercept(priority, tag, BOTTOM_BORDER)
        }else{
            super.intercept(priority, tag, logMsg)
        }
    }
    //获取调用栈信息
    private fun printStackInfo(priority: Int, tag: String) {
        var str = ""
        var line = 0
        val traces = Thread.currentThread().stackTrace.drop(3)
        for (i in traces.indices) {
            if (line >= 3) return   //这里只打印三行
            val element = traces[i]
            val perTrace = java.lang.StringBuilder(str)
            if (element.isNativeMethod) {
                continue
            }
            val className = element.className
            if (className.startsWith("android.")
                || className.contains("com.android")
                || className.contains("java.lang")
                || className.contains("com.youth.xframe")
                || className in blackList
            ) {
                continue
            }
            perTrace.append(element.className)
                .append('.')
                .append(element.methodName)
                .append("  (")
                .append(element.fileName)
                .append(':')
                .append(element.lineNumber)
                .append(")")
            str += "  "
            line++
            //打印日志
            next?.intercept(priority, tag, "$LEFT_BORDER $perTrace")
        }
    }
}
3.2 日志的打印

实现了日志的打印之后,我们就能把美化过的日志打印出来,这里的拦截器是值传递,所以我们可以把美化过的日志直接打印

/**
 * 使用Android Log 打印日志
 */
open class LogPrintInterceptor(private val isEnable: Boolean) : LogInterceptChain() {
    override fun intercept(priority: Int, tag: String, logMsg: String?) {
        if (isEnable) {
            Log.println(priority, tag, logMsg ?: "-")
        }
        super.intercept(priority, tag, logMsg)
    }
}

这里我jvm内存模型们使用Androhttp协议idhttp代理原生的日志打印,当然你如果想JVM用System的print服务来打印可以的,这只是一个引擎类,具体的实现,可以由我们自己来定,使用第三方的框架来打印日志都行的。

3.3 日志的保存

线程里就涉及到文件路径的管理 与 IO流操作文件中讲到过通过Okio持续写入文件的优化。

而我们需要协程异步的处理线程数越多越好吗,我们在动画的处理中有讲到 Handler线程Thread 子线程处理任务。

基于以上的基础我们实现日志的保存拦截器如下:

/**
 * 接收上面拦截器传递的数据-把Log信息保存到本地File
 */
class Log2FileInterceptor private constructor(
    private val dir: String, private val isEnable: Boolean
) : LogInterceptChain() {
    private val handlerThread = HandlerThread("log_to_file_thread")
    private val handler: Handler
    private var startTime = System.currentTimeMillis()
    private var bufferedSink: BufferedSink? = null
    private var logFile = File(getFileName())
    val callback = Handler.Callback { message ->
        val sink = checkSink()
        when (message.what) {
            TYPE_FLUSH -> {
                sink.use {
                    it.flush()
                    bufferedSink = null
                }
            }
            TYPE_LOG -> {
                val log = message.obj as String
                sink.writeUtf8(log)
                sink.writeUtf8("n")
            }
        }
        false
    }
    companion object {
        private const val TYPE_FLUSH = -1
        private const val TYPE_LOG = 1
        private const val FLUSH_LOG_DELAY_MILLIS = 3000L
        @Volatile
        private var INSTANCE: Log2FileInterceptor? = null
        fun getInstance(dir: String, isEnable: Boolean): Log2FileInterceptor = INSTANCE ?: synchronized(this) {
            INSTANCE ?: Log2FileInterceptor(dir, isEnable).apply { INSTANCE = this }
        }
    }
    init {
        handlerThread.start()
        handler = Handler(handlerThread.looper, callback)
    }
    override fun intercept(priority: Int, tag: String, logMsg: String?) {
        if (isEnable) {
            if (!handlerThread.isAlive) handlerThread.start()
            handler.run {
                removeMessages(TYPE_FLUSH)
                obtainMessage(TYPE_LOG, "[$tag] $logMsg").sendToTarget()
                val flushMessage = handler.obtainMessage(TYPE_FLUSH)
                sendMessageDelayed(flushMessage, FLUSH_LOG_DELAY_MILLIS)
            }
        }
        super.intercept(priority, tag, logMsg)
    }
    @SuppressLint("SimpleDateFormat")
    private fun getToday(): String =
        SimpleDateFormat("yyyy-MM-dd").format(Calendar.getInstance().time)
    private fun getFileName() = "$dir${File.separator}${getToday()}.log"
    private fun checkSink(): BufferedSink {
        if (bufferedSink == null) {
            bufferedSink = logFile.appendingSink().gzip().buffer()
        }
        return bufferedSink!!
    }
}

代码不多,相信大家都能看懂,如有不懂可以看之前的文章进阶。唯一需要注意的一点是使用了Okio的gzip函数,封装为压缩格式,是为了省云服务器的空间。

JVM、日志的拦截使用与控制

    YYLogUtils.addInterceptor(LogDecorateInterceptor(true))
    YYLogUtils.addInterceptor(LogPrintInterceptor(true))
    val logPath = if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED)
    application.applicationContext.getExternalFilesDir("log")?.absolutePath?: application.applicationContext.filesDir.absolutePath + "/log/"
        else
         application.applicationContext.filesDir.absolutePath + "/log/"
            val dir = File(logPath)
            if (!dir.exists()) {
                dir.mkdirs()
            }
            YYLogUtils.addInterceptor(Log2FileInterceptor. getInstance(logPath, true))

我们按顺序添加好拦截器即可实现日志的打印。

打印日志如下:

开箱即用-Android设计模式实战-拦截实现Log的打印与保存

Log文件如下:

开箱即用-Android设计模式实战-拦截实现Log的打印与保存

由于是gzip格式,我们需要解压并查看,日志文件信息如下:

开箱即用-Android设计模式实战-拦截实现Log的打印与保存

有人说和之前一样啊,没什么区别,是的,但是更加灵活了,比如不想要保http协议持到文件,我们接口是什么可以禁用Log保存的拦截器,比如要上线了,不想要打印出来,线程池只想保存到文件上传到服务器,则直接把Log打印的拦截器禁用,比如不想要美化太多多余的字符串,我们也可jvm垃圾回收机制以把美化javaeeLog的拦截器禁用掉。

例如我们去掉美化,

  YYLogUtils.addInterceptor(LogDecorateInterceptor(false))
  YYLogUtils.addInterceptor(LogPrintInterceptor(true))
  ...

打印日志如下

文件Log:

开箱即用-Android设计模式实战-拦截实现Log的打印与保存

解压之后展示Log文件:

开箱即用-Android设计模式实战-拦截实现Log的打印与保存

是不是更加的灵活了!

总结

我们可以通过自有的拦截器实现基本的功能实现,同时我们还支持自定义java语言的拦截器,通过添加自定义的拦截器实现其他的功能,非常的方便与灵活。

很多人都使用的第三方的Log框架,接口和抽象类的区别其实Androijvm垃圾回收机制d自带的Log打印就挺好用,我们使用自行的封装更加方便的管控,有什么自定义的需求也可以添加自定义拦截器来实现,java怎么读没必要什么功能接口crc错误计数都上框架的。

如大家有需求,源码在此!

好了,如果有更好的方案还请评论区交流,如有错漏还请指出!线程数越多越好吗

如果觉得本文不错还请点赞支持。

到此完结!

开箱即用-Android设计模式实战-拦截实现Log的打印与保存