Java 快速读取文本 (算法竞赛适用)

背景: 远离 Scanner

今天无意翻阅 Scanner 类时,发现了一个很坑的地方:

// 摘抄自 Scanner JDK15 源码

public final class Scanner implements Iterator<String>, Closeable {
    ....

    // Internal matcher used for finding delimiters
    private Matcher matcher;
    // Pattern used to delimit tokens
    private Pattern delimPattern;
    // Pattern found in last hasNext operation
    private Pattern hasNextPattern;

    private Pattern integerPattern;
    private String digits = "0123456789abcdefghijklmnopqrstuvwxyz";
    private String non0Digit = "[\\p{javaDigit}&&[^0]]";
    private int SIMPLE_GROUP_INDEX = 5;
    private String buildIntegerPatternString() {
        String radixDigits = digits.substring(0, radix);
        // \\p{javaDigit} is not guaranteed to be appropriate
        // here but what can we do? The final authority will be
        // whatever parse method is invoked, so ultimately the
        // Scanner will do the right thing
        String digit = "((?i)["+radixDigits+"\\p{javaDigit}])";
        String groupedNumeral = "("+non0Digit+digit+"?"+digit+"?("+
                                groupSeparator+digit+digit+digit+")+)";
        // digit++ is the possessive form which is necessary for reducing
        // backtracking that would otherwise cause unacceptable performance
        String numeral = "(("+ digit+"++)|"+groupedNumeral+")";
        String javaStyleInteger = "([-+]?(" + numeral + "))";
        String negativeInteger = negativePrefix + numeral + negativeSuffix;
        String positiveInteger = positivePrefix + numeral + positiveSuffix;
        return "("+ javaStyleInteger + ")|(" +
            positiveInteger + ")|(" +
            negativeInteger + ")";
    }
    private Pattern integerPattern() {
        if (integerPattern == null) {
            integerPattern = patternCache.forName(buildIntegerPatternString());
        }
        return integerPattern;
    }

    public int nextInt(int radix) {
        // Check cached result
        if ((typeCache != null) && (typeCache instanceof Integer)
            && this.radix == radix) {
            int val = ((Integer)typeCache).intValue();
            useTypeCache();
            return val;
        }
        setRadix(radix);
        clearCaches();
        // Search for next int
        try {
            String s = next(integerPattern());
            if (matcher.group(SIMPLE_GROUP_INDEX) == null)
                s = processIntegerToken(s);
            return Integer.parseInt(s, radix);
        } catch (NumberFormatException nfe) {
            position = matcher.start(); // don't skip bad token
            throw new InputMismatchException(nfe.getMessage());
        }
    }

    ....
}

可见,Java 的 Scanner 是基于正则表达式实现的,这意味着 Scanner 的效率 相当低。如果在蓝桥杯之类的算法竞赛用 Scanner,会有很大一部分CPU时间会浪费在 Scanner 的正则解析上,就很可能跑出 TLE

/img/jin1.jpg

这篇文章不探讨如何快速读取大型一般二进制文件,关注重点在于在低 JDK 版本下,如何快速解析文本,而非 “快速“ 读取文件。快速读取一般二进制大文件请考虑:

认识 StringTokenizer

StringTokenizer 是一个自 Java 1.0 就被引入的类,用于以标识符分隔字符串,也就是解析文本

Scanner 之所以复杂,主要在于它假定所有输入数据都是任意的、类型是不确定的,同时 Scanner 还要兼顾处理输入流,所以不得不使用正则表达式去进行处理。

由于算法竞赛的给定数据的类型都是已知的,我们可以直接使用 StringTokenizer 去代替 Scanner

有报告显示,StringTokenizer 结合 BufferedReader 读入流的性能甚至高于 C 语言的 scanf()

构造方法

StringTokenizer 有三个常用构造方法

  1. 直接输入要解析的字符串,默认会把 “ \t\n\r\f” 当作分隔符。同时,解析返回结果不包含分隔符

Constructs a string tokenizer for the specified string. The tokenizer uses the default delimiter set, which is “ \t\n\r\f”: the space character, the tab character, the newline character, the carriage-return character, and the form-feed character. Delimiter characters themselves will not be treated as tokens.
Params: str – a string to be parsed.
Throws: NullPointerException – if str is null

public StringTokenizer(String str) {
    this(str, " \t\n\r\f", false);
}
  1. 输入要解析的字符串和分隔符列表。解析返回结果不包含分隔符

Constructs a string tokenizer for the specified string. The characters in the delim argument are the delimiters for separating tokens. Delimiter characters themselves will not be treated as tokens.
Note that if delim is null, this constructor does not throw an exception. However, trying to invoke other methods on the resulting StringTokenizer may result in a NullPointerException.
Params: str – a string to be parsed.
delim – the delimiters.
Throws: NullPointerException – if str is null

public StringTokenizer(String str, String delim) {
    this(str, delim, false);
}
  1. 输入要解析的字符串和分隔符列表。你决定要不要包含分隔符,true 为包含。

用法

获取每一段字符串 nextToken()

public String nextToken()

返回每一段被分隔的字符串 (token),等效于 Scanner.next(), 返回结果是否包含分隔符取决于你的构造参数

如果没有更多 token,会抛出 java.util.NoSuchElementException

看看还有没有更多待分隔的字符串 hasMoreTokens()

public boolean hasMoreTokens()

true 表示有更多,可以安全地调用 nextToken()

增强 StringTokenizer

问题来了,StringTokenizer 好像并没有 ScannernextInt() nextLong() 之类的东西,怎么办?

现场写个类去解析他!

import java.util.StringTokenizer;

public class ExtendedStringTokenizer extends StringTokenizer {

    public ExtendedStringTokenizer(String str) {
        super(str);
    }

    public long nextLong() {
        return Long.parseLong(this.nextToken());
    }

    public double nextDouble() {
        return Double.parseDouble(this.nextToken());
    }

    public int nextInt() {
        return Integer.parseInt(nextToken());
    }
}

只需继承一下 StringTokenizer,寥寥几行代码便可搞定

注意:此处 nextInt() nextLong() 不允许输入为小数,否则会引发解析错误。如果确实需要处理小数,可以考虑在解析前使用 indexOf()substring() 截去小数。

文件,以及有 EOF 的流快读方案

现在知道如何解析字符串了,但是怎么从流中读入字符串呢?

假设现在我们有了一个 InputStream inputStream,这个 InputStream 可以是:

  • 标准输入 System.in
  • 文件 FileInputStream("FileName")

由于算法竞赛的 JDK 版本一般很低,无法使用 NIO

一次性全部读入

这是性能最好的,并且是典型的空间换时间的方案。

以蓝桥杯竞赛为例,通常提供高达 512 MB 的算法运行内存空间,这个时候可以直接把整个流全部读入后解析。

DataInputStream stream = new DataInputStream(inputStream);
ExtendedStringTokenizer tokenizer = new ExtendedStringTokenizer(new String(stream.readAllBytes()));

注: Java 1.7 以上可以使用 Files.readString(Path.of("filename")) 直接读入整个文件到字符串

为什么不直接使用 DataInputStream

有读者可能注意到了这里并没有直接使用 DataInputStream 提供的 readInt() readDouble() 等方法,这里有一点需要理解:

例如,下面代码:

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream("5 1234567 2".getBytes());
DataInputStream dataInputStream = new DataInputStream(byteArrayInputStream);
System.out.println(dataInputStream.readLong());
System.out.println(dataInputStream.readLong());
System.out.println(dataInputStream.readLong());

输出:

3828113774942106934
Exception in thread "main" java.io.EOFException

根本不是我们想要的。

上面的说法是为了告诉你不要用它,事实上,更准确的说,readInt() readDouble() 是把一串二进制字节当作一个整体读入,用于处理二进制文件。与我们今天要做的事情(读取文本)一点关系都没有。

逐行读入

Reader 是专用于处理字符流的类,相对的,Stream 处理的是字节流

逐行读入时,使用 BufferedReader 包装 InputStreamReaderInputStreamReader 包装 InputStream 可以有效提升性能

// 注:BufferedReader 可以提供第二个参数,类型为 int,表示缓冲区容量,提供一个较大的容量可以更快
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;

while ((line = reader.readLine()) != null) {
    ExtendedStringTokenizer tokenizer = new ExtendedStringTokenizer(line);
    // TODO: your code
}

如果读取目标是文件,可以用 new FileReader("filename") 代替 new InputStreamReader(inputStream)

由于 StringTokenizer 只是一个简单的小对象,所以循环反复创建的开销还是很小的。但还是比一次性读入慢了很多

不提供 EOF 的无限流快速方案

注意:System.in 标准输入流不一定不提供 EOF,若用户在终端上按下 Ctrl+D 或通过其他方式输入等效内容,则System.in 会将此视作一个 EOF。但若用户一直不按 Ctrl+D 那就是无限等待输入了。

由于这种流不提供 EOF,我们没法直接全部读入,直接全部读入会造成永久阻塞。

但是,这种流肯定会告诉我们输入数据有多少行,我们可以用行数作为依据判断是否还要继续读。

假设第一行告诉我们后续输入有多少行:

// 注:BufferedReader 可以提供第二个参数,类型为 int,表示缓冲区容量,提供一个较大的容量可以更快
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
ExtendedStringTokenizer initTokenizer = new ExtendedStringTokenizer(reader.readLine());
int lineNum = initTokenizer.nextInt();

for (int i = 0; i < lineNum; i++) {
    // TODO: your code
    ExtendedStringTokenizer tokenizer = new ExtendedStringTokenizer(reader.readLine());
}

如果读取目标是文件,可以用 new FileReader("filename") 代替 new InputStreamReader(inputStream)

Reader 逐字符读取

如果输入数据,一个字符(注意不是字节)就是一个整体,那么还可以逐字符读取:

所用方法:

public int read()

读一个字符,并返回它。如果读到了 EOF 则返回 -1

例如:读取一个 ASCII 编码的、读取目标为数字、有换行符的文件到一个二维数组,每一个数字都是一个整体,每次换行就读取到数组的下一行。

var reader = new BufferedReader(new FileReader(PATH), 1 << 16);

byte[][] maze = new byte[100][100]; //读取的结果位于 0~9 之间,byte省空间
int lineCode = 0; // 也是实际有效行数
int colCode = 0;  // 实际有效列数
int token;

while ((token = reader.read()) != -1) {
    if (token >= '0' && token <= '9') {
        maze[lineCode][colCode] = (byte) (token - '0');
        colCode++;
    } else if (token == '\n') {
        lineCode++;

        if (colCode == 0) {
            colNum = colCode;
        }

        colCode = 0;
    }
}

多个数字(字符)是一个整体

对于多个数字是一个整体的,当然也可以使用自行读取一组数据到缓冲区,然后自行解析。然而,编写解析代码所花费的时间可能远不如使用 StringTokenizer 划算,而且这样操作带来的性能提升实际上也非常有限。

所用 Reader 方法:

public int read()
public int read(char cbuf[], int off, int len) throws IOException

后者为:从流中读取指定长度(len)的字符流,并从偏移量(off)开始装入你的数组 cbuf[],返回实际读取的有效长度(实际长度小于等于 len)

当然也可以使用前文提到的 DataInputStream

后记:一些常见误区

遍历字符串,不要 toCharArray() 后遍历

就我个人在 LeetCode 的刷题经验,当字符串编码为 Latin 时,直接 String.charAt() 会比 toCharArray() 后遍历更快(事实上在 JDK 源代码也可以找到相关依据)。

若对此有疑问,请阅读 toCharArray() 的源代码,并与 String.charAt() 进行对比,可见:

  • toCharArray() 会创建一个新字符数组,然后逐字符拷贝,并与 0xFF 进行与运算,这比 System.arraycopy() 直接数组拷贝的性能低很多。
  • 尽管 charAt() 也会进行一次与运算,但却不需要逐字拷贝而是直接访问 String 内部的字符数组。

在普通的 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")

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个啥

配置用于 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有没有什么警告,同时,你会发现代码生成好了。