ai.hyacinth.framework:core-service-api-support

Core Service Framework

License

License

GroupId

GroupId

ai.hyacinth.framework
ArtifactId

ArtifactId

core-service-api-support
Last Version

Last Version

0.5.24
Release Date

Release Date

Type

Type

jar
Description

Description

ai.hyacinth.framework:core-service-api-support
Core Service Framework
Project URL

Project URL

https://github.com/hyacinth-ai/core-service-framework
Source Code Management

Source Code Management

https://github.com/hyacinth-ai/core-service-framework

Download core-service-api-support

How to add to project

<!-- https://jarcasting.com/artifacts/ai.hyacinth.framework/core-service-api-support/ -->
<dependency>
    <groupId>ai.hyacinth.framework</groupId>
    <artifactId>core-service-api-support</artifactId>
    <version>0.5.24</version>
</dependency>
// https://jarcasting.com/artifacts/ai.hyacinth.framework/core-service-api-support/
implementation 'ai.hyacinth.framework:core-service-api-support:0.5.24'
// https://jarcasting.com/artifacts/ai.hyacinth.framework/core-service-api-support/
implementation ("ai.hyacinth.framework:core-service-api-support:0.5.24")
'ai.hyacinth.framework:core-service-api-support:jar:0.5.24'
<dependency org="ai.hyacinth.framework" name="core-service-api-support" rev="0.5.24">
  <artifact name="core-service-api-support" type="jar" />
</dependency>
@Grapes(
@Grab(group='ai.hyacinth.framework', module='core-service-api-support', version='0.5.24')
)
libraryDependencies += "ai.hyacinth.framework" % "core-service-api-support" % "0.5.24"
[ai.hyacinth.framework/core-service-api-support "0.5.24"]

Dependencies

runtime (10)

Group / Artifact Type Version
org.projectlombok : lombok jar 1.18.10
org.slf4j : log4j-over-slf4j jar
javax.xml.bind : jaxb-api jar
org.glassfish.jaxb : jaxb-runtime jar
ai.hyacinth.framework : core-service-module jar 0.5.24
ai.hyacinth.framework : core-service-web-common jar 0.5.24
org.springframework.boot : spring-boot-starter-validation jar
org.springframework.boot : spring-boot-starter-json jar
org.springframework.cloud : spring-cloud-starter-openfeign jar
io.swagger : swagger-annotations jar 1.5.22

Project Modules

There are no modules declared in this project.

Core Service Framework

Overview

Based on latest Spring Cloud version, Core Service Framework is a collection of tools and code snippets as several modules to help quickly implement micro-service architecture. Feel free to use it as the start of your project or copy/paste what you like from this repo.

License: Apache License 2.0

Development

Code Standard

  • Using Unix LF (\n)
  • Java code format by Google Code Format (no "import ...*")

IDE

  • IntelliJ IDEA (Lombok, Google-Code-format, GenSerializeID)

Related Framework, Technology and Tools

  • Spring Framework Core (Context, Bean)
  • Spring Web MVC
  • Spring Reactor (Mono, Flux)
  • Spring WebFlux
  • Spring Boot (Config, BootPackage)
  • Spring Security (on both MVC and WebFlux)
  • Spring Cloud (Feign, Config, Eureka, Sleuth)
  • Zipkin
  • JUnit and Spring Boot Test
  • JPA v2.2 and Spring Data JPA
  • Jackson JSON Mapper
  • Swagger
  • Gradle
  • git & git-flow
  • curl, httpie, openssl
  • MySQL, Redis, RabbitMQ
  • JWT
  • Maven repository

Runtime Environment

  • Java SDK 10 / 11
  • Preferred charset: UTF-8

Build from Source

With Gradle 5.x,

gradle buildAll

Modules Reference

Dependency Graph of Subprojects

project-dependencies

Refer to geenerate-dep.sh under tools.

Typical Configuration

The property spring.application.name should be set on bootstrap.yml or application.yml like this:

spring:
  profiles:
    active: development
  application:
    name: food-service
    version: 1.0

Suggest that spring.profiles.active be set to a concrete execution environment value for easy development:

  • development
  • production
  • testing

and override it on deployment.

If there's no spring-cloud related modules (discovery, config) loaded, for example, in a pure spring-boot MVC service, the bootstrap.yml won't be loaded at all. Therefore, it's better to always set the application name in application.yml as well.

Async-Job

Enable async job scheduling by adding @EnableAsync and @EnableScheduling to the application configuration class.

Schedule a job with @Scheduled annotation.

  @Scheduled(fixedRateString = "PT2M", initialDelayString = "PT30S")
  public void syncDeploymentStatus() {
    // ...
  }

Config Server

Run core-service-config-server with overriding following properties:

spring:
  cloud:
    config:
      server:
        git:
          uri: http://docker.hyacinth.services:3000/user/config-repo.git
          # uri: file://${user.home}/projects/config-repo # use local directory

Default service port is 8888.

Testing urls are like:

http :8888/user-service/development/master
# match properties files: user-service-development.yml, user-service.yml, application-development.yml, application.yml ...

http :8888/resources/development/master/ssh_config
# match plain resource files: ssh_config-development, ssh_config ...

Config Client

Add core-service-config-support as dependency with overriding following properties:

spring:
  cloud:
    config:
      uri: http://localhost:8888

Service Discovery Server (Eureka Server)

Standalone Mode

java -jar build/libs/core-service-discovery-server-1.0.0.RELEASE-boot.jar

Default service port is 8761.

Peer Mode

Examples:

Peer #1:

SPRING_PROFILES_ACTIVE=peer SERVER_PORT=8761 EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://localhost:8762/eureka/ \
java -jar build/libs/core-service-discovery-server-1.0.0.RELEASE-boot.jar

Peer #2:

SPRING_PROFILES_ACTIVE=peer SERVER_PORT=8762 EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://localhost:8761/eureka/ \
java -jar build/libs/core-service-discovery-server-1.0.0.RELEASE-boot.jar

Service Discovery Support (Eureka Client)

Using core-service-discovery-support as dependency with overriding the following properties:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

To enable health check:

eureka:
  client:
    healthcheck:
      enabled: true

To disable service discovery and instance registration, set eureka.client.enabled property or using environment variable as follow:

EUREKA_CLIENT_ENABLED=false java -jar debug-service.jar

"Config First Bootstrap" is used instead of "Discovery First Bootstrap". That means that the application could pull discovery configuration from the configuration server before starting the discovery client.

Dynamic Configuration Reloading

Triggering endpoint /actuator/refresh causes configuration (yaml, properties) reloading and @ConfigurationProperties rebinding.

@RefreshScope can also be applied on the beans which need re-initialization after rebinding of configuration properties.

core-service-gateway-server module is a working example that shows the ability of dynamic re-building api routes from the configurations pulled from config server via core-service-config-support.

Service Endpoint

Implement a concrete service by adding dependency module core-service-endpoint-support which includes following basic features:

  • Basic transit dependency to implement a SpringMVC-based service on Spring Boot platform.
  • Most actuator endpoints including Prometheus are enabled at /actuator/prometheus.
  • Exception and error code conversion. Refer to class ServiceApiException and ServiceControllerExceptionHandler.
  • Swagger support. Access swagger console by http://<service-address>:<port>/swagger-ui.html.
  • Logging support. Use Slf4j to log on console and to a json-format file. The default file name is ${spring.application.name}.log.jsonl which could be overridden by property logging.file.

JsonView Support

Define view inferface.

public class DatasetSummaryView {
  public interface Normal {}
  public interface WithHeatmap extends Normal {}
}

Annotate a property in DTO.

class Pojo {
  // ...
  @JsonView(DatasetSummaryView.WithHeatmap.class)
  private String corrHeatmap;
}

Set the view interface on API method within a Controller class.

  @JsonView(DatasetSummaryView.WithHeatmap.class)
  @GetMapping("/summary")
  Pojo getSummary() {
    // ...
  }

If a property is not annotated by @JsonView, it is not returned as default.

This behavior can be changed by defining a config bean as follow.

  @Bean
  public Jackson2ObjectMapperFactoryBean jackson2ObjectMapperFactoryBean() {
    Jackson2ObjectMapperFactoryBean factory = new Jackson2ObjectMapperFactoryBean();
    factory.setDefaultViewInclusion(true);
    return factory;
  }

Error Response

If a ServiceApiException is raised, it is finally converted into a json format like

{
    "message": "UNKNOWN_ERROR",
    "code": "E80000",
    "data": "No converter found",
    "status": "error",
    "path": "/api/dataset",
    "service": "main-service",
    "timestamp": "2019-09-18T01:19:50.466+0000"
}

The fields status, path, service, timestamp are auto-generated. message, code, data are based on the exception thrown.

This format could be parsed by core-support-api-support module and converted back into a local corresponding exception during remote call in the downstream service if possible.

Distributed Tracing (Sleuth)

Use module core-service-tracing-support.

As a result, a tracing information section like [<Service>,<TraceId>,<SpanId>,<Exportable>] could be found for every log line.

[2019-04-15 20:40:38,960] [INFO ] ... [gateway-server,80adb541045a0103,80adb541045a0103,true] ...

Additionally, if zipkin is used, try the following config:

spring:
  zipkin:
    enabled: true
    base-url: http://zipkin-host:9411/
  sleuth:
    sampler:
      # either "rate" or "probability" is effective
      # probability: 0.1 # default
      rate: 3 # limit to N requests per second

Zipkin server (in-memory storage) can be started by

docker run -d -p 9411:9411 openzipkin/zipkin

Cache

Add core-service-cache-support as dependency and import CacheConfig.

Caffeine is a in-memory cache provider so Serializable is not mandatory on the cached object.

Here's the default configuration.

spring:
  cache:
    caffeine:
      spec: maximumSize=100,expireAfterAccess=10m

Create cache by setting cache names:

spring.cache.cache-name=cache-name-1,cache-name-2,piDecimals

Code example of using cache:

@Cacheable("piDecimals")
public String getPi(int decimals) {
  // ...
}

Generally, only one CacheManager instance is configured in application context. Remove dependency of this module if other type of cache is configured.

Web Request Validation

@Validated / @Valid on request body method argument with constraints on fields like @Size(min = 8, max = 10) or @NotNull.

Validation error processing is implemented in web support module. Error response could be like:

{
    "code": "E80100",
    "data": {
        "field": [
            "username"
        ]
    },
    "message": "REQUEST_VALIDATION_FAILED",
    "path": "/api/users",
    "service": "user-service",
    "status": "error",
    "timestamp": "2019-04-10T14:24:34.033+0000"
}

File Uploading

Default multipart configuration in core-service-web-support module:

spring:
  servlet:
    multipart:
      max-file-size: 5MB
      max-request-size: 10MB
      file-size-threshold: 1MB

Code example:

@PostMapping("/users/{userId}/portrait")
public Map<String, Object> uploadUserPortrait(
  @PathVariable String userId,
  @RequestParam("portrait") MultipartFile file) {
  // ...
}

NOTE:

Http header Expect: 100-continue is not supported when a multipart request passes through the gateway server due to a bug in Spring Cloud Gateway. curl use that header but most browsers don't.

However, sending request directly to a service works properly.

SSL Server

Enable SSL configuration:

server:
  ssl:
    enabled: true
    key-alias: tomcat
    key-store-password: password
    key-store: ./tomcat.ks # or "classpath:keystore.p12"
    # key-store-type: PKCS12

If certificate files are generated by certbot (by Let's Encrypt), try ./tools/certs_to_ks.sh which converts these files into java keystore format.

HTTP/2

Enable HTTP/2 by following settings (SSL is mandatory):

server:
  http2:
    enabled: true

If a HTTP/2 connection goes through Spring Cloud Gateway, the routed service should be able to serve this HTTP/2 request.

JWT on Gateway

Configure the following properties on core-service-gateway-server by:

ai.hyacinth.core.service.gateway.server:
  jwt:
    enabled: true
    signing-key-file: file:keys/sym_keyfile.key
    # or set base64 string of the key:
    # signing-key: utWVSlUPfb3be0npL0JN41vuKJpFehpVZZKzJz5...
    expiration: 2h

Signing key file is a >64-bytes file which represents a secret key. Try generating like this:

openssl rand 128 > sym_keyfile.key

# print base64 string if "signing-key" is used.
base64 -i ./keys/sym_keyfile.key

A JWT token is auto-generated each time an Authentication payload returned from configured backend service like this:

  - path: /auth-service/api/login
    method: POST
    authority: any
    service: user-service
    uri: /api/authentication/login
    post-processing:
      - authentication-jwt
      - api

The final response wrapping JWT token returns as:

{
    "data": {
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiZXhwIjoxNTU1MTc0NzQ5LCJhdXRob3JpdHkiOlsiVVNFUiIsIkFQSSJdLCJwcmluY2lwYWwiOjF9.nXrJIh4GRYkFDe-i4RrOpZXENn_-hfIYRa3QYBbQ1FaJVGOcwVqn-IDqBHbytW8GaOgrGt2CUFm6-LB-TW1bgg"
    },
    "status": "success"
}

API Rate Limiter on Gateway

API rate limiter could be configured below:

ai.hyacinth.core.service.gateway.server:
  rate-limiter:
    replenish-period: 1m
    replenish-rate: 20

Only authenticated user is restricted. It has no effect on public API (anonymous access).

H2 In-Memory Database

With core-service-jpa-support, if no specific JDBC datasource is configured, h2 is used as default database. H2 database console can be accessed via http://host:port/h2-console.

Try using in-memory h2 jdbc-url jdbc:h2:mem:testdb (user: sa, password: empty (no password)) when log into the console.

MySQL Database

A typical modern MySQL datasource configuration is like this:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db?characterEncoding=UTF-8&useSSL=false
    username: root
    password: password

  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate.dialect: org.hibernate.dialect.MySQL57Dialect
      hibernate.dialect.storage_engine: innodb

The recommended table creation DDL suffix is: ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci. It can also achieved in database level.

CREATE SCHEMA `hyacinth_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ;

Using JsonNode as domain property

Example:

  @Type(type = "json-node")
  @Column(columnDefinition = "text")
  @Basic(fetch = FetchType.LAZY)
  private JsonNode taskDoc;

If the database supports "JSON" type, for example, MySQL or PostgreSQL, use "json" as columnDefinition, "jsonb" as argument of @Type.

Generate JPA SQL Script

Enable SQL script generation by the following configuration. Refer to jpa-support module.

spring:
  jpa:
    properties:
      javax:
        persistence:
          schema-generation:
            scripts:
              action: drop-and-create # default is "none" which means no generation

Flyway Database Migration on Startup

Put SQL scripts (naming like V1__init_schema.sql) under classpath:db/migration or classpath:db/migration/mysql.

Enable flyway by:

spring.flyway.enabled: true

If database is not empty, baseline operation is required:

spring.flyway.baseline-on-migrate: true
spring.flyway.baseline-version: 1

Startup migration is not recommended in production environment due to different user/pass, priviledges used between admin who executes DDL and user who execute DML.

Database migration could be an separate job before starting a service. Read below.

Flyway Database Migration by Gradle

Use gradle. The simplest way:

export FLYWAY_URL="jdbc:mysql://user:pass@db-host:3306/database?characterEncoding=UTF-8&useSSL=false"

gradle flywayinfo

# gradle -Pflyway.user=user -Pflyway.password=password -Pflyway.url=... flywayvalidate

gradle flywaymigrate

# for non-empty database
gralde -Pflyway.baselineOnMigrate=true -Pflyway.baselineVersion=0 flywaymigrate

Dangerous flywayclean task is disabled in build-script.gradle.

Refer to Flyway via gradle for advanced usage.

Spring Cloud Bus and Bus-Event

With module core-service-bus-support and importing of BusConfig, Spring Cloud Bus can be enabled by configuring RabbitMQ properties:

spring:
  rabbitmq:
    # addresses: host1:port1,host2:port2
    host: docker.hyacinth.services
    port: 5672
    username: alice
    password: alice-secret

Send user-defined event by BusEvent:

  @Autowired private BusService busService;

  public void broadcast() {
    busService.publish(BusService.ALL_SERVICES, "MyEventType", eventPayload);
  }

  @EventListener
  public void handleBusEvent(BusEvent<?> busEvent) {
    // busEvent.getEventType();
    // ...
  }

Refer to org.springframework.cloud.bus.BusAutoconfiguration for internal implementation of Spring Cloud Bus.

Service Admin Server

Based on Spring Boot Admin Server, the admin server uses discovery mechanism to find all services registered.

Default port is 8080.

Configuration properties:

  • Setting spring.boot.admin.notify.slack.webhook-url enables Slack notification
  • Setting spring.boot.admin.notify.mail.enabled=true and spring.mail.host (port, username, password, ...) enables Email notification (tested on Gmail using Gmail app-password)

Test URL Examples

Using httpie

user-service:

http -v ':8080/api/users' username=ziyangx password=12345678 birthDate=1981-10-01
http -v ':8080/api/users?username=ziyangx'
http -v ':8080/api/users/5'
http -v --form ':8080/api/users/5/portrait' 'portrait@./project-dependencies.png'

# for user-pass auth:
http -v ':8080/api/authentication/login' username=ziyang password=12345678

# curl examples for uploading
curl -v -F "portrait=@./project-dependencies.png" 'http://localhost:8080/api/users/5/portrait'

order-service:

http -v ':7001/api/orders' userId:=5 productId:=1000 quantity:=2
http -v ':7001/api/orders?userId=5'

debug-service:

http -v ':8080/api/call'

gateway server:

http -v ':9090/auth-service/api/login' username=ziyangx password=12345678
http -v ':9090/user-service/api/users/me' 'Cookie:SESSION=788161d1-4d1e-445e-9823-a1a0e7037b44'
http -v ':9090/user-service/api/users/current' 'Cookie:SESSION=788161d1-4d1e-445e-9823-a1a0e7037b44'
http -v ':9090/order-service/api/orders?userId=5' 'Cookie:SESSION=90dd2087-278a-4d9a-a512-dfb9dce78e17'
http -v ':9090/order-service/api/orders' 'Cookie:SESSION=9bf96fb0-752e-4659-9511-bcdeeaa925be' userId:=4 productId:=1000 quantity:=2

# jwt
http -v ':9090/user-service/api/users/whoami' 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNTU1ODU5MTQyLCJpc3MiOiJnYXRld2F5LWlzc3VlciIsImV4cCI6MTU1NTg2NjM0MiwiYXV0aG9yaXR5IjpbIlVTRVIiLCJBUEkiXSwicHJpbmNpcGFsIjoxLCJ2ZXJzaW9uIjoxfQ.RJag7ZRn0P-ohz3k6xYah5unr4AmecO4EpayrJ6dAqAH4LAg2kp_DIgU-8Zk6n6Hc4Cu7Pzzb1pbrlJQ9OOX2Q'

Todo for First Release

  • Documentation for modules introduced (Config class)

  • Document for Job trigger server

Roadmap Points

TL;DR (commented, only shown in source file)

ai.hyacinth.framework

hyacinth.ai

Versions

Version
0.5.24
0.5.21
0.5.8
0.5.5
0.5.3
0.5.2
0.5.0