Microservices with Spring Boot and Spring Cloud Developer Tutorial
In this developer tutorial, we are going to understand the basic concepts of microservices, in what ways microservice architectures are better than monolithic ones, and how we can implement a microservice architecture using Spring Boot and Spring Cloud.
Let’s start from the beginning.
What are Microservices?
There isn’t one universally accepted definition for microservices, but for this tutorial we are going to define microservices as an architectural style for building a suite of autonomous, self-contained and loosely coupled services that communicate over lightweight mechanisms, such as HTTP resource APIs.
Characteristics of Microservices
• Autonomous: Microservices are self-contained and can be developed and deployed independently without affecting other services.
• Specialized: Each microservice is designed for one specific capability.
• Stateless: Microservices don’t share the state of the service; in some cases, if there is a requirement to maintain state, it will be maintained in a database.
• Well-defined interfaces (service contract): Microservices have well-defined interfaces that permit communication with them, such as a JSON schema or WSDL.
Monolithic vs. Microservices
Monolithic architectures have all processes tightly coupled, and they run as a single service.
Which one Should I Implement?
If you want to build a simple, lightweight application, the monolithic architecture can be an option, but if you prefer to develop complex and evolving software, the microservices architecture will definitely be the best choice. In the end, selecting the software architecture will depend entirely on your project size, time, and requirements, among other factors.
Creating our Microservices Architecture
Spring Cloud Netflix Eureka Server
This is an application that stores all the information about all the microservices. In other words, Eureka is a dynamic service discovery; every microservice will be registered in the server so that Eureka will know all the client applications running on each port and IP address.
Eureka consists of a server and a client-side component. The server component will be the registry in which all the microservices register their availability. The microservices will use the Eureka client to register; once the registration is complete, it reaches out to the server and notifies it of its existence. It is important to understand that the consuming components will also use the client to discover the service instances.
Setting up the Eureka Server
First, we are going to create a new Maven project with Spring Boot; we can create our project using the https://start.spring.io/ page. The project structure should look like this:
Once we’ve created the project, we are going to make sure that we have the “spring-cloud-starter-config” and “spring-cloud-starter-netflix-eureka-server” dependencies. Your pom.xml should look like this:
1.8
Greenwich.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-config
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
In this guide, we are going to create the Eureka server in stand-alone mode, so we are going to point the Eureka default zone to our stand-alone instance.
To set up our Eureka server, we need to follow these steps:
1. Add the following Eureka configuration to the “application.properties”:
a. Eureka server name
b. Server port
c. Additional configuration
# Eureka server name
spring.application.name=eureka-server
# Default port for Eureka server
server.port=8761
# We are going to set the following attributes to false, because by
#default Eureka registers itself as a client
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
2. Change the default “Application.java” to “EurekaServerApplication.java”. In this class, we need to add the “@EnableEurekaServer” annotation:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
3. Start the Eureka server, running the application as a Spring Boot app. Once the application starts, we can open the Eureka console, which is going to be hosted in the server port we previously defined in the application properties.
Eureka web console link: http://localhost:8761/
Note: As you can see, there is no instance registered under the “instances currently registered with Eureka” section. This is because we haven’t registered any microservices as a client yet.
Register Microservices as Eureka Clients
In this section, we are going to be creating our product, order and authentication microservices. In order to do so, follow these 8 steps:
1. Just as we did with the Eureka service, we are going to create three new Spring Boot applications (order, product, authentication), and we are going to make sure we have the following dependencies in all the pom.xml files:
1.8
Greenwich.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-config
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
2. Once we have the microservices, we need to add the “@EnableEurekaClient” annotation in the default “Application.java” for each service.
Product Application
@SpringBootApplication
@EnableEurekaClient
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
Order Application
@SpringBootApplication
@EnableEurekaClient
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
Authentication Application
@SpringBootApplication
@EnableEurekaClient
public class AuthenticationApplication {
public static void main(String[] args) {
SpringApplication.run(AuthenticationApplication.class, args);
}
3. Then, in the application properties file, we define the Eureka client configurations:
Product Application
# Service name
spring.application.name=product-service
# Port
server.port=8200
# Eureka server url
eureka.client.service-url.default-zone=http://localhost:8761/eureka
Order Application
# Service name
spring.application.name=order-service
# Port
server.port=8300
# Eureka server url
eureka.client.service-url.default-zone=http://localhost:8761/eureka
Authentication Application
# Service name
spring.application.name=auth-service
# Port
server.port=8400
# Eureka server url
eureka.client.service-url.default-zone=http://localhost:8761/eureka
4. In this example, we are going to call the “products” service within the “order” service. To achieve this, we are going to use an object that is capable of sending requests to other REST API services: the Spring RestTemplate.
This object is a load balancer client; in this guide, we are going to be using the Netflix Eureka default load balancer called “Ribbon.” In order to do this, we are going to be registering a RestTemplate as a “bean,” a method-level annotation allowing the Java “config” to execute and register the return value of the methods annotated with “@Bean.” Once we have that annotation, we are going to be adding the “@LoadBalance” annotation to the “OrderApplication.java” class:
@SpringBootApplication
@EnableEurekaClient
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Configuration
class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
5. Create the controllers for each service.
5.1 Product Service
ProductController.java
package com.huaylupo.microservice.product.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/product")
public class ProductController {
private static final String LOCAL_SERVER_PORT = "local.server.port";
@Autowired
private Environment environment;
@RequestMapping(method = GET)
public ResponseEntity getProduct(){
return ResponseEntity.ok("Product Controller, Port: " + environment.getProperty(LOCAL_SERVER_PORT));
}
}
5.2 Order Service
OrderController.java
package com.huaylupo.microservice.order.controller;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/order")
public class OrderController {
private static final String LOCAL_SERVER_PORT = "local.server.port";
@Autowired
private RestTemplate restTemplate;
@Autowired
private Environment environment;
@RequestMapping(method = POST)
public ResponseEntity getOrder(){
return ResponseEntity.ok("Order Controller, Port: " + environment.getProperty(LOCAL_SERVER_PORT));
}
@RequestMapping(method = GET)
public ResponseEntity getOrderWithProducts(){
//We use the restTemplate to call another service; in this case, the product-service.
//Remember that we are using the spring.application.name we defined for the product in the
//application.properties of the product microservice.
String product = restTemplate.getForObject("http://product-service/product", String.class);
return ResponseEntity.ok("Order Controller, Port: " + environment.getProperty(LOCAL_SERVER_PORT) + " " + product );
}
}
5.3 Authentication Service
AuthenticationController.java
package com.huaylupo.microservice.authentication.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth")
public class AuthenticationController {
private static final String LOCAL_SERVER_PORT = "local.server.port";
@Autowired
private Environment environment;
public ResponseEntity authenticate(){
//Here you can implement your own user and password authentication. This method will return the JWT access token used, which we are going to be using as the JWT authentication for the gateway.
return ResponseEntity.ok("Authentication Controller, Port: " + environment.getProperty(LOCAL_SERVER_PORT));
}
}
6. Your project’s structure should look like the following screenshot:
7. Start all services, including the Eureka server.
8. Go to the Eureka server web console. Now, you can see we have all the order, product and authentication instances up and running:
Setting up a Zuul Proxy as the API Gateway
Zuul is the Spring Cloud embedded gateway service. Why do we need it? In most microservice implementations, only a few endpoints are public; the other ones are kept as private services. Zuul is going to act as an intermediate layer between the user and those public services.
1. First, we are going to create another Spring Boot application. The project structure should look like this:
2. Once we’ve created the project, we are going to make sure that we have the “spring-cloud-starter-netflix-eureka-client” and “spring-cloud-starter-netflix-zuul” dependencies. Your pom.xml should look like this:
1.8
Greenwich.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-config
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-zuul
3. The next step is to integrate the API gateway with Eureka and map the product, order and authentication services. For this specific case, we are going to be using the “application.yml” instead of the “application.properties”:
server:
port: 8762
spring:
application:
name: zuul-server
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
zuul:
#Service will be mapped under the /api URI
#prefix: /api
# The ignoredServices disable accessing services using service name
# with these they should be only accessed through the path we defined below.
ignoredServices: '*'
routes:
product-service:
path: /products/**
service-id: product-service
order-service:
path: /orders/**
service-id: order-service
auth-service:
path: /authentication/**
service-id: auth-service
strip-prefix: true
sensitive-headers: Cookie,Set-Cookie
ribbon:
ConnectTimeout: 1000
ReadTimeout: 3000
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 1100
4. Edit “Application.java” to add the “@EnableZuulProxy” and the “@EnableEurekaClient” annotation to tell our gateway application that this is the Zuul proxy and that it is a Eureka client.
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
5. Run the application, making sure that the Eureka server is already running.
6. Go to the Eureka server web console. Now, you can see we have all the order, product, authentication and Zuul gateway instances up and running:
Testing our Microservices
At this point, we have a Eureka server, three services (the order, product and the authentication), and a Zuul proxy. All of them are up and running.
To test our microservices, we have to send a request to the gateway, adding the path of the specific service. For example: http://localhost:8762/orders/order
Once you hit that specific URL using POST, you should see the following message:
Order Controller, Port: 8300
If you hit that specific URL using GET, you should see the following message:
Order Controller, Port: 8300 Product Controller, Port: 8200
Setting up Authentication with Spring Security
The idea of setting up JWT authentication in the gateway is to prevent all unauthenticated requests to specific services. There are a lot of ways to implement JWT token validators; in this example, I will be using the Amazon Cognito JWT configuration that I previously explained in my Java Integration with Amazon Cognito blog post, but you can use any other JWT configuration.
In order to achieve this, we need to follow these steps:
1. Import all the dependencies and create all of the JWT config classes and the JWT filter. Remember to ensure a single execution of our JWT filter; it must extend from “OncePerRequestFilter”.
2. Create the “WebSecurityConfigurerAdapter” with our security configurations.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableTransactionManagement
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
public WebSecurityConfiguration() {
/*
* Ignores the default configuration, useless in our case (session management, etc..)
*/
super(true);
}
/* (non-Javadoc)
* @see org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#authenticationManagerBean()
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
/*
Overloaded to expose Authenticationmanager's bean created by configure(AuthenticationManagerBuilder).
This bean is used by the AuthenticationController.
*/
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
/* the secret key used to signe the JWT token is known exclusively by the server.
With Nimbus JOSE implementation, it must be at least 256 characters longs.
*/
//In case we need to load the secret.key
httpSecurity
.csrf().disable()
// make sure we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(STATELESS)
.and()
//.addFilterAfter(corsFilter(), ExceptionTranslationFilter.class)
// handle an authorized attempts
.exceptionHandling().authenticationEntryPoint((req, rsp, e) -> rsp.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
// Add a filter to validate the tokens with every request
.addFilterAfter(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// authorization requests config
.authorizeRequests()
// allow all who are accessing "auth" service
.antMatchers("/authentication").permitAll()
// must be an admin if trying to access admin area (authentication is also required here)
// Any other request must be authenticated
.anyRequest().authenticated();
}
private com.huaylupo.microservice.gateway.filter.CorsFilter corsFilter() {
/*
CORS requests are managed only if headers Origin and Access-Control-Request-Method are available on OPTIONS requests
(this filter is simply ignored in other cases).
This filter can be used as a replacement for the @Cors annotation.
*/
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader(ORIGIN);
config.addAllowedHeader(CONTENT_TYPE);
config.addAllowedHeader(ACCEPT);
config.addAllowedHeader(AUTHORIZATION);
config.addAllowedMethod(GET);
config.addAllowedMethod(PUT);
config.addAllowedMethod(POST);
config.addAllowedMethod(OPTIONS);
config.addAllowedMethod(DELETE);
config.addAllowedMethod(PATCH);
config.setMaxAge(3600L);
source.registerCorsConfiguration("/v2/api-docs", config);
source.registerCorsConfiguration("/**", config);
return new com.huaylupo.microservice.gateway.filter.CorsFilter();
}
}
3. Add the “web ignores” to the web security to accept the authentication request without the JWT token validation. Add the following method to “WebSecurityConfigurerAdapter.java”:
@Override
public void configure(WebSecurity web) throws Exception {
// TokenAuthenticationFilter will ignore the below paths
web.ignoring().antMatchers("/authentication");
web.ignoring().antMatchers("/authentication/**");
web.ignoring().antMatchers("/v2/api-docs");
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
Remember that we set the authentication path for the authentication service on “application.yml”; that is why we are setting the path “/authentication” on the “web ignore.”
Testing our JWT Authentication
1. Hit the URL “http://localhost:8762/orders/order” using POST; you should see the following message:
{
"metadata": {
"status": "UNAUTHORIZED"
},
"errors": [
{
"message": "Invalid Action, no token found",
"code": "09",
"detail": "No token found in Http Authorization Header"
}
]
}
2. Once you have implemented the JWT token “config” classes on the gateway and the authentication service, you will get a response similar to this one:
3. Copy the access token generated by your authentication service and pass it to the order service request.
4. Execute the order request again (http://localhost:8762/orders/order), and now you should see the following message:
Order Controller, Port: 8300 Product Controller, Port: 8200
Conclusion
The main purpose of this blog post was to show the costs and benefits of creating monolithic and microservice architectures, in addition to demonstrating how we can create a microservice architecture using Spring Boot and Spring Cloud.
Spring Boot allows you to create both architectures; as we have seen in this post, creating microservices with Spring Boot is really easy, and it provides an effective way to build and maintain them. On the other hand, Spring Cloud makes managing the additional complexity of the service registries and load balancing easier to understand and perform.
In the end, choose the architecture that best suits your context and requirements. Stick to the architecture you can live with!