Core Service Framework
- Overview
- Development
- Related Framework, Technology and Tools
- Runtime Environment
- Build from Source
- Modules Reference
- Dependency Graph of Subprojects
- Typical Configuration
- Async-Job
- Config Server
- Config Client
- Service Discovery Server (Eureka Server)
- Service Discovery Support (Eureka Client)
- Dynamic Configuration Reloading
- Service Endpoint
- Error Response
- Distributed Tracing (Sleuth)
- Cache
- Web Request Validation
- File Uploading
- SSL Server
- HTTP/2
- JWT on Gateway
- API Rate Limiter on Gateway
- H2 In-Memory Database
- MySQL Database
- Using
JsonNode
as domain property - Generate JPA SQL Script
- Flyway Database Migration on Startup
- Flyway Database Migration by Gradle
- Spring Cloud Bus and Bus-Event
- Service Admin Server
- Test URL Examples
- Todo for First Release
- Roadmap Points
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
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 inapplication.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 viacore-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 includingPrometheus
are enabled at/actuator/prometheus
. - Exception and error code conversion. Refer to class
ServiceApiException
andServiceControllerExceptionHandler
. - 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 propertylogging.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
(byLet'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 anduser
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 ofSpring 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
andspring.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)