Microservices Architecture With Spring Boot and Spring Cloud
Learn more about building a microservices architecture with Spring Boot and Spring Cloud.
Join the DZone community and get the full member experience.
Join For FreeDid you know that some of the largest tech companies, like Amazon and Google, are using Java to develop a microservices architecture? Many companies are building a microservices architecture to scale their people, but not their systems. If you’re also looking to do so, a good way to get started is to hire more Java developers (because there are so many of them).
Within the Java ecosystem, you’ll find some well-rooted patterns for building microservice architectures. And if you’ve developed with Spring before, then Spring Boot and Spring Cloud should feel like a nice homecoming.
In this tutorial, I will show you how to build a Java microservices architecture with Spring Boot and Spring Cloud.
Create Java Microservices With Spring Cloud and Spring Boot
In most of my tutorials, I show you how to build everything from scratch. Today, I’d like to take a different approach and step through a pre-built example with you. Hopefully, this will be a bit shorter and easier to understand.
You can start by cloning the @oktadeveloper/java-microservices-examples repository.
git clone https://github.com/oktadeveloper/java-microservices-examples.git
cd java-microservices-examples/spring-boot+cloud
In the spring-boot+cloud
directory, there are three projects:
discovery-service
: a Netflix Eureka server, used for service discovery.-
car-service
: a simple Car Service that uses Spring Data REST to serve up a REST API of cars. -
api-gateway
: an API gateway that has a/cool-cars
endpoint that talks to thecar-service
and filters out cars that aren’t cool (in my opinion, of course).
I created all of these applications using start.spring.io’s REST API and HTTPie.
http https://start.spring.io/starter.zip javaVersion==11 \
artifactId==discovery-service name==eureka-service \
dependencies==cloud-eureka-server baseDir==discovery-service | tar -xzvf -
http https://start.spring.io/starter.zip \
artifactId==car-service name==car-service baseDir==car-service \
dependencies==actuator,cloud-eureka,data-jpa,h2,data-rest,web,devtools,lombok | tar -xzvf -
http https://start.spring.io/starter.zip \
artifactId==api-gateway name==api-gateway baseDir==api-gateway \
dependencies==cloud-eureka,cloud-feign,data-rest,web,cloud-hystrix,lombok | tar -xzvf -
Spring Microservices With Java 11+
To make the discovery-service
run on Java 11, I had to add a dependency on JAXB.
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
The other two applications worked fine out-of-the-box on Java 11 with no changes in dependencies.
Java Service Discovery With Netflix Eureka
The discovery-service
is configured the same as you would most Eureka servers. It as an @EnableEurekaServer
annotation on its main class and properties that set its port and turn off discovery.
server.port=8761
eureka.client.register-with-eureka=false
The car-service
and api-gateway
projects are configured in a similar fashion. Both have a unique name defined and car-service
is configured to run on port 8090
so it doesn’t conflict with 8080
.
car-service/src/main/resources/application.properties:
server.port=8090
spring.application.name=car-service
api-gateway/src/main/resources/application.properties:
spring.application.name=api-gateway
The main class in both projects is annotated with @EnableDiscoveryClient
.
Build a Java Microservice With Spring Data REST
The car-service
provides a REST API that lets you CRUD (Create, Read, Update, and Delete) cars. It creates a default set of cars when the application loads using an ApplicationRunner
bean.
car-service/src/main/java/com/example/carservice/CarServiceApplication.java:
package com.example.carservice;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.stream.Stream;
@EnableDiscoveryClient
@SpringBootApplication
public class CarServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CarServiceApplication.class, args);
}
@Bean
ApplicationRunner init(CarRepository repository) {
return args -> {
Stream.of("Ferrari", "Jaguar", "Porsche", "Lamborghini", "Bugatti",
"AMC Gremlin", "Triumph Stag", "Ford Pinto", "Yugo GV").forEach(name -> {
repository.save(new Car(name));
});
repository.findAll().forEach(System.out::println);
};
}
}
@Data
@NoArgsConstructor
@Entity
class Car {
public Car(String name) {
this.name = name;
}
@Id
@GeneratedValue
private Long id;
@NonNull
private String name;
}
@RepositoryRestResource
interface CarRepository extends JpaRepository<Car, Long> {
}
Spring Cloud + Feign and Hystrix in an API Gateway
Feign makes writing Java HTTP clients easier. Spring Cloud makes it possible to create a Feign client with just a few lines of code. Hystrix makes it possible to add failover capabilities to your Feign clients so they’re more resilient.
The api-gateway
uses Feign and Hystrix to talk to the downstream car-service
and failover to a fallback()
method if it’s unavailable. It also exposes a /cool-cars
endpoint that filters out cars you might not want to own.
api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java:
package com.example.apigateway;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.Data;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.hateoas.Resources;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Collectors;
@EnableFeignClients
@EnableCircuitBreaker
@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
@Data
class Car {
private String name;
}
@FeignClient("car-service")
interface CarClient {
@GetMapping("/cars")
@CrossOrigin
Resources<Car> readCars();
}
@RestController
class CoolCarController {
private final CarClient carClient;
public CoolCarController(CarClient carClient) {
this.carClient = carClient;
}
private Collection<Car> fallback() {
return new ArrayList<>();
}
@GetMapping("/cool-cars")
@CrossOrigin
@HystrixCommand(fallbackMethod = "fallback")
public Collection<Car> goodCars() {
return carClient.readCars()
.getContent()
.stream()
.filter(this::isCool)
.collect(Collectors.toList());
}
private boolean isCool(Car car) {
return !car.getName().equals("AMC Gremlin") &&
!car.getName().equals("Triumph Stag") &&
!car.getName().equals("Ford Pinto") &&
!car.getName().equals("Yugo GV");
}
}
Run a Java Microservices Architecture
If you run all of these services with ./mvnw
in separate terminal windows, you can navigate to http://localhost:8761
and see they’ve registered with Eureka.
If you navigate to http://localhost:8080/cool-bars
in your browser, you’ll be redirected to Okta. What the?
Secure Java Microservices With OAuth 2.0 and OIDC
I’ve already configured security in this microservices architecture using OAuth 2.0 and OIDC. What’s the difference between the two? OIDC is an extension to OAuth 2.0 that provides identity. It also provides discovery so all the different OAuth 2.0 endpoints can be discovered from a single URL (called an issuer
).
How did I configure security for all these microservices? I’m glad you asked!
I added Okta’s Spring Boot starter to the pom.xml
in api-gateway
and car-service
:
<dependency>
<groupId>com.okta.spring</groupId>
<artifactId>okta-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
Then, I created a new OIDC app in Okta, configured with authorization code flow. You’ll need to complete the following steps if you want to see everything in action.
Create a Web Application in Okta
Log in to your Okta Developer account (or sign up if you don’t have an account).
- From the Applications page, choose Add Application.
- On the Create New Application page, select Web.
- Give your app a memorable name, add
http://localhost:8080/login/oauth2/code/okta
as a Login redirect URI, select Refresh Token (in addition to Authorization Code) and click Done.
Copy the issuer (found under API > Authorization Servers), client ID, and client secret into application.properties
for both projects.
okta.oauth2.issuer=$issuer
okta.oauth2.client-id=$clientId
okta.oauth2.client-secret=$clientSecret
The Java code in the section below already exists, but I figured I’d explain it so you know what’s going on.
Configure Spring Security for OAuth 2.0 Login and Resource Server
In ApiGatewayApplication.java
, I added Spring Security configuration to enable OAuth 2.0 login and enable the gateway as a resource server.
@Configuration
static class OktaOAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer().jwt();
// @formatter:on
}
}
The resource server configuration is not used in this example, but I added it in case you wanted to hook up a mobile app or SPA to this gateway. If you’re using a SPA, you’ll also need to add a bean to configure CORS.
@Bean
public FilterRegistrationBean<CorsFilter> simpleCorsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowedHeaders(Collections.singletonList("*"));
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
If you do use a CORS filter like this one, I recommend you change the origins, methods, and headers to be more specific, increasing security. |
The CarServiceApplication.java
is only configured as a resource server since it’s not expected to be accessed directly.
@Configuration
static class OktaOAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
// @formatter:on
}
}
To make it possible for the API gateway to access the Car Service, I created a UserFeignClientInterceptor.java
in the API gateway project.
api-gateway/src/main/java/com/example/apigateway/UserFeignClientInterceptor.java:
package com.example.apigateway;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Component;
@Component
public class UserFeignClientInterceptor implements RequestInterceptor {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_TOKEN_TYPE = "Bearer";
private final OAuth2AuthorizedClientService clientService;
public UserFeignClientInterceptor(OAuth2AuthorizedClientService clientService) {
this.clientService = clientService;
}
@Override
public void apply(RequestTemplate template) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(
oauthToken.getAuthorizedClientRegistrationId(),
oauthToken.getName());
OAuth2AccessToken accessToken = client.getAccessToken();
template.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER_TOKEN_TYPE, accessToken.getTokenValue()));
}
}
I configured it as a RequestInterceptor
in ApiGatewayApplication.java
:
@Bean
public RequestInterceptor getUserFeignClientInterceptor(OAuth2AuthorizedClientService clientService) {
return new UserFeignClientInterceptor(clientService);
}
And, I added two properties in api-gateway/src/main/resources/application.properties
so Feign is Spring Security-aware.
feign.hystrix.enabled=true
hystrix.shareSecurityContext=true
See Java Microservices Running With Security Enabled
Run all of the applications with ./mvnw
in separate terminal windows, or in your IDE if you prefer.
To make it simpler to run in an IDE, there is an aggregator pom.xml in the root directory. If you’d installed IntelliJ IDEA’s command line launcher, you just need to run idea pom.xml . |
Navigate to http://localhost:8080/cool-cars
and you’ll be redirected to Okta to log in.
Enter the username and password for your Okta developer account and you should see a list of cool cars.
If you made it this far and got the examples apps running, congratulations! You’re super cool!
Use Netflix Zuul and Spring Cloud to Proxy Routes
Another handy feature you might like in your microservices architecture is Netflix Zuul. Zuul is a gateway service that provides dynamic routing, monitoring, resiliency, and more.
To add Zuul, I added it as a dependency to api-gateway/pom.xml
:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
Then, I added @EnableZuulProxy
to the ApiGatewayApplication
class.
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {
...
}
To pass the access token to proxied routes, I created an AuthorizationHeaderFilter
class that extends ZuulFilter
.
package com.example.apigateway;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.core.Ordered;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import java.util.Optional;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
public class AuthorizationHeaderFilter extends ZuulFilter {
private final OAuth2AuthorizedClientService clientService;
public AuthorizationHeaderFilter(OAuth2AuthorizedClientService clientService) {
this.clientService = clientService;
}
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
Optional<String> authorizationHeader = getAuthorizationHeader();
authorizationHeader.ifPresent(s -> ctx.addZuulRequestHeader("Authorization", s));
return null;
}
private Optional<String> getAuthorizationHeader() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(
oauthToken.getAuthorizedClientRegistrationId(),
oauthToken.getName());
OAuth2AccessToken accessToken = client.getAccessToken();
if (accessToken == null) {
return Optional.empty();
} else {
String tokenType = accessToken.getTokenType().getValue();
String authorizationHeaderValue = String.format("%s %s", tokenType, accessToken.getTokenValue());
return Optional.of(authorizationHeaderValue);
}
}
}
You might notice that there’s code in the getAuthorizationHeader() method that’s very similar to the code that’s in UserFeignClientInterceptor . Since it’s only a few lines, I opted not to move these to a utility class. |
To make Spring Boot and Zuul aware of this filter, I registered it as a bean in the main application class.
public AuthorizationHeaderFilter authHeaderFilter(OAuth2AuthorizedClientService clientService) {
return new AuthorizationHeaderFilter(clientService);
}
To proxy requests from the API Gateway to the Car Service, I added routes to api-gateway/src/main/resources/application.properties
.
zuul.routes.car-service.path=/cars
zuul.routes.car-service.url=http://localhost:8090
zuul.routes.home.path=/home
zuul.routes.home.url=http://localhost:8090
zuul.sensitive-headers=Cookie,Set-Cookie
I added a HomeController
to the car-service
project for the /home
route.
package com.example.carservice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
@RestController
public class HomeController {
private final static Logger log = LoggerFactory.getLogger(HomeController.class);
@GetMapping("/home")
public String howdy(Principal principal) {
String username = principal.getName();
JwtAuthenticationToken token = (JwtAuthenticationToken) principal;
log.info("claims: " + token.getTokenAttributes());
return "Hello, " + username;
}
}
Confirm Your Zuul Routes Work
Since these changes are already in the project you cloned, you should be able to view https://localhost:8080/cars
and http://localhost:8080/home
in your browser.
What About Spring Cloud Config?
One of the things you might’ve noticed in this example is you had to configure the OIDC properties in each application. This could be a real pain if you had 500 microservices. Yes, you could define them as environment variables and this would solve the problem. However, if you have different microservices stacks using different OIDC client IDs, this approach will be difficult.
Spring Cloud Config is a project that provides externalized configuration for distributed systems. Rather than adding it to this example, I’ll cover it in a future tutorial.
What About Kotlin?
I wrote this post with Java because it’s the most popular language in the Java ecosystem. However, Kotlin is on the rise, according to RedMonk’s programming language rankings from January 2019.
For this quarter, at least, Kotlin grew substantially while all three of its fellow JVM-based counterparts declined. Kotlin jumped so far, in fact, that it finally broke into the Top 20 at #20 and leapfrogged Clojure (#24) and Groovy (#24) while doing so. It’s still well behind Scala (#13), but Kotlin’s growth has been second only to Swift in this history of these rankings so it will be interesting to see what lies ahead in the next run or two.
Spring has excellent support for Kotlin, and you can choose it as a language on start.spring.io. If you’d like to see us write more posts using Kotlin, please let us know in the comments!
Known Issues With Refresh Tokens
By default, Okta’s access tokens expire after one hour. This is expected, and short-lived access tokens are recommended when using OAuth 2.0. Refresh tokens typically live a lot longer — think days or months — and can be used to get new access tokens. This should happen automatically when using Okta’s Spring Boot starter, but it does not.
I configured my Okta org so its access tokens expire in five minutes. You can do this by going to API > Authorization Servers > Access Policies, click on the Default Policy, and edit its rule. Then change the access token lifetime from 1 hour to 5 minutes.
Hit http://localhost:8080/cool-cars
in your browser and you’ll be redirected to Okta to login. Log in and you should see a JSON string of cars.
Go do something else for more than 5 minutes.
Come back, refresh your browser and you’ll see []
instead of all the cars.
I’m still working on a solution to this and will update this post once I find one. If you happen to know of a solution, please let me know!
Update: Spring Security 5.1 doesn’t yet automatically refresh the OAuth access token. It should be available in Spring Security 5.2.
Have More Fun With Spring Boot, Spring Cloud, and Microservices
I hope you liked this tour of how to build Java microservice architectures with Spring Boot and Spring Cloud. You learned how to build everything with minimal code, then configure it to be secure with Spring Security, OAuth 2.0, and Okta.
You can find all the code shown in this tutorial on GitHub.
Java Microservices with Spring Boot and Spring Cloud was originally published on the Okta Developer Blog on May 22, 2019.
Published at DZone with permission of Matt Raible, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments