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:

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:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>build-info</goal>
            </goals>
        </execution>
    </executions>
</plugin>

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:

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:

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:

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.