利用 ThreadLocal + Lambda,实现有状态变量的单例模式

通常情况下单例模式的对象应该具有状态,然而现实是复杂的,总会有那么一些特殊情况下需要小小地【违例】一下。

动机

一个父类的方法执行前需要设置一个变量的值,变量值会对方法的执行结果产生影响。现希望子类以单例的方式继承父类。

以我实际遇到的一个问题为例,JOOQ 是一个 ORM 类库,这个类库能够自动扫描数据库并生成 DAO,但是自动生成的 DAO 功能有限,通常需要继承来扩展这些 DAO。每个 DAO 在实例化时需要传入一个 Configuration,这个 Configuration 包含有关数据库的信息。

通常情况下 Configuration 可以由所有的 DAO 共享,然而,在启动事务后 Configuration 会被 JOOQ 派生,且后续所有的事务内数据库操作都应该使用派生的 Configuration

现希望子 DAO 为单例,同时,在不改变父 DAO 的情况下,且不修改子 DAO 的已有方法和对这些方法的调用的情况下,实现 configuration 变量的 “智能” 修改。

我们可以利用 ThreadLocal + Lambda 解决这个问题。

基本概念

ThreadLocal

ThreadLocal<E> 是一个容器,它内部采用 Map 实现,以线程的某种唯一特征为键,用户自定义类型 E 为值。因此不同线程存取同一个 ThreadLocal 容器会得到不同的值,且不同线程互不影响。一个线程总是只能访问到属于他自己的那份值。

Kotlin 协程

建议先跳过本节

Kotlin 协程除了具有协程的本身本性之外,实际上是由 Kotlin 管理的一个线程池。

协程的挂起指的是当前正在运行一块协程代码的线程从这块代码脱离,不再负责这块代码的执行。线程暂时处于空闲状态。每当执行到一个 suspend 函数调用时,都会发生挂起。

协程挂起发生后,开始执行 suspend 函数,负责具体执行这个函数的线程由函数的 withContext() 调用决定。注意:此时可能发生了线程的切换,脱离了原先的线程。

suspend 函数完成后,刚才挂起的协程代码恢复执行。注意:此时可能再次发生线程切换,刚才没执行的代码继续回到原先的线程执行。

问题的解决

要解决一开始提出的问题,容易想到,我们可以利用 ThreadLocal 声明一个“全局”变量,当 DAO 需要用到 Configuration 时,就从 ThreadLocal 容器去取。

同时,再声明一个函数,负责临时更改 ThreadLocal 容器的 Configuration 具体值,当传入的 lambda 执行完毕后再改回原先状态。

传入的 lambda,就是我们希望在新的 Configuration 上下文中所执行的代码。


fun <R> Configuration.use(then: ((Configuration) -> R)): R {
    //jooqConfigurationOrNull 是负责管理存储 Configuration 的属性
    val initialState = jooqConfigurationOrNull  //保存初始状态
    jooqConfigurationOrNull = this //临时变更为新状态
    return try { //注意捕获异常,防止发生异常时无法还原状态
        then(this) //执行传入的 lambda 代码块
    } catch (exception: Throwable) {
        logger.debug("Config use block failed: $exception")
        throw exception //原样抛出异常
    } finally {
        logger.debug("Recover thread local jooq config to initial")
        jooqConfigurationOrNull = initialState //还原初始状态
    }
}

var currentThreadJooqConfiguration: Configuration
    get() = currentThreadJooqConfigurationOrNull ?: jooqConfiguration
    set(value) { currentThreadJooqConfigurationOrNull = value }

//合理利用 getter 和 setter 让 ThreadLocal 对用户不可见
var jooqConfigurationOrNull: Configuration?
    get() = currentThreadJooqConfigurationContainer.get()
    set(value) {
        if (value == null)
            currentThreadJooqConfigurationContainer.remove()
        else
            currentThreadJooqConfigurationContainer.set(value)
    }

// 真正的全局 ThreadLocal 容器
private val currentThreadJooqConfigurationContainer = ThreadLocal<Configuration>()

val jooqConfiguration: Configuration
    get() = realJooqConfiguration.derive() // derive() 等效 clone(),直接取的总是派生出来的。

private lateinit var realJooqConfiguration: Configuration // 派生的根基。只做派生用途。

由于使用了 ThreadLocal,不同线程从该容器取出的结果各不相同且不会互相影响。同时,由于我们的代码霸占着这个线程,因此这里虽然临时改变了 jooqConfigurationOrNull ,但是对其他线程并没有影响。

针对一开始提出的问题,要让父 DAO 获得的 Configuration 也发生改变,只需重写父类的 getter,让父类总是从 ThreadLocal 容器获得值即可。

局限

不难发现,刚才的做法实际上是有漏洞的。

  1. 如果 lambda 代码块内调用线程池执行其他代码,绝对不能执行和数据库操作相关的动作。这是容易理解的,我们所修改的 Configuration 只在当前线程上下文起作用,调用线程池实际上就脱离了当前上下文。
  2. 不应该在 lambda 代码块内直接调用挂起函数。即使所调用的挂起函数并没有操作数据库也不可以。这是因为当前线程在空闲时可能会被安排执行其他协程任务,导致隐患。另外,当协程跑到其他线程上执行时上下文会丢失。
  3. (Kotlin) 不应该在 lambda 代码块内进行 return。若导致 use 函数没有执行后续还原初始状态的代码会导致大问题。

漏洞 1 可以通过人为规范避免。Kotlin 在语法上避免了 3 问题。

针对问题 2,要执行 suspend 函数比较困难,但是也不是不能做。首先,通过 runBlocking 使得当前线程处于阻塞状态,不允许安排其他协程任务(但遗憾的是你的函数也不肯在当前线程跑)。然后,使用 asContextElement 使协程上下文携带某种信息。但这意味着前面的代码几乎都得为此重写,总体上还是比较难做的。

状态压缩的动态规划问题:骨牌完全覆盖棋盘问题

这个算法问题对于算法菊苣们来讲不过是小菜一碟。发到博客主要是希望能够对未来拿到这个问题而毫无头绪的读者带来些许帮助。

这篇文章实际上是我的《算法分析与设计》课程的大作业,从 Word 粘贴到博客仅做了一些格式上的修改,因此文章风格会比较离谱(报告文档的风格),敬请谅解。

我的 Windows 10 2004 新增 Bug 解决办法记录

虚拟化相关

大量端口被无端保留,50000 以上高位端口无法分配

这是 Hyper-V 导致的,可以卸载 Hyper-V 平台解决它。

具体参考:

系统为每个网卡都创建一个 vEthernet 虚拟网卡

这是 Hyper-V 平台导致的,只要装了 Hyper-V 平台就会这样,暂时没找到关闭办法。

先卸载 Hyper-V 平台、沙盒、容器、虚拟机平台、虚拟机监控程序平台。

如果之后需要使用这些功能可以重启后再启用。

WSL 2 进入 shell 卡死

执行:wsl --shutdown 强制停止 WSL 后再试。

VMware Workstation 兼容性问题

升级最新的 15.5.5 可以兼容 Hyper-V 了。

VMware Workstation 15 频繁未响应

似乎是一个 Bug,当剪贴板有图片时进入虚拟机就会导致卡死。

关于这个,我在 VMWare 社区发了个帖子:Workstation and Player GUI freezed when host clipboard has image on Windows 10

其他软件

手心输入法字体渲染模糊

打开手心输入法的安装目录,对所有的 exe:

属性 -- 兼容性选项 -- 更改所有用户的设置 -- 更改高 DPI 设置 -- 高 DPI 缩放替代 -- 选择 “应用程序”

另外可以禁用掉手心输入法的开机启动,没有任何其他影响

网易云音乐升级成了 x86 转制版本

我保留了一份 网易云音乐 UWP 版 Appx Bundle 存档,可以在这里下载:kenvix/NeteaseMusicUWP

这个沙雕版本正好和 Windows 10 2004 同时发布,就写到一起了

OneDrive 一直在 “连接中”

如果你开启了系统代理,请排除以下地址:

mobile.pipe.aria.microsoft.com
login.windows.net

此外,如果你使用 OneDrive for Business 还需要排除掉你的 SharePoint 域名,例如 example-my.sharepoint.com

Kotlin 的那些骚操作

最近在学习 Kotlin 这门编程语言,不得不感叹 Kotlin 这语言是真的骚。

重载操作符

在伴生对象重载 invoke 操作符

interface BotUser {
    val id: Long
    val name: String
    val description: String

    companion object {
        operator fun invoke(
            id: Long,
            name: String = "",
            description: String = ""
        ): BotUser {
            return BotUserImpl(id, name, description)
        }
    }
}

class BotUserImpl(...) { ... }

如果你这样做的话,可以让接口或者抽象类仿佛看起来能够被实例化一样(看似调用“构造方法”,实际上是调用了 invoke() )。

用途:项目中途将某个类抽象为一个接口,并将原有类作为此接口的默认实现,这样可以做到源代码的兼容(对 Java 代码以及二进制仍不兼容,毕竟本质不同)。

代码来自我的项目 “MoeCraftBotNG”

其他操作符重载

一般可以重载 get() set() 来使对象能够按数组、字典一样去操作。

为对象编写 contains() 方法时(判断某个元素是否“属于”此对象),可以顺便为此方法加个 operator 关键字可以让 in 关键字支持这个对象

然而操作符重载还是慎用为妙,众所周知这个特性就是被 C++ 玩坏的,当你 java 甚至以不支持操作符重载为荣。好在 Kotlin 在这方面也比较节制。

协程

如果想让你的 suspend 函数被 Java 用户友善地调用,防止你被人砍,你可以:

用 Kotlin 编写一个类:

class Coroutines {
    /**
     * 获取在 Java 代码中调用 Kotlin Suspend 函数所需的最后一个参数 (Continuation)
     * @param onFinished 当suspend函数执行完毕后所调用的回调。若 Throwable 不为 null 则说明执行失败。否则为执行成功
     * @param dispatcher 协程执行线程的类型。可以为 Dispatchers.Default(CPU密集型) Dispatchers.Main(主线程) Dispatchers.IO(IO密集型)
     */
    @JvmOverloads
    fun <R> getContinuation(onFinished: BiConsumer<R?, Throwable?>, dispatcher: CoroutineDispatcher = Default): Continuation<R> {
        return object : Continuation<R> {
            override val context: CoroutineContext
                get() = dispatcher

            override fun resumeWith(result: Result<R>) {
                //注意 Result 是 inline class,不可直接给出去
                onFinished.accept(result.getOrNull(), result.exceptionOrNull())
            }
        }
    }
}

kotlin suspend 函数的调用难点在于最后一个参数,这个参数是 suspend 函数自动生成的,但是在 Java 方面处理起来却十分棘手。你可以提供这样的一个类来生成最后一个参数的值。

然后在 Java 中,就可以这样调用了:

Coroutines coroutines = new Coroutines();

//假设一个 suspend fun login(username: String, password: String): RequestResult,位于 object UserUtils
UserUtils.INSTANCE.login("user", "pass", coroutines.getContinuation(
        (result, throwable) -> {
            //suspend fun执行结束的回调
            System.out.println("Coroutines finished");
            System.out.println("Result: " + result);
            System.out.println("Exception: " + throwable);
        }
    )
);

另外,也可以使用 org.jetbrains.kotlinx:kotlinx-coroutines-jdk8,这个库可以让 suspend fun 返回 CompletableFuture<> 以便在 java 使用

fun doSomethingAsync(): CompletableFuture<List<MyClass>> =
    GlobalScope.future { doSomething() } //返回 CompletableFuture 包装的 suspend fun doSomething()

内置函数

Kotlin 有很多实用的内置函数,只提几个。

run()

run() 字面意思,用于执行一个任意代码块,并返回代码块的返回值

run() 有两种,签名如下:

public inline fun <R> run(block: () -> R): R
public inline fun <T, R> T.run(block: T.() -> R): R

第一种 run() 在为构造函数委托传递参数时特别有用,例如有一个类和两个次构造函数

class ManagedJavaProperties(val inputStream: InputStream, val outputStream: OutputStream? = null) {
    constructor(file: File): this(file.inputStream(), file.outputStream())
    constructor(fileName: String): this(???) //需要进行处理才能委托给其他构造函数
}

第二个次构造函数中需要对 fileName 进行一些处理,此时需要 run() 登场了:

class ... {
    constructor(fileName: String): this(
        kotlin.run {
            val file = File(fileName)
            if (!file.exists()) {
                file.createNewFile()
            }

            file
        }
    )
}

第二种 run() 则是将调用者当作 this 传递给 lambda,除此之外和第一种完全相同

run() 也可以用于防止代码块内变量污染当前作用域。(类似于 Java 的 { }

抑制错误

Kotlin 的 @Suppress 注解不仅可以抑制警告,还可以抑制任何错误。具体的错误名字可以到 kotlin 编译器项目按错误提示寻找。

在普通的 Gradle Java/Kotlin 项目中使用 BuildConfig

简介

Android Studio 为其 Android 项目提供了十分方便的 BuildConfig 功能,该功能在运行编译时自动生成 BuildConfig.java 文件,其中储存了编译时的一些系统信息(如APP版本号、渠道、编译时间、编译器等),并可以用于条件编译。

所幸,有人已经仿照出了具有类似功能的 Gradle 插件。本文将说明如何使用这个插件。

使用方法

1. 添加依赖

build.gradle 文件中

plugins {
    id 'java'
    id 'de.fuerstenau.buildconfig' version '1.1.8' //添加 BuildConfig 插件
}

plugins 节应位于 importbuildscript 节的后面

2. 定义项目属性

例如,对于项目 com.kenvix.moecraftbot.ng

group 'com.kenvix'
version '1.0'
def applicationName = 'MoeCraftBotNG'
def versionCode = 1

archivesBaseName = 'moecraftbot.ng'
def mainSrcDir = 'src/main/java'  //项目java源代码目录
def fullPackageName = "${group}.$archivesBaseName"
def fullPackagePath = fullPackageName.replaceAll('.', '/')
def isReleaseBuild = System.getProperty("isReleaseBuild") != null //根据环境变量判断是否为正式发行版(判断是否是Release版本的构建)

3. 添加 BuildConfig 信息

/*********************************************************************/
/**                 Application Build Config Settings               **/
/*********************************************************************/

buildConfig {
    appName = project.name       // sets value of NAME field
    version = project.version   // sets value of VERSION field,
    // 'unspecified' if project.version is not set

    clsName = 'BuildConfig'      // sets the name of the BuildConfig class
    packageName = fullPackageName  // sets the package of the BuildConfig class,
    // 'de.fuerstenau.buildconfig' if project.group is not set
    charset = 'UTF-8'            // sets charset of the generated class,
    // 'UTF-8' if not set otherwise

    buildConfigField 'String', 'APPLICATION_NAME', applicationName
    buildConfigField 'String', 'VERSION_NAME',     version as String
    buildConfigField 'int',    'VERSION_CODE',     versionCode as String
    buildConfigField 'long',   'BUILD_UNIXTIME',   System.currentTimeMillis() + 'L'
    buildConfigField 'java.util.Date', 'BUILD_DATE', 'new java.util.Date(' + System.currentTimeMillis() + 'L)'

    buildConfigField 'String', 'BUILD_USER',       System.getProperty("user.name")
    buildConfigField 'String', 'BUILD_JDK',        System.getProperty("java.version")
    buildConfigField 'String', 'BUILD_OS',         System.getProperty("os.name")
    buildConfigField 'boolean','IS_RELEASE_BUILD', isReleaseBuild as String

其中,buildConfigField 表示这是自定义字段,后面紧随的是字段类型,要用字符串书写类名。(例如 'String'

其后是字段名称 'APPLICATION_NAME'(同样用字符串),其后是内容,也必须是字符串。如果内容为 intboolean 等类型,则必须强制转换。

内容部分可以书写代码,以字符串形式书写即可。

4. 让 IDE 识别代码

默认 IDE 不会识别 BuildConfig 生成的代码,为此要手动将其加入 sourceSets

// Add generated build-config directories to the main source set, so that the
// IDE doesn't complain when the app references BuildConfig classes
sourceSets.main.java {
    srcDirs += new File(mainSrcDir) //项目本身源代码
    srcDirs += new File(buildDir, 'gen/buildconfig/src') //BuildConfig
}

5. 生成项目

刚才我有提到“判断是否是Release版本的构建”,可以这样使用它:

例如,在打包 jar 时,可以在 gradle 命令上添加 JVM 参数 -DisReleaseBuild=true 来将 isReleaseBuild 设置为 true,让项目执行在 BuildConfig.IS_RELEASE_BUILD == true 时的代码,从而达到条件编译的效果。

项目生成后, BuildConfig 效果应该如下图所示:

配置到此结束。

我们还可以在项目启动时打印一下版本信息:

println("${BuildConfig.APPLICATION_NAME} Ver.${BuildConfig.VERSION_NAME} By Kenvix")

println("Built at ${BuildConfig.BUILD_DATE.format()} By ${BuildConfig.BUILD_USER} @ ${BuildConfig.BUILD_OS} JDK ${BuildConfig.BUILD_JDK}")

if (!BuildConfig.IS_RELEASE_BUILD)
    println("Debug build")

修复国行 MIUI 打开 Google Play 始终提示 DF-DFERH-01 的问题

问题描述

自带 Google 服务框架的国行 MIUI,即使设置全局代理,打开 Google Play 仍然提示 DF-DFERH-01.

反复对 Play Store/Service 清除数据、插拔 SIM 卡均无效。在其他第三方 ROM 上 Play 正常使用。

使用 adb logcat 查看日志,捕获到下列错误:

解决

思路(过程)

  1. Root 手机
  2. 打开 adb shell
  3. 重新挂载 /system 为 RW. mount -o remount,rw /system
  4. 强制删除 /system/etc/permissions/services.cn.google.xml
    rm -f /system/etc/permissions/services.cn.google.xml
  5. 重启手机 reboot
  6. 对 Play Store/Service 清除缓存

上述操作将导致手机无法 OTA 升级。并且全量升级后需要重新进行上述操作

所以说这个办法并不好,为此,我制作了一个修复此问题的 Magisk 模块,下载后直接刷入即可

Magisk 模块

下载地址

https://github.com/kenvix/miui-df-dferh-01-fix/releases

随包附赠 Google Photos 原始画质无限容量解锁

解决 VSCode 持续调用 WMIC 导致一个 CPU 核心完全被占满的问题

问题描述

只要启动 VSCode,无论是否有负载,VSCode 都会不停地调用 WMIC.exe ,WMIC.exe 完全占满一个 CPU 核心,导致 CPU 温度很高。

解决办法

打开 VSCode 设置,搜索 debug.node.autoAttach,将其设置为 off

In general the “wmic” call does not produce significant load. But a few users see a lot of load for unknown reasons.

尽管可以解决空耗 CPU 的问题,但是这可能导致无法调试 nodejs 程序。

Java 注解预处理 Annotation Processing & 代码生成

关于 Java 的注解预处理的资料实在是过于稀少,连stackoverflow上都没多少人研究,以致于我这个萌新在尝试使用注解预处理来生成代码时踩了不少坑,正好博客也快长草了,遂决定留一篇文章,希望能够对后来者有所帮助。

本文章同时对一般 Java 项目和 Android 项目适用。

为何使用 Java 注解预处理

诚然,用反射处理注解来替代代码的复制粘贴可以让代码更加简洁、易懂(优雅),但是,反射实在是太了。

啥?反射不慢?来来来,一个 Activity 就用几十次反射,要不要和复制粘贴做一下对比?(手动阴险)

那反射这么慢,有没有什么办法?当然就是今天的主题了——代码生成: 让编译器来给你“复制粘贴”,既优雅,又高效(反正生成的代码你也不看)。

如何使用 Java 注解预处理

关于注解预处理的基本使用方法的资料还是很多的,这里就不细说了,概括一下就是:

  1. 建一个类,继承并实现 javax.annotation.processing.AbstractProcessor
  2. 建一个 META-INF.services.javax.annotation.processing.Processor
  3. 在这里写上你的预处理器的完整类名(带包名)

注意:对于 Android 项目,你需要单独建立一个 “Java 类” 项目,不可以直接在原 Android 项目中使用 注解预处理,否则你会发现没有 javax 这个包。
然后,在 Android 项目的 build.gradle 中的 dependencies 添加 annotationProcessor project(':项目名')

处理我们的注解

假定我们要处理的注解名为 ViewAutoLoad,定义为:

@Retention(RetentionPolicy.CLASS) //保留此注解到编译期
@Target(ElementType.FIELD) //此注解只适用于“字段”
public @interface ViewAutoLoad {
}

本文通过介绍对字段注解的处理来讲述如何实现注解预处理,对于方法,用法其实没啥区别。

然后,重写 process 方法:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    return true;
}

为啥要留个 return true? true表示这个注解已经被我们处理过了,编译器不用再调用其他注解处理器了。

然后开始写我们的处理代码,这里就有两种处理注解的办法了:

办法1:一次性全局处理注解

这种方法 不能 知道这个字段(方法)到底是哪个类的,自然也不能获取除了你正在处理的字段(方法)所在类的其他信息,但是用起来方便一些。

获取 全局所有 具有此注解的字段,然后用 processAnnotation 方法逐一处理它们:

roundEnv.getElementsAnnotatedWith(ViewAutoLoad.class).forEach(this::processAnnotation);

这里先讲一些常用操作,假定我们现在在实现上文的 processAnnotation 方法,它的方法签名为:

private void processFormNotEmpty(Element annotatedElement)

获取字段的类型

如果你将来要生成代码或者将注解用作编译时检查,十有八九要用到这个字段的类型。

TypeMirror fieldType = annotatedElement.asType(); 

获取这个字段的注解,或者注解的值

ViewAutoLoad annotation = annotatedElement.getAnnotation(ViewAutoLoad.class);

现在,你可以直接使用你在注解接口定义的方法了,虽然作为示例的 ViewAutoLoad 没定义任何方法。
假装定义了 value()annotation.value()

获取这个字段(方法)的名字

我觉得这个肯定会用吧

Name fieldVarName = annotatedElement.getSimpleName();
//string: fieldVarName.toString();

获取这个方法的修饰符

annotatedElement.getModifiers()

返回一个集合,这个集合装着 javax.lang.model.element.Modifier 这个枚举

办法2:逐类处理注解

虽然麻烦了点,但是这个办法让我们可以知道我们在处理哪个类了。

我们回到 process 方法:

Set<? extends Element> rootElements = roundEnv.getRootElements();

这次我们直接拿到所有编译器处理的类的基础信息了,嗯,没有过滤器。

现在我们得手撸过滤器了,既然是 Set,先遍历走起。

然后怎么过滤呢?这里有一些思路:

  • 给字段(方法)上注解的时候就指定好这个类的名称,比如 @Example("com.kenvix.test.TestClass")
    注意:不要指定成 TestClass.class,在编译期无法这样读取类名,因为类尚未编译。
  • 遍历所有类,通过字段(方法)的一些特征查找这个类

第一种思路

第一种可以是十分简单粗暴了。

String targetName = "com.kenvix.test.TestClass"; 
Element targetClass = null;
for (Element element : rootElements) {
    if(element.toString().equals(targetName)) {
        targetClass = element;
        break;
    }
}
//这里只拿到了类,注解处理方法暂时省略,见下文。

第二种思路

显然,第一种实在是不怎么优雅,第二种方法又有这些思路:

  • 先通过包名,滤掉所有绝对不相关的东西,比如 Android 项目中,包名符合 android.* 就可以去掉了。或者只看包名以自己项目开头的。
  • 字段(方法)的名称有一些我们可以利用的特征,例如都以类名开头
  • 一个类中所有字段(方法)的数量和名称是唯一的。也就是说,不存在两个类同时有一样数量的同名字段。
  • 找到有目标注解的类后,将这个类和目标注解直接加到 Map
Map<Element, List<Element>> tasks = new HashMap<>();

for (Element classElement : rootElements) {
    if(classElement.toString().startsWith(Environment.TargetAppPackage)) {
        List<? extends Element> enclosedElements = classElement.getEnclosedElements();

        for(Element enclosedElement : enclosedElements) {
            List<? extends AnnotationMirror> annotationMirrors = enclosedElement.getAnnotationMirrors();

            for (AnnotationMirror annotationMirror : annotationMirrors) {
                if(ViewAutoLoad.class.getName().equals(annotationMirror.getAnnotationType().toString())) { //好像没有其他办法在这里判断是否是目标注解了
                    if(!tasks.containsKey(classElement))
                        tasks.put(classElement, new LinkedList<>());

                     tasks.get(classElement).add(enclosedElement);
                }
            }
        }
    }
}

这样,这个 Map<> 中就包含了我们需要的类和这个类持有的字段了,接下来进行处理即可

嗯?效率低?这是编译期,加钱换CPU或用第一种,请(手动滑稽)

生成代码

这里需要用到 javapoet 这个依赖,编辑gradle配置,加入依赖:

implementation 'com.squareup:javapoet:1.8.0'

然后重写 init 方法:

    protected Types typeUtil;
    protected Elements elementUtil;
    protected Filer filer;
    protected Messager messager;
    protected ProcessingEnvironment processingEnv;

    @Override
    public synchronized final void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.processingEnv = processingEnv;
        typeUtil = processingEnv.getTypeUtils();
        elementUtil = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        onPreprocessorInit();

        messager.printMessage(Diagnostic.Kind.NOTE, "Preprocessor: " + this.getClass().getSimpleName() + " Initialized");
    }

回到 process 方法,刚才我们已经拿到了要处理的注解,接下来开始处理这些注解:

JavaPoet 资料到处都是啊,要写还不容易?

我咋取一个不可能导入的包的类型?

这问题还是很常见的,比如我们没法在一个 Java 项目中用 Android 包的东西,但是却需要生成相关的代码.

例如,我们需要用到一个类 AppCompatActivity,它在 android.support.v7.app 这个包,则可以这样写:

ClassName appCompatClass = ClassName.get("android.support.v7.app", "AppCompatActivity");

我咋表示类型通配符、泛型限定?

接上,我们还想表示 ? extends AppCompatActivity,可以这样写:

MethodSpec.Builder builder = code; //这里是你的方法builder

builder.addTypeVariable(TypeVariableName.get("T", appCompatClass)).addParameter(TypeVariableName.get("T"), "target")

保存我们的生成的代码,并在下一步编译生成的代码

回到 process 方法,加上:

if(roundEnv.processingOver()) {
    //创建FormChecker这个类
    TypeSpec formChecker = TypeSpec.classBuilder("FormChecker")
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addMethods(methods)
        .build();

    //创建类文件
    JavaFile javaFile = JavaFile.builder("com.kenvix.eg.generated", formChecker)
            .addFileComment(getFileHeader())
            .build();

    try {
        javaFile.writeTo(filer); 
    } catch (IOException ex) {
        throw new IllegalStateException(ex.toString());
    }    
}

对同一个 javaFilejavaFile.writeTo(filer) 只能调用一次,故需要判断是否为最后一轮注解预处理。

其他的可以看看这篇文章,虽然标题挺扯的(够你🐴)

其他小问题

我咋调试啊

显然这个时候按 IDE 的断点按钮是莫得了。
直接 System.outLogger 也不太好,分分钟被一堆垃圾编译消息淹没。用着还麻烦。
好吧,其实有个简单粗暴的方法,抛个运行时异常嘛,这样就能直接停止编译然后让 IDE 显示我们想要的东西了。

throw new IllegalStateException("something");

IDEA 对 addModifiers(), javaFile.writeTo(filer) 报错

IDEA bug
别理他,编译就行了

想用 getClass() 反射处理?

别想了,类都没编译好呢你get个啥

Ubuntu 上通过以太网分享网络连接(NAT)

Ubuntu 自带网络分享功能,但该功能很不稳定,往往断开连接后再连就无法使用了。
现在我们使用 DNSMASQ+IPTables 手动配置NAT.

禁用 systemd-resolved

Ubuntu 提供的 systemd-resolved 抢占53端口,首先禁用它。

systemctl stop systemd-resolved
systemctl disable systemd-resolved

删除不当设置

如果你之前配置过网络分享或已经用有线连接过电脑了,则需要这一步
运行 nm-connection-editor
删除所有有关你要分享的网卡的设置

安装并配置DNSMASQ

安装

apt install dnsmasq
service dnsmasq stop
nano /etc/dnsmasq.conf

编辑 /etc/dnsmasq.conf,加入下列内容:

dns-forward-max=15000
#eno1为你的要分享的网卡名
interface=eno1
dhcp-range=192.168.33.2,192.168.33.150,255.255.255.0,12h

配置域名解析

nano /etc/resolv.conf

填写你的DNS服务器,例如:

nameserver 223.5.5.5
nameserver 223.6.6.6
nameserver 114.114.114.114

启用内核IPV4转发

/etc/sysctl.conf

加入:

net.ipv4.ip_forward=1

运行:

sysctl -p

配置转发,为网卡分配初始IP

eno1为你的要分享的网卡名
enp2s0为有网的(被分享的)网卡名

ifconfig eno1 192.168.33.2
iptables -t nat -A POSTROUTING -o enp2s0 -j MASQUERADE
iptables -A FORWARD -i eno1 -o enp2s0 -m state --state RELATED,ESTABLISHED -j ACCEPT

上述内容重启后无效,可加入 /etc/rc.local 以开机自动应用。

启动DNSMASQ

systemctl enable dnsmasq
systemctl start dnsmasq
systemctl status dnsmasq

完事!

Windows 选择指定的网卡来开承载网络型热点

Windows 10 自带的移动热点比较废,只允许带8个设备,并且在断网后自动关闭,不能满足需求。
而通过 netsh 创建的 Microsoft 承载网络虽好,但却不支持选择用于创建热点的网卡。
本文介绍的玄学方法可以让用户做到自己选择网卡开 Microsoft 承载网络 热点。

  1. 打开 网络和 Internet 设置
  2. 更改适配器选项
  3. 选择你希望使用的网卡,重命名
  4. 起个字母排序在你不想使用的网卡的名字之前的名字,例如 !AWLAN
  5. 开!热!点!
netsh wlan set hostednetwork mode=allow ssid=namehere key=passwordhere keyUsage=persistent
netsh wlan start hostednetwork

博主真的是随便试出来的(