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

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

修复升级 Windows10 版本后所有内置应用闪退+第三方应用参数错误的问题

昨天把Windows10升级到1809后所有应用都挂了,应用商店和内置应用闪退,常用的应用参数错误,连WSL都出问题了。容我先亲切问候一下微软 :)

修复内置应用闪退、应用商店打不开

这里有 几个没啥用的方法,反正对我来说真的没啥用。

修复办法:

  1. 打开 C:\Program Files
  2. 显示隐藏的文件,找到 WindowsApps,点击上方菜单的 共享 – 高级共享
  3. 修改所有者为 Everyone
  4. 启用继承,保存

说白就是因为 ALL APPLICATION PACKAGES 没有权限。
另外还有人说是因为N卡驱动问题、LicenseManager服务被禁用等等,反正我不是这个问题。

修复第三方应用参数错误

没修好,我选择重装

配置用于 Gradle + SQLite 的 jOOQ 3.11 代码自动生成 (已过期)

敬告:这篇文章已经过期,不适用于最新版的 JOOQ,也不支持 Java11+ 和 Gradle 6.x,关于最新版的 JOOQ 使用说明,点击此处请阅读我的新文章

为什么要写这篇文章

关于如何配置用于 Gradle + SQLite 的 jOOQ 3.11 代码自动生成的文档可谓少之又少,网络上大部分文档,要么是使用 Maven,要么是针对于早期版本的 jOOQ(并不向后兼容),而 jOOQ 官方文档又很不全面,以致于许多像我这样的萌新在初次接触 jooq 时踩了不少坑,浪费了不少宝贵的时间 :)

为何一定要使用 jOOQ

复制一下 别人对此的介绍

JOOQ,全称Java Object Oriented Querying,即面向Java对象查询。它是Data Geekery公司研发的DA方案(Data Access Layer),主要解决两个问题:

  • Hibernate 的抽象使得我们离SQL太远,对SQL的掌控力度弱
  • JDBC 过于嘈杂,需要干的事情太多

JOOQ希望干的就是在上述两者中寻找一个最佳的平衡。它依据数据库中的表生成DA相关的代码,开发者将生成的代码引入项目中即可使用。

配置办法

官方文档 只贴了个代码,可以说是十分”友善”了

编辑 build.gradle

// Configure the Java plugin and the dependencies
apply plugin: 'java'

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    //在此处放置你的项目的原有依赖
    //添加jooq依赖
    compile group: 'org.jooq', name: 'jooq', version: '3.11.5'
    //<!> 一定要添加所用数据库的依赖,否则会报错而且不告诉你原因
    runtime group: 'org.xerial', name: 'sqlite-jdbc', version: '3.25.2'
}

buildscript {
    repositories {
        mavenLocal()
        mavenCentral()
    }

    dependencies {
        //添加jooq依赖
        classpath 'org.jooq:jooq-codegen:3.11.5'
        //<!> 一定要添加所用数据库的依赖,否则会报错而且不告诉你原因
        classpath group: 'org.xerial', name: 'sqlite-jdbc', version: '3.25.2'
    }
}

// Use your favourite XML builder to construct the code generation configuration file
def writer = new StringWriter()
def xml = new groovy.xml.MarkupBuilder(writer)
        .configuration('xmlns': 'http://www.jooq.org/xsd/jooq-codegen-3.11.0.xsd') {
    jdbc() {
        url('jdbc:sqlite:src/main/resources/database.db') // src/main/resources/database.db为数据库路径
        //user() //不需要用户名,省略
        //password() //不需要密码,省略
    }
    generator() {
        database() {
            includes('.*') //包括的数据表
            excludes() //排除的数据表
            inputSchema() //默认数据库
        }

        target() {
            packageName('com.kenvix.pixiv.generated.jooq') //计划用于存储生成结果的包名
            directory('src/main/java') //将生成结果储存于src/main/java
        }
    }
}

// Run the code generator
// ----------------------
org.jooq.codegen.GenerationTool.generate(writer.toString())

然后运行 gradlew buildEnvironment 看看jooq有没有什么警告,同时,你会发现代码生成好了。

配置用于 Gradle6.x + MySQL 8 的 jOOQ 3.14 代码自动生成 (已更新)

为什么要写这篇文章

之前介绍了一下在旧版Gradle、SQLite上配置JOOQ,不过那篇文章实在是太旧了,在新版Gradle上已无法使用,也不兼容Java11。

关于如何配置用于 Gradle + M 的 jOOQ 3.11 代码自动生成的文档可谓少之又少,网络上大部分文档,要么是使用 Maven,要么是针对于早期版本的 jOOQ(并不向后兼容),而 jOOQ 官方文档又很不全面,以致于许多像我这样的萌新在初次接触 jooq 时踩了不少坑,浪费了不少宝贵的时间 :)

为何一定要使用 jOOQ

复制一下 别人对此的介绍

JOOQ,全称Java Object Oriented Querying,即面向Java对象查询。它是Data Geekery公司研发的DA方案(Data Access Layer),主要解决两个问题:

  • Hibernate 的抽象使得我们离SQL太远,对SQL的掌控力度弱
  • JDBC 过于嘈杂,需要干的事情太多

JOOQ希望干的就是在上述两者中寻找一个最佳的平衡。它依据数据库中的表生成DA相关的代码,开发者将生成的代码引入项目中即可使用。

配置办法

官方文档 只贴了个代码,可以说是十分”友善”了

编辑 build.gradle

// Configure the Java plugin and the dependencies
apply plugin: 'java'

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    //在此处放置你的项目的原有依赖
    //添加jooq依赖
    compile group: 'org.jooq', name: 'jooq', version: '3.11.5'
    //<!> 一定要添加所用数据库的依赖,否则会报错而且不告诉你原因
    runtime group: 'org.xerial', name: 'sqlite-jdbc', version: '3.25.2'
}

buildscript {
    repositories {
        mavenLocal()
        mavenCentral()
    }

    dependencies {
        //添加jooq依赖
        classpath 'org.jooq:jooq-codegen:3.11.5'
        //<!> 一定要添加所用数据库的依赖,否则会报错而且不告诉你原因
        classpath group: 'org.xerial', name: 'sqlite-jdbc', version: '3.25.2'
    }
}

// Use your favourite XML builder to construct the code generation configuration file
def writer = new StringWriter()
def xml = new groovy.xml.MarkupBuilder(writer)
        .configuration('xmlns': 'http://www.jooq.org/xsd/jooq-codegen-3.11.0.xsd') {
    jdbc() {
        url('jdbc:sqlite:src/main/resources/database.db') // src/main/resources/database.db为数据库路径
        //user() //不需要用户名,省略
        //password() //不需要密码,省略
    }
    generator() {
        database() {
            includes('.*') //包括的数据表
            excludes() //排除的数据表
            inputSchema() //默认数据库
        }

        target() {
            packageName('com.kenvix.pixiv.generated.jooq') //计划用于存储生成结果的包名
            directory('src/main/java') //将生成结果储存于src/main/java
        }
    }
}

// Run the code generator
// ----------------------
org.jooq.codegen.GenerationTool.generate(writer.toString())

然后运行 gradlew buildEnvironment 看看jooq有没有什么警告,同时,你会发现代码生成好了。

修复 Windows 环境下的程序访问 WSL 中的 MySQL 提示 Access Denied 的问题

症状

Windows 10 版本 1803 + Ubuntu 18.04
位于 Windows 下的程序(例如Navicat)连接 MySQL 提示 Access Denied
同样位于 WSL 的程序可以正常访问 MySQL

原因

MySQL 把 Windows 下的程序的连接视为远程 MySQL 请求,若你使用的 MySQL 用户没有远程权限,则会出错。
WSL 特性?#(滑稽)

解决办法

WSL 输入 mysql -uroot

use mysql;
update user set host = '%' where user = 'root';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '' WITH GRANT OPTION;
FLUSH PRIVILEGES;

现在就可以使用 root 连接了(无密码,本地开发环境搞个毛的密码)

修复 WSL 下 PHP+FastCGI 卡死的问题

症状

Windows 10 版本 1803 + Ubuntu 18.04
以 Nginx 服务端为例,访问多数 PHP 文件,PHP 会直接卡死。
访问那些极其简单的 PHP 文件 (例如Hello world, phpinfo()) 虽然可以加载出来但浏览器显示网页仍未加载完全。

原因

你肯定用了 Unix Socket 方式连接 Nginx 和 PHP,然而,WSL 对 Unix Socket 的支持有 bug….

解决办法

nginx.confserver 节点添加:

fastcgi_buffering off;

使用任意磁盘或路径保存 Windows 文件历史记录

不知为何,博主的 Windows 不允许使用本机的机械硬盘保存文件历史记录,这大概又是一个秘制bug吧。

无论是 Windows7 的备份还是 Windows10 的文件历史记录,它们都允许用户选择网络位置保存数据,于是我们就可以这样操作:

\\localhost\X$\Path

该路径表示本机 X: 盘的 Path 文件夹
例如,我要将文件历史记录保存到 D:\FileHistory,只需在 “文件历史记录” – 选择驱动器 – 添加网络位置 中输入 \\localhost\D$\FileHistory

[1.12.2+Mod] MoeCraft :: 自由开放的科技向公益 Mod 服务器

特色&概述

screenshot-main

MoeCraft 创立于 2016 年六月,是一个非盈利的、旨在为热爱 MC 的玩家创造最优环境的公益 Minecraft 服务器

高水平

每份入服申请均由多名老玩家及操作员审核,最大限度确保新玩家素质达到要求

自由

MoeCraft 并无严格的规则来限制玩家的行为,MoeCraft 信任每一位玩家的自我约束能力,即,MoeCraft 开放了几乎一切能开放的功能给全部玩家。

你喜欢采矿镭射那高效直接的采矿手段吗?那就扔掉手中的钻头吧!
你喜欢核弹爆炸产生的快感吗?那就尽情享用吧!

本服服规即为:不做你认为不应该做的

平等

MoeCraft 旨在淡化 OP 对游戏的影响,我们认为,OP 亦为玩家。
同时,我们坚信,影响玩家的决策应该由玩家决定。

如何加入 MoeCraft

  1. 请打开 https://accounts.moecraft.net/index.php?m=home&c=apply&a=applyinvitecode 提交邀请码申请
  2. 我们会在最长 72 小时内审核您的申请。申请通过后您将收到邀请码邮件。您也可以手动查询申请状态
  3. 申请通过后,请您点击用户中心(https://accounts.moecraft.net/)上的注册按钮,输入信息完成注册。
  4. 完成注册后,按照提示输入 MC 玩家名确保可以登录游戏。
  5. 点击用户中心主页的“下载客户端”按钮下载 MoeCraft 客户端(根据你的系统选择客户端平台)
  6. 打开更新器,完成更新。(注意: 未来若有更新从这步开始操作)
  7. 按照更新器指示完成更新。
  8. 点击 Launcher,添加账号,账号类型选择 Authlib-Injector
  9. 输入你的用户中心邮箱和密码。
  10. 游戏选择“ MoeCraft ”或“ 1.12.2-Forge”,双击进入游戏.

注意: 若提示“无效的用户名”,则说明你操作错误,请关闭游戏,重新按照上面第六步操作。
注意: 若上述操作过程出现异常问题,请联系我们(见下文)

Mod 清单

ActuallyAdditions-1.12.2-r135.jar
ae2stuff-0.7.0.4-mc1.12.2.jar
AppleCore-mc1.12.2-3.1.3.jar
appliedenergistics2-rv5-stable-11.jar
Aroma1997Core-1.12.2-2.0.0.0.b155.jar
bdlib-1.14.3.12-mc1.12.2.jar
BiblioCraft[v2.4.5][MC1.12.2].jar
BrandonsCore-1.12-2.4.2.157-universal.jar
Chisel-MC1.12.2-0.2.0.31.jar
CodeChickenLib-1.12.2-3.1.9.344-universal.jar
CoFHCore-1.12.2-4.5.2.19-universal.jar
CoFHWorld-1.12.2-1.2.0.5-universal.jar
CompactSolars-1.12.2-5.0.17.340-universal.jar
CookingForBlockheads_1.12.2-6.4.40.jar
CraftTweaker2-1.12-4.1.9.jar
CustomSkinLoader_Forge-14.8.jar
Draconic-Evolution-1.12-2.3.11.290-universal.jar
extrautils2-1.12-1.7.6.jar
foamfix-0.9.9.1-1.12.2-law.jar
forestry_1.12.2-5.8.0.305.jar
Forgelin-1.7.4.jar
Hwyla-1.8.26-B41_1.12.2.jar
industrialcraft-2-2.8.83-ex112.jar
industrialforegoing-1.12.2-1.10.1-176.jar
ironchest-1.12.2-7.0.40.824.jar
jei_1.12.2-4.10.0.198.jar
Mantle-1.12-1.3.2.24.jar
mcjtylib-1.12-3.0.2.jar
Mekanism-1.12.2-9.4.13.349.jar
MekanismGenerators-1.12.2-9.4.13.349.jar
MTLib-3.0.5.jar
NBTEdit-0.7.jar
NotEnoughItems-1.12.2-2.4.1.233-universal.jar
OpenBlocks-1.12.2-1.7.6.jar
OpenComputers-MC1.12.2-1.7.2.67.jar
OpenModsLib-1.12.2-0.11.5.jar
Pam's+HarvestCraft+1.12.2u.jar
plustic-6.5.2.0.jar
RedstoneFlux-1.12-2.0.2.3-universal.jar
rftools-1.12-7.54.jar
roost-1.12-1.2.0.jar
SpiceOfLife-mc1.12-1.3.12.jar
TConstruct-1.12.2-2.10.1.84.jar
tesla-core-lib-1.12.2-1.0.14.jar
ThermalCultivation-1.12.2-0.3.0.7-universal.jar
ThermalDynamics-1.12.2-2.5.1.14-universal.jar
ThermalExpansion-1.12.2-5.5.0.29-universal.jar
ThermalFoundation-1.12.2-2.5.0.19-universal.jar
tinker_io-1.12.2-release+2.6.1.jar
TinkerToolLeveling-1.12.2-1.0.5.jar
UniDict-1.12.2-2.5c.jar
WanionLib-1.12.2-1.5.jar
woot-1.12.2-1.4.1.jar

联系我们

您可以通过以下方式联系我们:
电子邮件: admin$moecraft.net
用户中心的留言板(内容公开)

现已支持自助加入 MoeCraft Telegram 群,通过身份验证后即可加入
地址: https://t.me/MoeCraftBot