How to Build a Full stack application with Angular and Spring Cloud Microservices – Part 2

Welcome to the 2nd part of the Angular with Spring Cloud Microservices tutorial series. So far, we have decomposed the existing application into 3 different microservices. Now, we are gonna implement a Spring API Cloud Gateway, Discovery Service, and Spring Cloud Config service.

Create Config Service

In order to externalize and centralize our application configurations, Let’s create a new config-service maven project to serve the application properties from a remote git repository. You can also configure the server to serve from a local git repo or a file in the local file system.

The Git-backed configuration API provided by our server can be queried using the following paths:

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

The {application}, {profile}, {label} placeholders refer to the client’s application name (configured using spring.application.name property), the client’s current active application profile, and the Git branch respectively.

Project Dependencies

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
		<parent>
		<groupId>com.javachinna</groupId>
		<artifactId>spring-social-login-cloud-demo</artifactId>
		<version>0.0.1-SNAPSHOT</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<artifactId>config-service</artifactId>
	<name>config-service</name>
	<description>Config Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-config-server</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

spring-cloud-config-server dependency provides server-side support for externalized configuration.

Create Main Application Class

Spring Config Server can be embedded in a Spring Boot application using @EnableConfigServer annotation.

ConfigServiceApplication.java

package com.javachinna;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class ConfigServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(ConfigServiceApplication.class, args);
	}

}

Create Application Properties

The Config Server needs to know which repository to manage. There are several choices here but start with a Git-based filesystem repository. You could easily point the Config Server to a Github or GitLab repository.

To set up a Git-based filesystem repository, On the file system, create a new directory and run git init in it. Then add a file called user-auth-service.properties to the Git repository. Then run git commit in it. Later, you will connect to the Config Server with a Spring Boot application whose spring.application.name property identifies it as user-auth-service to the Config Server. This is how the Config Server knows which set of configurations to send to a specific client.

It also sends all the values from any file named application.properties or application.yml in the Git repository. Property keys in more specifically named files (such as user-auth-service.properties) override those in application.properties or application.yml.

application.properties

server.port=8888
spring.application.name=config-service
spring.cloud.config.server.git.uri=https://github.com/JavaChinna/spring-cloud-config-repo.git
spring.cloud.config.server.git.clone-on-start=true

Here we have configured our Git-url of the remote config repository, which provides our version-controlled configuration content. It can also be configured with protocols like httpssh, or a simple file on a local filesystem. In the remote config repo, we just have the default application.properties file which contains some common configurations from the original monolithic application.

application.properties from the remote git repo

app.auth.tokenSecret=j4W/RwddjfVCE01lqfpiwq8d0FAmQsSGaYD6uoo/A2MgX2DvojqAcOe/yiWizSDUXZsxDXI2aAZzbP3VaUs4MQ==
app.auth.tokenExpirationMsec=864000000
# For detailed logging during development
#logging.level.com=TRACE
logging.level.web=DEBUG
#logging.level.org.hibernate.SQL=TRACE
#logging.level.org.hibernate.type=TRACE
razorpay.key=rzp_test_tF42jI563EUWQE
razorpay.secret=eGakMHimSEH7Lzb1f002golU

Create Discovery Service

Spring Cloud Netflix provides Netflix OSS integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment and other Spring programming model idioms. With a few simple annotations, we can quickly enable and configure the common patterns inside our application and build large distributed systems with battle-tested Netflix components. The patterns provided include Service Discovery (Eureka).

Service Discovery is one of the key tenets of a microservice-based architecture. Trying to hand-configure each client or some form of convention can be difficult to do and can be brittle. Eureka is the Netflix Service Discovery Server and Client. The server can be configured and deployed to be highly available, with each server replicating the state about the registered services to the others.

Let’s create a new maven project called discovery-service. In this project, we are gonna use the spring-cloud-starter-netflix-eureka-server dependency and @EnableEurekaServer annotation to run our own embedded Eureka server. So that, eureka instances can be registered with the server and clients can discover the instances using Spring-managed beans.

Project Dependencies

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>com.javachinna</groupId>
		<artifactId>spring-social-login-cloud-demo</artifactId>
		<version>0.0.1-SNAPSHOT</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<artifactId>discovery-service</artifactId>
	<name>discovery-service</name>
	<description>Spring Cloud Discovery Demo project</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

Create Main Application Class

DiscoveryServiceApplication.java

package com.javachinna;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(DiscoveryServiceApplication.class, args);
	}
}

Create Application Properties

application.properties

server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

Here, we have set the eureka.client.register-with-eureka property to false to tell the built-in Eureka Client not to register with itself because our application should be acting as a server.

Also, we have set eureka.fetch-registry to false. This is because Eureka clients fetch the registry information from the server and cache it locally. After that, the clients use that information to find other services. Since this is a server, we don’t want the in-built client to do that.

Create Gateway Service

Spring Cloud Gateway provides a library for building an API Gateway on top of Spring WebFlux. It aims to provide a simple, yet effective way to route to APIs and provide cross-cutting concerns to them such as security, monitoring/metrics, and resiliency. It also provides Circuit Breaker and Spring Cloud DiscoveryClient integration.

Spring Cloud Circuit breaker provides an abstraction across different circuit breaker implementations. It provides a consistent API to use in your applications allowing you the developer to choose the circuit breaker implementation that best fits your needs for your app. It supports Resilience4J & Spring Retry implementations. Now we are gonna use the Resilience4J implementation. Let’s create a new maven project called gateway-service.

Project Dependencies

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>com.javachinna</groupId>
		<artifactId>spring-social-login-cloud-demo</artifactId>
		<version>0.0.1-SNAPSHOT</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<artifactId>gateway-service</artifactId>
	<name>gateway-service</name>
	<description>Spring Cloud API Gateway Demo project</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-gateway</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-config</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
		</dependency>
		<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

The spring-cloud-starter-gateway and spring-cloud-starter-circuitbreaker-reactor-resilience4j dependencies provide the Spring Cloud API Gateway and Resilience4J circuit breaker support respectively.

Note: You will get the below warning message while running this service. So you can switch to using Caffeine cache in production, by adding it in the pom.xml as shown below

2022-07-13 01:04:58.245  WARN 5548 --- [  restartedMain] iguration$LoadBalancerCaffeineWarnLogger : Spring Cloud LoadBalancer is currently working with the default cache. While this cache implementation is useful for development and tests, it's recommended to use Caffeine cache in production.You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath.
<!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

Create Main Application Class

GatewayServiceApplication.java

package com.javachinna;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class GatewayServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(GatewayServiceApplication.class, args);
	}

}

Create Application Properties

application.properties

serve.port=8080
spring.application.name=api-gateway
spring.config.import=configserver:http://localhost:8888
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping=true
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedOrigins=*
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedHeaders=*
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods=*
logging.level.web=DEBUG

The Gateway can be configured to create routes based on services registered with a DiscoveryClient compatible service registry. To enable this, we have set spring.cloud.gateway.discovery.locator.enabled=true and we already have the Eureka DiscoveryClient implementation on the classpath and enabled it.

The gateway can also be configured to control CORS behavior. Here, we have configured it to allow the CORS requests from any origin/header/http method. However, you should allow only the required origin/header/method in production.

To provide the same CORS configuration to requests that are not handled by some gateway route predicate, we have set the property spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping to true. This is useful when trying to support CORS preflight requests and our route predicate doesn’t evaluate to true because the HTTP method is options.

AppProperties.java

package com.javachinna.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
	private final Auth auth = new Auth();

	@Getter
	@Setter
	public static class Auth {
		private String tokenSecret;
		private long tokenExpirationMsec;
	}
}

Create Authentication Filter

AuthenticationFilter checks If the request is a secured route. If so, then it extracts the JWT from the authorization header, validates it, extracts the user id & role from the token and populates it as headers in the request. So that, the downstream services can make use of these to determine the identity of the user who initiated the request.

This filter implements the GatewayFilter which is a Contract for interception-style, chained processing of Web requests that may be used to implement cross-cutting, application-agnostic requirements such as security, timeouts, and others.

AuthenticationFilter.java

package com.javachinna.config;

import java.util.List;

import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;

import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Mono;

@RefreshScope
@Component
@RequiredArgsConstructor
public class AuthenticationFilter implements GatewayFilter {

    private final RouterValidator routerValidator;
    private final TokenHelper tokenHelper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        if (routerValidator.isSecured.test(request)) {

        	final String token = getJwtFromRequest(request);
        	if (token == null)
                return this.onError(exchange, "Authorization header/token is missing in request", HttpStatus.UNAUTHORIZED);

            if (!tokenHelper.validateToken(token))
                return this.onError(exchange, "Authorization header is invalid", HttpStatus.UNAUTHORIZED);

            this.populateRequestWithHeaders(exchange, token);
        }
        return chain.filter(exchange);
    }


    private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);
        return response.setComplete();
    }

    private void populateRequestWithHeaders(ServerWebExchange exchange, String token) {
        Claims claims = tokenHelper.getAllClaimsFromToken(token);
        exchange.getRequest().mutate()
                .header("id", String.valueOf(claims.getSubject()))
                .header("role", String.valueOf(claims.get("roles")))
                .build();
    }
    
	private String getJwtFromRequest(ServerHttpRequest request) {
		 List<String> authHeaders = request.getHeaders().getOrEmpty("Authorization");
		 if(!authHeaders.isEmpty()) {
			 String bearerToken = authHeaders.get(0);
			 if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
				 return bearerToken.substring(7, bearerToken.length());
			 }
		 }
		return null;
	}
}

Create Routing Configuration

The Spring Cloud Gateway uses routes to process requests to downstream services. Routes can be configured in a number of ways, but, for this guide, we use the Java API provided by the Gateway. To get started, create a new Bean of type RouteLocator in GatewayConfig class.

GatewayConfig.java

package com.javachinna.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GatewayConfig {

	@Autowired
	AuthenticationFilter filter;

	@Bean
	public RouteLocator routes(RouteLocatorBuilder builder) {
		return builder.routes().route("user-auth-service", r -> r
				.path("/api/users/**", "/api/auth/**", "/oauth2/authorization/**", "/login/oauth2/code/**")
				.filters(f -> f.filter(filter).circuitBreaker(
						config -> config.setName("user-service-circuit-breaker").setFallbackUri("forward:/user-auth-fallback")))
				.uri("lb://user-auth-service"))
				.route("product-service",
						r -> r.path("/api/products/**").filters(f -> f.filter(filter)).uri("lb://product-service"))
				.route("order-service",
						r -> r.path("/api/order/**").filters(f -> f.filter(filter)).uri("lb://order-service"))
				.build();
	}
}

Here we have configured the routes for each microservice. Also, we have configured a fallback URI for the user-auth-service using the Circuit Breaker. So that, when the user-auth-service goes down, then the request will be forwarded to the fallback URI where we can return a default response.

Create Router Validator

This is just a validator to determine if a route is secured or not.

RouterValidator.java

package com.javachinna.config;

import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.function.Predicate;

@Component
public class RouterValidator {

    public static final List<String> openApiEndpoints = List.of(
            "/auth/signup",
            "/auth/signin",
            "/oauth2/authorization",
            "/login/oauth2/code/"
    );

    public Predicate<ServerHttpRequest> isSecured =
            request -> openApiEndpoints
                    .stream()
                    .noneMatch(uri -> request.getURI().getPath().contains(uri));

}

Create JWT Helper

TokenHelper.java

package com.javachinna.config;

import java.security.Key;

import org.springframework.stereotype.Service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class TokenHelper {

	private Key key;

	public TokenHelper(AppProperties appProperties) {
		this.key = Keys.hmacShaKeyFor(appProperties.getAuth().getTokenSecret().getBytes());
	}

    public Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

	public boolean validateToken(String authToken) {
		try {
			Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(authToken);
			return true;
		} catch (MalformedJwtException ex) {
			log.error("Invalid JWT token");
		} catch (ExpiredJwtException ex) {
			log.error("Expired JWT token");
		} catch (UnsupportedJwtException ex) {
			log.error("Unsupported JWT token");
		} catch (IllegalArgumentException ex) {
			log.error("JWT claims string is empty.");
		}
		return false;
	}
}

Create Fallback Controller

This is just a fallback controller which will serve as a default response when the actual user-auth-service is down.

FallbackController.java

package com.javachinna.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import com.javachinna.dto.ApiResponse;

@RestController
public class FallbackController {

    @PostMapping("/user-auth-fallback")
    public ResponseEntity<?> authFallback() {
    	return new ResponseEntity<>(new ApiResponse(false, "User Auth Service is down! Please try later"), HttpStatus.SERVICE_UNAVAILABLE);
    }
}

Create DTOs

ApiResponse.java

package com.javachinna.dto;

import lombok.Value;

@Value
public class ApiResponse {
	private Boolean success;
	private String message;
}

Run Spring Boot App with Maven

Firstly, the discovery service should be started. This is because all other services will try to register with this registry during startup. If the registry is not running then, the discovery client in those services will throw errors.

Secondly, the config-service should be started since the other microservices will try to connect to the config-service to get the configuration properties during startup.

Thirdly, the gateway service and other services can be started in any order. You can run each microservice using mvn clean spring-boot:run and the services can be accessed via the API Gateway service http://localhost:8080 and the eureka server dashboard can be reached via http://localhost:8761

Netflix Eureka service registry dashboard

Run the Angular App

You can run this App with the below command and hit the URL http://localhost:8081/ in the browser

ng serve --port 8081

Source Code

https://github.com/JavaChinna/angular-spring-cloud-demo

Conclusion

That’s all folks. In this article, we have decomposed a monolithic Spring Boot application into microservices using Spring Cloud.

Thank you for reading.

Leave a Reply