Статьи

Использование jOOQ с JAX-RS для создания простого сервера лицензий

В некоторых случаях целесообразно иметь одноуровневую архитектуру на стороне сервера. Как правило, такие архитектуры предоставляют  RESTful  API, реализующий клиентский код и пользовательский интерфейс, используя что-то вроде  AngularJS .

В Java стандартным API для приложений RESTful является  JAX-RS , который является частью  JEE 7 вместе со стандартной  реализацией JSON . Но вы можете использовать JAX-RS и вне контейнера JEE. В следующем примере показано, как настроить простой сервер лицензий с использованием этих технологий:

  • Maven  для строительства и запуска
  • Jetty  как легкая реализация сервлета
  • Джерси , эталонная реализация JAX-RS ( JSR 311  &  JSR 339 )
  • JOOQ  как слой доступа к данным

Для примера мы будем использовать базу данных PostgreSQL.

Пример кода

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

https://github.com/jOOQ/jOOQ/tree/master/jOOQ-examples/jOOQ-jax-rs-example

Образец кода лицензируется в соответствии с условиями  Apache Software License 2.0 .

Создание базы данных сервера лицензий

Мы оставим пример простым и используем  LICENSE таблицу для хранения всех лицензионных ключей и связанной информации, тогда как  LOG_VERIFY таблица используется для регистрации доступа к серверу лицензий. Вот DDL:

CREATE TABLE LICENSE_SERVER.LICENSE (
  ID           SERIAL8      NOT NULL,

  -- The date when the license was issued
  LICENSE_DATE TIMESTAMP    NOT NULL,
  -- The e-mail address of the licensee
  LICENSEE     TEXT         NOT NULL,
  -- The license key
  LICENSE      TEXT         NOT NULL,
  -- The licensed version(s), a regular expression
  VERSION      VARCHAR(50)  NOT NULL DEFAULT '.*',

  CONSTRAINT PK_LICENSE PRIMARY KEY (ID),
  CONSTRAINT UK_LICENSE UNIQUE (LICENSE)
);

CREATE TABLE LICENSE_SERVER.LOG_VERIFY (
  ID           SERIAL8      NOT NULL,

  -- The licensee whose license is being verified
  LICENSEE     TEXT         NOT NULL,
  -- The license key that is being verified
  LICENSE      TEXT         NOT NULL,
  -- The request IP verifying the license
  REQUEST_IP   VARCHAR(50)  NOT NULL,
  -- The version that is being verified
  VERSION      VARCHAR(50)  NOT NULL,
  -- Whether the verification was successful
  MATCH        BOOLEAN      NOT NULL,

  CONSTRAINT PK_LOG_VERIFY PRIMARY KEY (ID)
);

Чтобы сделать вещи немного более интересными (и безопасными), мы также добавим генерацию лицензионного ключа в базу данных, генерируя ее из хранимой функции как таковой:

CREATE OR REPLACE FUNCTION
LICENSE_SERVER.GENERATE_KEY(
    IN license_date TIMESTAMP WITH TIME ZONE,
    IN email TEXT
) RETURNS VARCHAR
AS $
BEGIN
    RETURN 'license-key';
END;
$ LANGUAGE PLPGSQL;

Фактический алгоритм может использовать секретную соль для хеширования аргументов функции. Для обучения достаточно константной строки.

Настройка проекта

Мы собираемся  настроить генератор кода jOOQ с помощью Maven

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.jooq</groupId>
  <artifactId>jooq-webservices</artifactId>
  <packaging>war</packaging>
  <version>1.0</version>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.0.2</version>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>maven-jetty-plugin</artifactId>
        <version>6.1.26</version>
        <configuration>
          <reload>manual</reload>
          <stopKey>stop</stopKey>
          <stopPort>9966</stopPort>
        </configuration>
      </plugin>

      <plugin>
        <groupId>org.jooq</groupId>
        <artifactId>jooq-codegen-maven</artifactId>
        <version>3.2.0</version>

        <!-- See GitHub for details -->
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-server</artifactId>
      <version>1.0.2</version>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-json</artifactId>
      <version>1.0.2</version>
    </dependency>
    <dependency>
      <groupId>com.sun.jersey.contribs</groupId>
      <artifactId>jersey-spring</artifactId>
      <version>1.0.2</version>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
    </dependency>

    <dependency>
      <groupId>org.jooq</groupId>
      <artifactId>jooq</artifactId>
      <version>3.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <version>9.2-1003-jdbc4</version>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.16</version>
    </dependency>
  </dependencies>
</project>

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

Класс обслуживания лицензии

После того, как мы запустить  генератор кода jOOQ с помощью Maven , мы можем написать следующий класс обслуживания:

/**
 * The license server.
 */
@Path("/license/")
@Component
@Scope("request")
public class LicenseService {

  /**
   * <code>/license/generate</code> generates
   * and returns a new license key.
   *
   * @param mail The input email of the licensee.
   */
  @GET
  @Produces("text/plain")
  @Path("/generate")
  public String generate(
    final @QueryParam("mail") String mail
  ) {
    return run(new CtxRunnable() {

      @Override
      public String run(DSLContext ctx) {
        Timestamp licenseDate = new Timestamp(
          System.currentTimeMillis());

        return
        ctx.insertInto(LICENSE)
           .set(LICENSE.LICENSE_, generateKey(
                inline(licenseDate), inline(mail)))
           .set(LICENSE.LICENSE_DATE, licenseDate)
           .set(LICENSE.LICENSEE, mail)
           .returning()
           .fetchOne()
           .getLicense();
      }
    });
  }

  /**
   * <code>/license/verify</code> checks if a given
   * licensee has access to version using a license.
   *
   * @param request
   *   The servlet request from the JAX-RS context.
   * @param mail
   *   The input email address of the licensee.
   * @param license
   *   The license used by the licensee.
   * @param version
   *   The product version being accessed.
   */
  @GET
  @Produces("text/plain")
  @Path("/verify")
  public String verify(
    final @Context HttpServletRequest request,
    final @QueryParam("mail") String mail,
    final @QueryParam("license") String license,
    final @QueryParam("version") String version
  ) {
    return run(new CtxRunnable() {
      @Override
      public String run(DSLContext ctx) {
        String v = (version == null
                 || version.equals(""))
          ? ""
          : version;

        return
        ctx.insertInto(LOG_VERIFY)
           .set(LOG_VERIFY.LICENSE, license)
           .set(LOG_VERIFY.LICENSEE, mail)
           .set(LOG_VERIFY.REQUEST_IP,
                request.getRemoteAddr())
           .set(LOG_VERIFY.MATCH, field(
               selectCount()
              .from(LICENSE)
              .where(LICENSE.LICENSEE.eq(mail))
              .and(LICENSE.LICENSE_.eq(license))
              .and(val(v).likeRegex(LICENSE.VERSION))
              .asField().gt(0)))
           .set(LOG_VERIFY.VERSION, v)
           .returning(LOG_VERIFY.MATCH)
           .fetchOne()
           .getValue(LOG_VERIFY.MATCH, String.class);
      }
    });
  }

  // [...]
}

INSERT INTO LOG_VERIFY Запрос на самом деле довольно интересно. В простом SQL это будет выглядеть так:

INSERT INTO LOG_VERIFY (
  LICENSE,
  LICENSEE,
  REQUEST_IP,
  MATCH,
  VERSION
)
VALUES (
  :license,
  :mail,
  :remoteAddr,
  (SELECT COUNT(*) FROM LICENSE
   WHERE LICENSEE = :mail
   AND LICENSE = :license
   AND :version ~ VERSION) > 0,
  :version
)
RETURNING MATCH;

Помимо вышесказанного, он  LicenseService также содержит несколько простых утилит:

/**
 * This method encapsulates a transaction and
 * initialises a jOOQ DSLcontext. This could also be
 * achieved with Spring and DBCP for connection
 * pooling.
 */
private String run(CtxRunnable runnable) {
  Connection c = null;

  try {
    Class.forName("org.postgresql.Driver");
    c = getConnection(
        "jdbc:postgresql:postgres",
        "postgres",
        System.getProperty("pw", "test"));
    DSLContext ctx =
    DSL.using(new DefaultConfiguration()
       .set(new DefaultConnectionProvider(c))
       .set(SQLDialect.POSTGRES)
       .set(new Settings()
       .withExecuteLogging(false)));

    return runnable.run(ctx);
  }
  catch (Exception e) {
    e.printStackTrace();
    Response.status(Status.SERVICE_UNAVAILABLE);
    return "Service Unavailable";
  }
  finally {
    JDBCUtils.safeClose(c);
  }
}
private interface CtxRunnable {
    String run(DSLContext ctx);
}

Configuring Spring and Jetty

All we need now is to configure Spring…

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">

  <context:component-scan
     base-package="org.jooq.example.jaxrs" />

</beans>

… and Jetty …

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <listener>
    <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
  </listener>
  <servlet>
    <servlet-name>Jersey Spring Web Application</servlet-name>
    <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>Jersey Spring Web Application</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
</web-app>

… and we’re done! We can now run the server with the following command:

mvn jetty:run

Or if you need a custom port:

mvn jetty:run -Djetty.port=8088

Using the license server

You can now use the license server at the following URLs

http://localhost:8088/jooq-jax-rs-example/license/generate?mail=test@example.com
-> license-key

http://localhost:8088/jooq-jax-rs-example/license/verify?mail=test@example.com&license=license-key&version=3.2.0

-> true

http://localhost:8088/jooq-jax-rs-example/license/verify?mail=test@example.com&license=wrong&version=3.2.0

-> false

Let’s verify what happened, in the database:

select * from license_server.license
-- id | licensee         | license     | version
------------------------------------------------
--  3 | test@example.com | license-key | .*

select * from license_server.log_verify
-- id | licensee         | license     | match
----------------------------------------------
--  2 | test@example.com | license-key | t
--  5 | test@example.com | wrong       | f

Downloading the complete example

The complete example can be downloaded for free and under the terms of the Apache Software License 2.0 from here:

https://github.com/jOOQ/jOOQ/tree/master/jOOQ-examples/jOOQ-jax-rs-example