Skip to content

A small, but complete solution using the Spring framework and a microservice architecture for demonstration purposes. Also usable as a fully dockerized basis for a POC project.

License

Notifications You must be signed in to change notification settings

nagypet/wstemplate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wstemplate

This project implements a small, but complete solution using the Spring framework and a microservice architecture for demonstration purposes. This is actually the test environment of the spvitamin project.

The first version was a standalone docker-compose deployment, with an API gateway and a Eureka discovery service. Then I have reworked the project using Kubernetes as the deployment target. So the template-gateway project was replaced by an nginx ingress-controller and the template-eureka project is not used anymore.

Also, besides the performance-tester project there is now a JMeter performance test.

What makes this solution complete?

  • scalability and high availability with redundant services
  • https communication
  • secured webservice endpoints
  • unit tests
  • integration tests
  • Swagger online documentation and test UI
  • sonarqube
  • (Eureka, Spring Cloud Gateway, Hystrix with fault tolerance)
  • deployment in Kubernetes
  • monitoring with Prometheus/Grafana

The project can be used as a basis for further POC projects as a fully containerized microservice environment.

Architecture

architecture

There are 3 main components of the system:

  • template-auth-service: an authorization and user management service with Jwt authentication
  • template-scalable-service: 3 instances of the service are installed to achieve scalability and high availability.
  • performance-tester: to generate a simulated load for the system

Build and run

Prerequisits

Install AdoptOpenJDK 11

wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | sudo apt-key add -
sudo add-apt-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/
sudo apt install adoptopenjdk-11-hotspot
  • Get the sources. Please note that the spvitamin library is availyble in the maven central repository, so you do not have to install in in source format. Getting the sources is only needed when developing the spvitamin library:
git clone https://github.com/nagypet/wstemplate.git

optional:

cd wstemplate
git submodule update --init

This project requires spvitamin 2.0.2-RELEASE or higher.

Frontend

  • Install Node.js
npm install -g @angular/cli
npm update
build.bat

Maybe you will need to update your local ng CLI:

npm install --save-dev @angular/cli@latest

Backends

  • The build scripts will create the docker images and push them in a repository for use within the Kubernetes cluster. I have used docker-registry:5000, but you can customize this in build.gradle.
  • cd to the root folder and
gradlew clean dockerImage

When the containers are up and running, go to http://pgadmin.wstemplate.k8s-test.perit.hu for pgadmin.

  • login with postgres/sa
  • create a new connection. Parameters: host: postgres, port: 5432, username: postgres, password: sa
  • create a new database with the name 'testdb'.
  • run the script in db\scripts.sql

Check if all container is up and running.

architecture

Start the performance-tester in a command-line. The executables are located in performance-tester\build\install\performance-tester\bin. Or use the JMeter tester.

Monitoring

Go to http://grafana.wstemplate.k8s-test.perit.hu for Grafana. Login with admin/admin. image

Know-how tags:

This chapter is not up-to-date!

Find the tags in the source code to see how it was made.

Tag Description
#know-how:access-spring-managed-beans-from-outside How can we access beans managed within the application context from outside of the context
#know-how:custom-authentication-provider
#know-how:simple-httpsecurity-builder The WebSecurityConfigurerAdapter is a real pain in the ... We have SimpleHttpSecurityBuilder to simplify the security configuration.
#know-how:custom-rest-error-response In case of any exception in the server side we want to provide a useful HTTP body in Json form. This can be converted back to an Exception on the client side.
#know-how:hibernate-configuration How to configure hibernate in the most flexible way?
#know-how:jpa-auditing How to configure custom auditing features to track creation/modification of entities?
#know-how:disable-ssl-certificate-validation How to completely disable SSL certificate validation in the development environment?
#know-how:custom-zuul-error-filter Custom Zuul error filter
#know-how:gc-timer Forced garbage collection

#know-how:custom-rest-error-response

I am a big fan of propagating as much information as possible in an exception thrown on the server side. It would be great if we could catch exceptions on the client side in the same way as on the server side. There are two major problems with is:

  • Exceptions cannot be deserialized from Json
  • On the client side exception classes might not be known. As an example, imagine, the server throws a SqlServerException, but we do not include any Sql-Server dependency on the client side, still we want to be able to catch SqlServerException in the client.

To overcome those two issues I have implemented ServerExceptionProperties. This peace of information will be sent over the webservice boundaries; it contains each information of the original exception and therefore can be turned back to the proper exception on the client side. If the original exception class is not available on the client side, an instance of ServerException will be thrown. If you only want to log the exception, our ServerException behaves like the original exception. The method toString() provides the very same output as the original exception.

public interface ServerExceptionInterface {
    String getClassName();
    boolean instanceOf(Class anExceptionClass);
    boolean instanceOf(String anExceptionClassName);
    List<String> getSuperClassNames();
    Annotation[] getAnnotations();
}

There is a wrapper class ExceptionWrapper which can be created out of a Throwable. No matter if the Throwable was a ServerException or a regular exception, the wrapper handles both types identically.

    @Override
    public boolean isFatalException(Throwable ex) {
        ExceptionWrapper exception = ExceptionWrapper.of(ex);

        if (exception.causedBy("org.apache.http.conn.ConnectTimeoutException")
                || exception.causedBy("org.apache.http.NoHttpResponseException")
                || exception.causedBy("org.apache.http.conn.HttpHostConnectException")
        ) {
            return false;
        }

        return true;
    }

The method causedBy is more advanced than instanceof because the first returns true if the exception itself is an instance of the parameter, or the parameter is anywhere in the cause chain.

#know-how:disable-ssl-certificate-validation

In a development environment often we do not have a signed certificate. We generate a self-signed one, just for encryption and do not want a validation. As long as only Feign or RestTemplate is in use for building a HTTP request, we can easily customize both of them to ignore certificate validation. But if many spring cloud services are in use, like Eureka, Ribbon, Zuul, etc., each and every of them have an own way of creating the request. We simply do not have as many time to switch off validation at all the components we have. So we need a general solution which grabs the problem at a lower layer: at the layer of java.security.Provider. We can easiliy implement a NullSecurityProvider, which points to our own factory methods to instantiate a NullTrustManager.

public class NullSecurityProvider extends java.security.Provider {

    public NullSecurityProvider(String name, String versionStr, String info) {
        super(name, versionStr, info);

        put("TrustManagerFactory.PKIX", "hu.perit.spvitamin.spring.security.NullTrustManagerFactory$SimpleFactory");
        put("TrustManagerFactory.SunX509", "hu.perit.spvitamin.spring.security.NullTrustManagerFactory$SimpleFactory");
    }
}

For convenience we can implement NullSecurityProviderConfigurer and have a config key in our application.properties to easiliy allow or disable certificate validation.

@Component
@Log4j
public class NullSecurityProviderConfigurer {

    private final ServerProperties serverProperties;

    public NullSecurityProviderConfigurer(ServerProperties serverProperties) {
        this.serverProperties = serverProperties;
    }

    @PostConstruct
    void init() {
        if (this.serverProperties.getSsl() != null && this.serverProperties.getSsl().isIgnoreCertificateValidation()) {
            Provider nullSecurityProvider = new NullSecurityProvider("NullSecurityProvider", "1.0", "Skipping SSL certificate validation");
            Security.insertProviderAt(nullSecurityProvider, 1);

            log.warn("NullSecurityProvider installed!");
        }
    }
}

#know-how:custom-zuul-error-filter

Having Zuul in our classpath we can easily implement an API gateway, to have a single access point for our microservice components. Without any further configuration we only receive the following response from the gateway, in case of an internal failure, which might be completely correct, but a little more information would be nice.

{
    "timestamp": "2020-08-16 09:39:58",
    "status": 500,
    "error": "Internal Server Error",
    "message": ""
}

Implementing our CustomZuulErrorFilter we will have a better response, like this:

{
    "timestamp": "2020-08-16 09:32:12.273",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/authenticate",
    "exception": {
        "message": "Filter threw Exception",
        "exceptionClass": "com.netflix.zuul.exception.ZuulException",
        "superClasses": [
            "com.netflix.zuul.exception.ZuulException",
            "java.lang.Exception",
            "java.lang.Throwable",
            "java.lang.Object"
        ],
        "stackTrace": [
            {
                "classLoaderName": "app",
                "moduleName": null,
                "moduleVersion": null,
                "methodName": "processZuulFilter",
                "fileName": "FilterProcessor.java",
                "lineNumber": 227,
                "className": "com.netflix.zuul.FilterProcessor",
                "nativeMethod": false
            }
        ],
        "cause": {
            "message": "com.netflix.zuul.exception.ZuulException: Forwarding error",
            "exceptionClass": "org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException",
            "superClasses": [
                "org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException",
                "java.lang.RuntimeException",
                "java.lang.Exception",
                "java.lang.Throwable",
                "java.lang.Object"
            ],
            "stackTrace": [
                {
                    "classLoaderName": "app",
                    "moduleName": null,
                    "moduleVersion": null,
                    "methodName": "run",
                    "fileName": "RibbonRoutingFilter.java",
                    "lineNumber": 124,
                    "className": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter",
                    "nativeMethod": false
                }
            ],
            "cause": {
                "message": "Forwarding error",
                "exceptionClass": "com.netflix.zuul.exception.ZuulException",
                "superClasses": [
                    "com.netflix.zuul.exception.ZuulException",
                    "java.lang.Exception",
                    "java.lang.Throwable",
                    "java.lang.Object"
                ],
                "stackTrace": [
                    {
                        "classLoaderName": "app",
                        "moduleName": null,
                        "moduleVersion": null,
                        "methodName": "handleException",
                        "fileName": "RibbonRoutingFilter.java",
                        "lineNumber": 198,
                        "className": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter",
                        "nativeMethod": false
                    }
                ],
                "cause": {
                    "message": "Load balancer does not have available server for client: template-auth-service",
                    "exceptionClass": "com.netflix.client.ClientException",
                    "superClasses": [
                        "com.netflix.client.ClientException",
                        "java.lang.Exception",
                        "java.lang.Throwable",
                        "java.lang.Object"
                    ],
                    "stackTrace": [
                        {
                            "classLoaderName": "app",
                            "moduleName": null,
                            "moduleVersion": null,
                            "methodName": "getServerFromLoadBalancer",
                            "fileName": "LoadBalancerContext.java",
                            "lineNumber": 483,
                            "className": "com.netflix.loadbalancer.LoadBalancerContext",
                            "nativeMethod": false
                        }
                    ],
                    "cause": null
                }
            }
        }
    }
}

#know-how:gc-timer

Calling System.gc() is generally not recommended. But my applications have smaller memory footprint when calling gc() periodically. I have setup a scheduled job for calling gc() once in a minute, and see what happened:

JVM Total G1 Eden Space
image image

About

A small, but complete solution using the Spring framework and a microservice architecture for demonstration purposes. Also usable as a fully dockerized basis for a POC project.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published