Статьи

JSR-308 и Checker Framework добавляют еще больше безопасности типов в jOOQ 3.9

Java 8 представила JSR-308, который добавил новые возможности аннотации к языку Java. Самое главное: введите аннотации. Теперь можно создавать монстров, как показано ниже:

Код, отображаемый в этом твите, действительно компилируется. Каждый тип может быть аннотирован сейчас, чтобы улучшить систему типов любым пользовательским способом. Почему, спросите вы? Одним из основных вариантов использования этого улучшения языка является среда проверки , библиотека с открытым исходным кодом, которая позволяет легко реализовывать произвольные плагины компилятора для сложной проверки типов. Самый скучный и тривиальный пример — обнуляемость. Рассмотрим следующий код:

01
02
03
04
05
06
07
08
09
10
import org.checkerframework.checker.nullness.qual.Nullable;
 
class YourClassNameHere {
    void foo(Object nn, @Nullable Object nbl) {
        nn.toString(); // OK
        nbl.toString(); // Fail
        if (nbl != null)
            nbl.toString(); // OK again
    }
}

Приведенный выше пример может быть запущен непосредственно в демо-консоли live . Компиляция вышеуказанного кода с использованием следующего процессора аннотаций:

javac -процессор org.checkerframework.checker.nullness.NullnessChecker afile.java

Урожайность:

Ошибка: [dereference.of.nullable] разыменование возможно нулевой ссылки nbl: 5: 9

Это довольно круто! Он работает почти так же, как, например, чувствительная к потоку типизация, реализованная на Цейлоне или Котлине , за исключением того, что он гораздо более многословен. Но он также намного более мощный, потому что правила, которые реализуют вашу улучшенную и аннотированную систему типов Java, могут быть реализованы непосредственно в Java с использованием процессоров аннотаций! Это делает аннотации завершенными, таким образом & # 55357; & # 56841;

Как это помогает JOOQ?

jOOQ уже давно поставляется с двумя типами аннотаций документации API. Эти аннотации:

  • @PlainSQL — чтобы указать, что метод DSL принимает строку «обычный SQL», которая может представлять риски внедрения SQL
  • @Support — для указания того, что метод DSL работает или изначально, или может быть эмулирован для данного набора SQLDialect.

Примером такого метода является предложение CONNECT BY , которое поддерживается Cubrid, Informix и Oracle и перегружено, чтобы для удобства также принимать предикат «простого SQL»:

1
2
3
@Support({ CUBRID, INFORMIX, ORACLE })
@PlainSQL
SelectConnectByConditionStep<R> connectBy(String sql);

До сих пор эти аннотации были только для целей документирования. С jOOQ 3.9, больше нет. Сейчас мы представляем две новые аннотации для API jOOQ:

  • org.jooq.Allow — разрешить использование набора диалектов (или аннотации @PlainSQL ) в заданной области видимости
  • org.jooq.Require — требовать поддержки набора диалектов через аннотацию @Support в заданной области

Это лучше всего объяснить на примере. Давайте сначала посмотрим на @PlainSQL

Ограничение доступа к @PlainSQL

Одним из самых больших преимуществ использования jOOQ API является то, что SQL-инъекция осталась в прошлом. Поскольку jOOQ является внутренним языком, специфичным для предметной области, пользователи действительно определяют дерево выражений SQL непосредственно в своем коде Java, а не в виде строковой версии оператора, как в JDBC. Дерево выражений, скомпилированное в Java, не дает возможности вводить какие-либо нежелательные или непредвиденные выражения через пользовательский ввод.

Хотя есть одно исключение. jOOQ не поддерживает все функции SQL в каждой базе данных. Вот почему jOOQ поставляется с богатым API «простого SQL», где пользовательские строки SQL могут быть встроены в любое место дерева выражений SQL. Например, вышеприведенное предложение CONNECT BY :

1
2
3
4
DSL.using(configuration)
   .select(level())
   .connectBy("level < ?", bindValue)
   .fetch();

Приведенный выше запрос jOOQ переводится в следующий запрос SQL:

1
2
3
SELECT level
FROM dual
CONNECT BY level < ?

Как видите, вполне возможно «сделать это неправильно» и создать риск внедрения SQL, как в JDBC:

1
2
3
4
DSL.using(configuration)
   .select(level())
   .connectBy("level < " + bindValue)
   .fetch();

Разница очень тонкая. С jOOQ 3.9 и средой проверки теперь можно указать следующую конфигурацию компилятора Maven:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.3</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <fork>true</fork>
        <annotationProcessors>
            <annotationProcessor>org.jooq.checker.PlainSQLChecker</annotationProcessor>
        </annotationProcessors>
        <compilerArgs>
            <arg>-Xbootclasspath/p:1.8</arg>
        </compilerArgs>
    </configuration>
</plugin>

org.jooq.checker.PlainSQLChecker гарантирует, что никакой клиентский код, использующий API, аннотированный @PlainSQL не скомпилируется. Сообщение об ошибке, которое мы получаем, выглядит примерно так:

C: \ Users \ lukas \ workspace \ jOOQ \ jOOQ-examples \ jOOQ-checker-framework-example \ src \ main \ java \ org \ jooq \ example \ checker \ PlainSQLCheckerTests.java: [17,17] ошибка: [Обычная Использование SQL недопустимо в текущей области. Используйте @ Allow.PlainSQL.]

Если вы знаете, что вы делаете ™, и вам абсолютно необходимо использовать @PlainSQL API @PlainSQL в очень определенном месте (области), вы можете аннотировать это местоположение (область) с помощью @Allow.PlainSQL и код снова прекрасно компилируется :

1
2
3
4
5
6
7
8
// Scope: Single method.
@Allow.PlainSQL
public List<Integer> iKnowWhatImDoing() {
    return DSL.using(configuration)
              .select(level())
              .connectBy("level < ?", bindValue)
              .fetch(0, int.class);
}

Или даже:

01
02
03
04
05
06
07
08
09
10
// Scope: Entire class.
@Allow.PlainSQL
public class IKnowWhatImDoing {
    public List<Integer> iKnowWhatImDoing() {
        return DSL.using(configuration)
                  .select(level())
                  .connectBy("level < ?", bindValue)
                  .fetch(0, int.class);
    }
}

Или даже (но тогда вы можете просто отключить проверку):

1
2
3
// Scope: entire package (put in package-info.java)
@Allow.PlainSQL
package org.jooq.example.checker;

Однако преимущества очевидны. Если безопасность очень важна для вас (и должна быть), просто включите org.jooq.checker.PlainSQLChecker в каждой сборке разработчика или, по крайней мере, в сборках CI, и получайте ошибки компиляции всякий раз, когда «случайное» @PlainSQL API @PlainSQL происходит встречается.

Ограничение доступа к SQLDialect

Теперь гораздо более интересным для большинства пользователей является возможность проверить, действительно ли jOOQ API, используемый в клиентском коде, поддерживает вашу базу данных. Например, указанное выше предложение CONNECT BY поддерживается только в Oracle (если мы игнорируем не очень популярные базы данных Cubrid и Informix). Предположим, вы работаете только с Oracle. Вы хотите убедиться, что все используемые вами jOOQ API совместимы с Oracle. Теперь вы можете поместить следующую аннотацию ко всем пакетам, которые используют jOOQ API:

1
2
3
// Scope: entire package (put in package-info.java)
@Allow(ORACLE)
package org.jooq.example.checker;

Теперь просто активируйте org.jooq.checker.SQLDialectChecker чтобы проверить тип кода на соответствие @Allow и все готово:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.3</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <fork>true</fork>
        <annotationProcessors>
            <annotationProcessor>org.jooq.checker.SQLDialectChecker</annotationProcessor>
        </annotationProcessors>
        <compilerArgs>
            <arg>-Xbootclasspath/p:1.8</arg>
        </compilerArgs>
    </configuration>
</plugin>

С этого момента, всякий раз, когда вы используете какой-либо jOOQ API, вышеприведенная программа проверки будет проверять, что любой из следующих трех выводит true:

  • Используемый API jOOQ не аннотирован @Support
  • Используемый API jOOQ аннотируется @Support , но без какого-либо явного SQLDialect (т. SQLDialect «Работает на всех базах данных»), такого как DSLContext.select()
  • Используемый jOOQ API аннотирован @Support и, по крайней мере, одним из SQLDialects на SQLDialects ссылается @Allow

Таким образом, внутри пакета аннотируется как таковой …

1
2
3
// Scope: entire package (put in package-info.java)
@Allow(ORACLE)
package org.jooq.example.checker;

… Использование метода, аннотированного как такового, хорошо:

1
2
3
@Support({ CUBRID, INFORMIX, ORACLE })
@PlainSQL
SelectConnectByConditionStep<R> connectBy(String sql);

… Но использование метода, аннотированного как такового, не является:

1
2
@Support({ MARIADB, MYSQL, POSTGRES })
SelectOptionStep<R> forShare();

Чтобы разрешить использование этого метода, клиентский код может, например, разрешить диалект MYSQL в дополнение к диалекту ORACLE:

1
2
3
// Scope: entire package (put in package-info.java)
@Allow({ MYSQL, ORACLE })
package org.jooq.example.checker;

Отныне весь код в этом пакете может ссылаться на методы, поддерживающие MySQL и / или Oracle.

Аннотация @Allow помогает предоставить доступ к API на глобальном уровне. Несколько аннотаций @Allow (потенциально различной области) создают дизъюнкцию разрешенных диалектов, как показано здесь:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// Scope: class
@Allow(MYSQL)
class MySQLAllowed {
 
    @Allow(ORACLE)
    void mySQLAndOracleAllowed() {
        DSL.using(configuration)
           .select()
            
           // Works, because Oracle is allowed
           .connectBy("...")
            
           // Works, because MySQL is allowed
           .forShare();
    }
}

Как можно видеть выше, разделение на два диалекта дизъюнктивно не гарантирует, что данный оператор будет работать с любой из баз данных. Так…

Что если я хочу, чтобы обе базы данных поддерживались?

В этом случае мы прибегнем к использованию новой аннотации @Require . Несколько аннотаций @Require (потенциально различной области) создают соединение требуемых диалектов, как показано здесь:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// Scope: class
@Allow
@Require({ MYSQL, ORACLE })
class MySQLAndOracleRequired {
 
    @Require(ORACLE)
    void onlyOracleRequired() {
        DSL.using(configuration)
           .select()
            
           // Works, because only Oracle is required
           .connectBy("...")
            
           // Doesn't work because Oracle is required
           .forShare();
    }
}

Как это использовать

Предположим, ваше приложение требует только работы с Oracle. Теперь вы можете поместить следующую аннотацию в ваш пакет, и вам будет запрещено использовать любой API-интерфейс только для MySQL, например, потому что MySQL не разрешен в качестве диалекта в вашем коде:

1
2
@Allow(ORACLE)
package org.jooq.example.checker;

Теперь, когда требования меняются, вы хотите начать поддерживать MySQL также из своего приложения. Просто измените спецификацию пакета на следующую и начните исправлять все ошибки компиляции при использовании jOOQ.

1
2
3
4
5
6
// Both dialects are allowed, no others are
@Allow({ MYSQL, ORACLE })
 
// Both dialects are also required on each clause
@Require({ MYSQL, ORACLE })
package org.jooq.example.checker;

Значения по умолчанию

По умолчанию для любой области видимости следующие org.jooq.checker.SQLDialectChecker предполагают следующие аннотации:

  • Ничего не разрешено. Каждая аннотация @Allow добавляет набор разрешенных диалектов.
  • Все требуется. Каждая аннотация @Require удаляет из набора необходимых диалектов.

Увидеть это в действии

Эти функции станут неотъемлемой частью jOOQ 3.9. Они доступны, просто добавив следующую зависимость:

01
02
03
04
05
06
07
08
09
10
<dependency>
    <!-- Use org.jooq            for the Open Source edition
             org.jooq.pro        for commercial editions,
             org.jooq.pro-java-6 for commercial editions with Java 6 support,
             org.jooq.trial      for the free trial edition -->
     
    <groupId>org.jooq</groupId>
    <artifactId>jooq-checker</artifactId>
    <version>${org.jooq.version}</version>
</dependency>

… А затем выбирая соответствующие процессоры аннотаций для вашего плагина компилятора.

Не можете дождаться, пока JOOQ 3.9? Вам не нужно. Просто проверьте версию 3.9.0-SNAPSHOT от GitHub и следуйте примеру проекта, приведенному здесь:

Готово! Отныне, используя jOOQ, вы можете быть уверены, что любой написанный вами код будет работать на всех базах данных, которые вы планируете поддерживать!

Я думаю, что титул чемпиона Annotatiomaniac в этом году должен достаться создателям фреймворка:

Дальнейшее чтение о структуре проверки: