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
to the Config Server. This is how the Config Server knows which set of configurations to send to a specific client. user-auth-service
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 http, ssh, 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
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.