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
JsonNodeas 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:
developmentproductiontesting
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.ymlwon't be loaded at all. Therefore, it's better to always set the application name inapplication.ymlas 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-servermodule 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
actuatorendpoints includingPrometheusare enabled at/actuator/prometheus. - Exception and error code conversion. Refer to class
ServiceApiExceptionandServiceControllerExceptionHandler. - 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.jsonlwhich 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
CacheManagerinstance 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-continueis not supported when a multipart request passes through the gateway server due to a bug in Spring Cloud Gateway.curluse 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.shwhich 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
adminwho executes DDL anduserwho 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.BusAutoconfigurationfor 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-urlenables Slack notification - Setting
spring.boot.admin.notify.mail.enabled=trueandspring.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)