Blue Vibes Application
The starter/configuration for BlueVibes application provides necessary set of dependencies and preconfigured beans that are needed for creating an application.
1. How to start
The first step that should be done is to include dependency in pom:
<dependency>
<groupId>io.bluevibes</groupId>
<artifactId>bv-app</artifactId>
</dependency>
Once you add bv-app dependency in your application, by default you have the following features:
- proxy and routing support - helps running application behind a proxy and rerouting sources.
- Resource - creating RESTful API: HATEOAS, Dto conversion, annotations, etc
- Swagger - documenting RESTful API
- Lombok - get rid of boilerplate code
- Feign Client - interacting with other Rest API, enhanced with validation.
2. Running application behind the proxy
In many cases, micro-application runs behind the proxy, and the proxy does not provide enough information about itself needed for the micro-application. Also, often the proxy path is different for different environments(dev, UAT, etc..).
2.1. Frontend configuration
To avoid using absolute link in frontend and having different frontend builds for different environments, we have introduced frontend configuration into BlueVibes. Once when frontend configuration is set, the BlueVibes creates a cookie witch provides information about proxy and backend.
The cookie with the api-path is optional and disabled by default, and it should be used only if you really need to provide the absolute URL manually for whatever reason. In most cases just a relative path with context path set will suffice (e.g. ./micro-app-context/api/hello
).
Example of configuration:
bv:
app:
frontend:
env: prod
api-url: /app/example
other: application specific
cookie-name: FRONT_END_CONFIG
secure: false
cookie-enabled: true
The frontend configuration consists of 4 params, only api-url
is required and should be used in frontend(JS) for creating proper link.
Let’s imagine that our application example has endpoint /api/hello
, but in production it runs behind reverse proxy link /app/example
, so to access the endpoint we have to call link /app/example/api/hello
. The property env
brings to frontend what environment it is, so it could be use for different purpose(i.e. different coloring, logging, etc.). Parameter other
is reserved for some application specific information if it’s needed. And the last parameter cookie-name
is used for cookie name, and default value is ‘FRONT_END_CONFIG’. This param should be specified only in case if name ‘FRONT_END_CONFIG’ cannot be used from some reason(i.e. firewall). The flag secure tells is cookie marked as secure or not, this property will not be serialized into the cookie.
To extract the configuration in the front-end, following javascript code could be used:
function getConfig() {
const value = "; " + document.cookie;
const parts = value.split("; FRONT_END_CONFIG=");
if (parts.length === 2) {
const cookie = parts.pop().split(";").shift();
return JSON.parse(decodeURIComponent(cookie));
}
return {};
}
2.2. Proxy aware
In order to make HATEOAS able to generate proper links (accessible through the proxy) we have introduced ProxyAware feature. The feature consists of a filter(ProxyAwareFilter) and configuration. By default, feature is disabled, it could be enabled with the configuration. Here is an example of the configuration:
bv:
app:
proxy-aware:
host: proxy-example.bluevibes.io
context-path: /example-app
scheme: https
port: 11000
ssl: true
In example below: we have assumed that we have a proxy on sub-domain: proxy-example.bluevibes.io
, and that all requests that come to the https://proxy-example.bluevibes.io:11000/example-app/**
are redirected to the bv-application /**
.
3. Routing
Since version 0.6.0 routing is disabled by default, because the overriding routing may have impact ond some already predefined endpoints(i.e. actuator, security, etc). If there is a need for frontend routing, the hash router has to be used, since it does not require backend handling. In case that developer want to use backend routing, enabling and configuration can be done through the configuration. Notice: routing is bug prone and can be tricky, so if you use it, use it wisely.
Configuration example:
bv:
app:
routing:
enabled: true
routes:
- path:
- /view/**
- /error
url: /
- path:
- /test
- /t??/**
action: redirect
url: /test.html
The parameter enabled gives you possibility to enable/disable routing completely. Next parameter indicates list of routes. Every route consists of the 2 required params: path - path matchers(absolute paths or AntPathMatcher patterns), url - final destination. Third optional parameter is action which represents how routing will be processed. Currently, we support two types: forward and redirect, if parameter action is not specified forward is used.
4. Logging Mapped Diagnostic Context(MDC)
BlueVibes provides common mechanism for injecting information about the build into MDC. It also supports passing the context to async tasks. By default, this feature is disabled. To enable this feature, the following steps must be done:
- enable by setting property
bv.mdc.enableInjection=true
- setup spring boot plugin to provide build-properties in application context:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
- setup logging pattern to use some of available props:
group
,artifactId
andversion
(i.e.logging.pattern.level="%X{artifactId}-%X{version}: %5p"
)
5. Logging request content
BlueVibes provides request logging out of the box. You can enable it but setting property bv.app.request-logging.enabled=true
in application configuration. By default, request logging is disabled.
Request logging is using debug
log level.
Entire list of properties is shown bellow:
bv:
app:
request-logging:
enabled: true
include-client-info: true
include-query-string: false
include-headers: false
include-payload: true
paths:
- "/user-info"
- "/csm/api/v1/resources/*"
Property paths
is empty by default, which means that all the requests will be logged. If you want to filter requests you want to log, you can add list of paths, like it’s shown in example above. Spring’s AntPathMatcher
class is used for matching paths from the configuration with request URI.
6. Changing the logging level in runtime
BlueVibes by default includes Actuator and exposes the endpoint for changing log level. When the bv-security
is enabled the endpoint is only accessible by localhost requests! So the level can be changed directly from console by curl
, for example:
curl -i -X POST -H 'Content-Type: application/json' -d '{"configuredLevel": "WARN"}' http://localhost:8080/actuator/loggers/root
curl -i -X POST -H 'Content-Type: application/json' -d '{"configuredLevel": "DEBUG"}' http://localhost:8080/actuator/loggers/io.bluevibes
In the example above we are changing root
logger level to WARN and the package io.bluevibes
log level to DEBUG
.
7. Interaction with other micro-services
In the cloud native world it’s almost unthinkable that a micro-application does not interact with other micro services. So in order to make that interaction easier, bv-app by default includes spring-cloud OpenFeign
dependency and provides additional enhancement for annotation based validation of response entity. Feign client is not enabled by default, so it has to be enabled explicitly by the developer. To enable client, @EnableFeignClients
should be put on the Application class or on the configuration class. If a response entity need to be validated, the class has to be marked by @Valid
, and field has to be marked with proper Hibernate validation annotations.
Example of client and response entity:
@FeignClient(name = "message", url = "http://bluevibes.io")
interface MessageClient {
@GetMapping("/api/message/{id}")
Message getMessage(@PathVariable("id")String messageId);
}
@Data
class Message {
@NotBlank(message = "Id must be a non blank value.")
private String messageId;
@NotBlank(message = "Text must be a non blank value.")
private String text;
@Valid
List<Comment> comments;
}
@Data
class Comment {
@NotBlank(message = "Comment must be a non blank value.")
private String comment;
}
@Sevice
class Service {
@Autowired
private PostClient postClient;
void doSomething() {
postClient.getAllPost();
}
}
In example above, the FeignClient performs get request to the specified endpoint and converts response body to Message object. After the converting, the object is going to be validated. First it validates message object and if some of validation fails it throws DecodeException
, if message is valid it continues validation of every comment. With the validation we want to be sure, that result is as it’s promised in definition of the API. It prevents unexpected exceptions somewhere deeper in service layer and shows that REST api does not fulfill the contract.
8. Resource API
According to clean architecture the Resource/Rest API is details and belongs to the adapter layer, further that implicates that resource controller should not contain any logic. The controller should be just an adapter to the service layer. In order to make writing API more efficient in declarative way with less boiler plate code, BlueVibes brings a set of annotations, preconfigured beans and utils.
8.1. Declaring a Rest endpoint
At the first place we wanted to keep definition of rest service as simple as it’s possible, the second we wanted to keep resource definition readable. So to aim that we have introduced a abstract resource class BaseResource
along with annotation @ResourceApi
. A resource should extend the class and be marked with the annotation. In our opinion, controller classes should be small, expose only one resource and deal with one service. Sometimes, it is acceptable to deal with more than one service, but it should not be often case. Similar is applicable for resources and sub-resources, sometimes is ok to have additional (sub)resource in same class, but not too often. Following the clean code principle it’s always better to extract this part in new class.
Example:
@ResourceApi(path = "/api/example")
public class ExampleResource extends BaseResource<ExampleDto, Example, ExampleService> {
public WorkspaceResource(ExampleService exampleService, ConversionService conversionService) {
super(exampleService, conversionService);
}
@PostMapping
@ResponseStatus(OK)
ExampleDto save(@RequestBody ExampleDto example) {
return asDto(service.save(asEntity(example)));
}
@GetMapping
@ResponseStatus(OK)
ResourceList<ExampleDto> findAll() {
return asDtoList(service.findAll());
}
@GetMapping
@ResponseStatus(OK)
ResourceList<ExampleDto> getById(@PathVariable String id) {
return asDto(service.getById(id)
.orElseThrow(() -> new EntityNotFoundException("There is no example with ID:" + id))
);
}
@DeleteMapping("/{id}")
@ResponseStatus(NO_CONTENT)
void deleteById(@PathVariable String id) {
service.delete(id);
}
}
So if we take a look in the example, definition of CRUD REST is minimalistic and done in declarative way. Thanks to BaseResource
converting dto to entity and vice versa is done by one monadic method call.
Thanks to default exception handlers, application exception is converted to 500, EntityNotFoundException to 404. In this example we assumed that service#getById
returns optional, so throwing EntityNotFound exception is done manually.
8.2. Using DTOs
Many developers do not like idea of using DTOs, in their opinion using of DTOs is double work. Sometimes that makes sense, especially in case when we have a tiny application, with API that won’t change in the future. But, in all other cases we recommend using DTOs. Some benefits are:
- if version of API has to be changed, only resource&dto package is changed(i.e. birthday should not be visible anymore, but still is needed for logic)
- Annotation based validation of input.
- swagger documenting is done in dto package, so domain is not contaminated with those annotations
- keeps clear boundary between resource and domain layer.
- having navigable API
Once when we decided to go with DTOs, we wanted to simplify writing DTOs and avoid repetitive writing of converters, so we introduced dynamic converters. By default, dynamic converting is not enabled, so it has to be enabled manually by marking a configuration with @EnableDynamicConverter
. Once dynamic converter is enabled, to make a Dto class “visible” for dynamic converter, the dto class has to be marked with @Dto(entity = Example.class)
. With this annotation the marked dto class is registered for conversion to entity class and vice versa. If we want to disable conversion from Dto to Entity we should set annotation property in
on false(@Dto(entity = Example.class, in=false)
). Either to disable conversion from entity to dto the property out
has to be set on false (@Dto(entity = Example.class, out=false)
). Dynamic conversion maps fields with the same name. This is going to cover most cases, for a special case developer has to write the manual converter. It could be done in two ways:
- writing standard spring
Converter
- extending BlueVibes
GenericConverter
The first one is covered by the spring documentation, so we are focusing on the second. GenericConverter
is abstract class that provides converting based on reflection. It is also used for DynamicConverter mentioned above. First, developer has to create class that extends GenericConcerter<SOURCE,RESULT>
. Types SOURCE and RESULT must be provided, and the bean has to be registered in the context, otherwise it will not be visible for spring conversion service. Some examples where extending GenericConverter
could be useful:
- some fields should be excluded during conversion: method
getExcludeSourceFields
has to be overridden in a way to return set of field names that should be excluded. - Result instance can’t be created with reflection (i.e. builder has to be used instead of default constructor): method
newResultInstance
. - Some fields require custom mapping: method
convert
has to be overridden in a way that callssuper#conver
, and set custom fields to result object.
8.3. Using HATEOAS for RESTful API
BlueVibes tends to provide support for creating RESTful API with hyperlinks. In order to achieve that, we included spring HATEOAS by default in dependency. Since adding links to response with HATEOAS is repeatable and create a lot of boiler plate code, BlueVibes offers set of annotation that should be used for more efficient coding. To make generating links possible, first the Resource class has to be marked with @ExposesResourceFor(ExampleDto.class)
. It gives information that marked Resource handler exposes mentioned Dto. Further, the DTO class has to extend org.springframework.hateoas.ResourceSupport
. Then for every field that we want to add the link, we are adding annotation @LinkTo
.
Example:
public class ExampleDto extends ResourceSupport {
@LinkTo(value = ExampleDto.class, relation = "self")
private String exampleId;
@LinkTo(value = UserDto.class, relation = "creator")
private String userId;
}
In example above, during converting entities into Dto, linker will add HATEOAS link for example itself, and for user.
8.4. Exception handling
BlueVibes comes with default rest exception handler, which will convert Exception to proper HTTP status. Beside providing proper http status, handler will provide additional information in response body(ErrorDto). The main aim of putting ErrorDto in response is to help the user of API to understand what is the problem.
Following error are mapped: * 400
- BAD_REQUEST * IllegalArgumentException
* MethodArgumentNotValidException
* 401
and 403
- are handled by spring security
* 404
- NOT_FOUND * EntityNotFoundException
* 500
- INTERNAL_SERVER_ERROR * ConversionFailedException
* ApplicationException
* Exception
9. Logging request headers
BlueVibes provides an out-of-the-box saving of specified request headers into MDC (Mapped Diagnostic Context). Saved headers can be propagated to other services using feign client configuration FeignClientTracingHeaderPropagationConfiguration
. To start saving request headers into MDC you need to add the following configuration:
bv:
app:
tracing-headers:
- tracing-header:
header-name: "reqIdHeader"
default-value: uuid
- tracing-header:
header-name: "userHeader"
default-value: none
Two parameters can be set for saving the request header. One is the name of the header header-name
and the second one default-value
is used to decide if the value should be generated for a missing header. Type uuid
means if the header is missing in the request, the value for a header will be generated for storing into MDC. Type none
stores nothing into MDC if the header is missing in the request.
As mentioned, values stored in MDC can be propagated using feign client configuration FeignClientTracingHeaderPropagationConfiguration
. If you use this configuration in feign client, all headers defined in yaml
configuration will be propagated if they are available in MDC. There is a possibility to skip some headers when doing propagation. For such a setup you need to extend FeignClientTracingHeaderPropagationConfiguration
in your service and override getBlacklistedHeaders()
method. This method should return the names of headers you don’t wish to propagate.