Spring Cloud APIGW and Open API Specification Integration issues - spring

I am looking to develop a Spring Cloud APIGW and Open API Specification Integration example. I am following: https://piotrminkowski.com/2018/04/26/quick-guide-to-microservices-with-spring-boot-2-0-eureka-and-spring-cloud/, but use all latest dependency of Spring Boot which is 2.7.0
http://localhost:8011/swagger-ui.html is not working
gateway.properties
server.port=8011
spring.application.name=api-gateway
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka
spring.config.import=configserver:http://localhost:8012/
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
# Actuator
# https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.enabling
management.endpoint.gateway.enabled=true
management.endpoints.web.exposure.include=beans, health, metrics, mappings, env, info,configprops, caches, gateway
# All Microservices Routes
spring.cloud.gateway.routes[0].id=organization-service
spring.cloud.gateway.routes[0].uri=lb://organization-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/organization/**
spring.cloud.gateway.routes[0].predicates[1]=Method=GET,POST,PUT,DELETE
spring.cloud.gateway.routes[1].id=department-service
spring.cloud.gateway.routes[1].uri=lb://department-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/department/**
spring.cloud.gateway.routes[1].predicates[1]=Method=GET,POST,PUT,DELETE
spring.cloud.gateway.routes[2].id=employee-service
spring.cloud.gateway.routes[2].uri=lb://employee-service
spring.cloud.gateway.routes[2].predicates[0]=Path=/employee/**
spring.cloud.gateway.routes[2].predicates[1]=Method=GET,POST,PUT,DELETE
spring.cloud.gateway.routes[3].id=openapi
spring.cloud.gateway.routes[3].uri=http://localhost:${server.port}
spring.cloud.gateway.routes[3].predicates[0]=Path=/v3/api-docs/**
spring.cloud.gateway.routes[3].predicates[1]=Method=GET,POST,PUT,DELETE
spring.cloud.gateway.routes[4].id=openapi2
spring.cloud.gateway.routes[4].uri=http://localhost:${server.port}
spring.cloud.gateway.routes[4].predicates[0]=Path=/webjars/**
spring.cloud.gateway.routes[4].predicates[1]=Method=GET,POST,PUT,DELETE
# Open API Specification
springdoc.swagger-ui.urls[0].name=employee
springdoc.swagger-ui.urls[0].url=/v3/api-docs/employee
springdoc.swagger-ui.urls[1].name=department
springdoc.swagger-ui.urls[1].url=/v3/api-docs/department
springdoc.swagger-ui.urls[2].name=organization
springdoc.swagger-ui.urls[2].url=/v3/api-docs/organization
server.max-http-header-size=2000000
MainApp.java
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
import org.springdoc.core.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
#EnableEurekaClient
#SpringBootApplication
public class GatewayServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceApplication.class, args);
}
#Autowired
RouteDefinitionLocator locator;
#Bean
public List<GroupedOpenApi> apis() {
List<GroupedOpenApi> groups = new ArrayList<>();
List<RouteDefinition> definitions = locator.getRouteDefinitions().collectList().block();
assert definitions != null;
definitions.stream().filter(routeDefinition -> routeDefinition.getId().matches(".*-service")).forEach(routeDefinition -> {
String name = routeDefinition.getId().replaceAll("-service", "");
groups.add(GroupedOpenApi.builder().pathsToMatch("/" + name + "/**").group(name).build());
});
return groups;
}
}
Note: I am showing for 1 microservice, I've done almost same for others
application.properties
server.port=${PORT:0}
spring.application.name=employee-service
spring.config.import=configserver:http://localhost:8012/
eureka.client.fetch-registry=true
eureka.client.register-with-eureka=true
springdoc.packages-to-scan=com.example.demo
and Added below to entry service main app.
MainApp.java
#OpenAPIDefinition(info = #Info(title = "Employee API", version = "1.0", description = "Documentation Employee API v1.0"))
#EnableEurekaClient
#SpringBootApplication
public class EmployeeServiceApplication {
public static void main(String[] args) {
SpringApplication.run(EmployeeServiceApplication.class, args);
}
}

Related

Debug Spring Boot application in VSC

I have one simple Spring boot app that runs normally in console.
But I can't debug it in VSC.
The code is:
package com.sample.service.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
/**
* sample service
*
*/
#EnableDiscoveryClient
#SpringBootApplication
#EnableBinding(Source.class)
#EnableMongoRepositories(basePackages = "com.sample.service.sample.repositories")
public class ServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceApplication.class, args);
}
}
The exception is:
nested exception is java.lang.NoClassDefFoundError: org/objenesis/strategy/InstantiatorStrategy
What did I miss in VSC setting for Spring Boot?

How to provide custom SSLContext to Netty server on spring boot

How can we configure a custom SSLContext to a spring boot application with Netty server?
From the source code, I see 'reactor.ipc.netty.http.server.HttpServerOptions' which are some server startup options, but I don't find a way to configure them.
Is there any handler through which we can inject our custom SSLContext?
I am looking something similar to this (Spring 5 WebClient using ssl) where WebClient is configured with a custom SSLContext through 'reactor.ipc.netty.http.client.HttpClientOptions'.
Netty can be customized like blow example in spring-boot 2.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
/**
* author : Mohammad Ghoreishi
*/
#Configuration
#ImportResource({"classpath:convert-iban-service.xml", "classpath:config-loader-context.xml", "classpath*:error-resolver.xml"})
#EnableAutoConfiguration
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
#Bean
public WebServerFactoryCustomizer<NettyReactiveWebServerFactory> customizer(){
return new WebServerFactoryCustomizer<NettyReactiveWebServerFactory>() {
#Override
public void customize(NettyReactiveWebServerFactory factory) {
Ssl ssl = new Ssl();
// Your SSL Cusomizations
ssl.setEnabled(true);
ssl.setKeyStore("/path/to/keystore/keystore.jks");
ssl.setKeyAlias("alias");
ssl.setKeyPassword("password");
factory.setSsl(ssl);
factory.addErrorPages(new ErrorPage("/errorPage"));
}
};
}
}

Two Spring Boot projects both with #SpringBootApplication

I have a data project and UI project. Both projects are Spring Boot applications. Both projects have the same root package (com.myorg) with a main class annotated with #SpringBootApplication.
Data project's main class is:
package com.myorg;
#SpringBootApplication
public class DataApplication {
public static void main(String[] args) {
SpringApplication.run(DataApplication.class, args);
}
}
The UI project's main class is:
package com.myorg;
#SpringBootApplication
public class UiApplication {
public static void main(String[] args) {
SpringApplication.run(UiApplication .class, args);
}
}
The UI project depends on the data project via the following Gradle dependency:
dependencies {
compile('com.myorg:data:1.0')
}
If I run the UI application, it runs without issue. However, if I run an integration test within the UI application such as follows:
package com.myorg
#RunWith(SpringRunner.class)
#SpringBootTest
public class UiIntTest {
#Test
public void contextLoads() {
}
}
The following initialization error occurs:
java.lang.IllegalStateException: Found multiple #SpringBootConfiguration annotated classes
In the data project's main class, if I replace #SpringBootApplication with
#Configuration
#EnableAutoConfiguration
#ComponentScan({ "com.myorg" })
I get the following initialization error when trying to run its integration tests:
java.lang.IllegalStateException: Unable to find a #SpringBootConfiguration, you need to use #ContextConfiguration or #SpringBootTest(classes=...) with your test
For example, if I try to run:
package com.myorg
#RunWith(SpringRunner.class)
#SpringBootTest
public class DataIntTest {
#Test
public void contextLoads() {
}
}
How can I properly configure the data and UI projects?
You need to specify which Spring Boot Main class to use along with #SpringBootTest:
#SpringBootTest(classes = YourUiSpringBootApp.class)
You shouldn't have two SpringApplication annotations in the same package.
Package one.
twoapps.one;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication(scanBasePackageClasses = {One.class})
#EnableAutoConfiguration
public class One extends SpringApplication {
}
Package two.
twoapps.two;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication(scanBasePackageClasses = {Two.class})
#EnableAutoConfiguration
public class Two extends SpringApplication {
}
Root package and launcher
package twoapps;
import org.springframework.boot.SpringApplication;
import twoapps.one.One;
import twoapps.two.Two;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Application {
public static void main(String[] args) throws Exception {
new Thread(() -> SpringApplication.run(One.class, args(args, "--spring.profiles.active=one"))).start();
new Thread(() -> SpringApplication.run(Two.class, args(args, "--spring.profiles.active=two"))).start();
}
private static String[] args(String[] args, String s) {
List<String> collect = Arrays.stream(args).collect(Collectors.toList());
collect.add(s);
String[] strings = collect.toArray(new String[]{});
return strings;
}
}
This is a terrible idea. please don't do it. It is much better to have two different projects and a common project.

Hystrix Dashboard Issue in Spring Boot

I am new to Hystrix Dashboard. I have written sample application with Hystrix.
I want to see the Hystrix chart (command metric stream). But I am getting the below error:
Circuit: Unable to connect to Command Metric Stream
Thread Pools: Loading...
I am using STS with Maven.
Below is the code used:
Simple server microservice application (Spring boot web running in port 8085)
package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
#RestController
#SpringBootApplication
public class BookstoreApplication {
#RequestMapping(value = "/recommended")
public String readingList(){
return "Spring in Action (Manning), Cloud Native Java (O'Reilly), Learning Spring Boot (Packt)";
}
public static void main(String[] args) {
SpringApplication.run(BookstoreApplication.class, args);
}
}
Simple client microservice application (Spring boot web running in port 8095) I have included the dependency of Hystrix and Hystrix Dashboard along with Web, so all the Hystrix dependencies are in classpath
package hello;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
#Service
public class BookService {
private final RestTemplate restTemplate;
public BookService(RestTemplate rest) {
this.restTemplate = rest;
}
#HystrixCommand(fallbackMethod = "reliable")
public String readingList() {
URI uri = URI.create("http://localhost:8090/recommended");
return this.restTemplate.getForObject(uri, String.class);
}
public String reliable() {
return "Cloud Native Java (O'Reilly)";
}
}
package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.web.client.RestTemplate;
#EnableHystrixDashboard
#EnableHystrix
#EnableCircuitBreaker
#RestController
#SpringBootApplication
public class ReadingApplication {
#Autowired
private BookService bookService;
#Bean
public RestTemplate rest(RestTemplateBuilder builder) {
return builder.build();
}
#RequestMapping("/to-read")
public String toRead() {
return bookService.readingList();
}
public static void main(String[] args) {
SpringApplication.run(ReadingApplication.class, args);
}
}
By running the above code, the hystrix is working fine, when the BooKStoreApplication is down, it is going to fallback method.
Both the urls are working fine.
Normal Case:
http://localhost:8085/recommended
Output: Spring in Action (Manning), Cloud Native Java (O'Reilly), Learning Spring Boot (Packt)
http://localhost:8095/to-read
Output: Spring in Action (Manning), Cloud Native Java (O'Reilly), Learning Spring Boot (Packt)
When BookStoreApplication is down (http://localhost:8085/recommended) accessing http://localhost:8095/to-read returns "Cloud Native Java (O'Reilly)" as expected.
But when I tried to invoke this url http://localhost:8095/hystrix, I am getting the Hystrix DashBoard Page and asking for the stream value.
I have tried given http://localhost:8095/ or http://localhost:8095/to-read, and clicked "Monitor Stream" and it is going to next page with error:
Circuit: Unable to connect to Command Metric Stream
Thread Pools: Loading...
I've experienced the same. The main problem was, that I didn't have the actuator dependency in my maven pom. So I could not get the hystrix stream.
Include the spring-boot-actuator.
Check if localhost:8085/health is running.
Try to enter localhost:8085/hystrix.stream to stream value in Hystrix Dashboard.
Execute the service few times -> the dashboard should show the monitored method/command.

Exporting Spring Boot Actuator Metrics (& Dropwizard Metrics) to Statsd

I'm trying to export all of the metrics which are visible at the endpoint /metrics to a StatsdMetricWriter.
I've got the following configuration class so far:
package com.tonyghita.metricsdriven.service.config;
import com.codahale.metrics.MetricRegistry;
import com.ryantenney.metrics.spring.config.annotation.EnableMetrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.ExportMetricReader;
import org.springframework.boot.actuate.autoconfigure.ExportMetricWriter;
import org.springframework.boot.actuate.metrics.reader.MetricReader;
import org.springframework.boot.actuate.metrics.reader.MetricRegistryMetricReader;
import org.springframework.boot.actuate.metrics.statsd.StatsdMetricWriter;
import org.springframework.boot.actuate.metrics.writer.MetricWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
#EnableMetrics(proxyTargetClass = true)
public class MetricsConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(MetricsConfig.class);
#Value("${statsd.host:localhost}")
private String host = "localhost";
#Value("${statsd.port:8125}")
private int port;
#Autowired
private MetricRegistry metricRegistry;
#Bean
#ExportMetricReader
public MetricReader metricReader() {
return new MetricRegistryMetricReader(metricRegistry);
}
#Bean
#ExportMetricWriter
public MetricWriter metricWriter() {
LOGGER.info("Configuring StatsdMetricWriter to export to {}:{}", host, port);
return new StatsdMetricWriter(host, port);
}
}
Which writes all of the metrics which I've added to Statsd, but I'd like to also send the system/JVM metrics that are visible on the /metrics endpoint.
What am I missing?
I had the same problem and found a solution here: https://github.com/tzolov/export-metrics-example
Just add a MetricsEndpointMetricReader to your config and everything available at th e/metrics endpoint will be published to the StatsdMetricWriter.
Here is a complete example config for spring boot 1.3.x and dropwizard metrics-jvm 3.1.x:
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
import com.codahale.metrics.jvm.ThreadStatesGaugeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.ExportMetricWriter;
import org.springframework.boot.actuate.endpoint.MetricsEndpoint;
import org.springframework.boot.actuate.endpoint.MetricsEndpointMetricReader;
import org.springframework.boot.actuate.metrics.Metric;
import org.springframework.boot.actuate.metrics.statsd.StatsdMetricWriter;
import org.springframework.boot.actuate.metrics.writer.Delta;
import org.springframework.boot.actuate.metrics.writer.MetricWriter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class MetricsConfiguration {
#Bean
public MetricRegistry metricRegistry() {
final MetricRegistry metricRegistry = new MetricRegistry();
metricRegistry.register("jvm.memory",new MemoryUsageGaugeSet());
metricRegistry.register("jvm.thread-states",new ThreadStatesGaugeSet());
metricRegistry.register("jvm.garbage-collector",new GarbageCollectorMetricSet());
return metricRegistry;
}
/*
* Reading all metrics that appear on the /metrics endpoint to expose them to metrics writer beans.
*/
#Bean
public MetricsEndpointMetricReader metricsEndpointMetricReader(final MetricsEndpoint metricsEndpoint) {
return new MetricsEndpointMetricReader(metricsEndpoint);
}
#Bean
#ConditionalOnProperty(prefix = "statsd", name = {"prefix", "host", "port"})
#ExportMetricWriter
public MetricWriter statsdMetricWriter(#Value("${statsd.prefix}") String statsdPrefix,
#Value("${statsd.host}") String statsdHost,
#Value("${statsd.port}") int statsdPort) {
return new StatsdMetricWriter(statsdPrefix, statsdHost, statsdPort);
}
}
From what I've seen in spring-boot code, only calls to CounterService and GaugeService implementations are forwarded to dropwizard's MetricRegistry.
Therefore, as you already observed, only counter.* and gauge.* metrics from the /metrics endpoint will end up in Statsd.
System and JVM metrics are exposed through custom SystemPublicMetrics class, which doesn't use counter or gauge service.
I'm not sure if there is a simpler solution (maybe someone from Spring team will comment), but one way to do it (not spring-boot specific) would be to use a scheduled task that periodically writes system stats to the MetricRegistry.
To register JVM metrics you can use the JVM related MetricSets supplied by codehale.metrics.jvm library. You can just add the whole set without supplying whether they are gauges or counters.
Here is my example code where I am registering jvm related metrics:
#Configuration
#EnableMetrics(proxyTargetClass = true)
public class MetricsConfig {
#Autowired
private StatsdProperties statsdProperties;
#Autowired
private MetricsEndpoint metricsEndpoint;
#Autowired
private DataSourcePublicMetrics dataSourcePublicMetrics;
#Bean
#ExportMetricReader
public MetricReader metricReader() {
return new MetricRegistryMetricReader(metricRegistry());
}
public MetricRegistry metricRegistry() {
final MetricRegistry metricRegistry = new MetricRegistry();
//jvm metrics
metricRegistry.register("jvm.gc",new GarbageCollectorMetricSet());
metricRegistry.register("jvm.mem",new MemoryUsageGaugeSet());
metricRegistry.register("jvm.thread-states",new ThreadStatesGaugeSet());
return metricRegistry;
}
#Bean
#ConditionalOnProperty(prefix = "metrics.writer.statsd", name = {"host", "port"})
#ExportMetricWriter
public MetricWriter statsdMetricWriter() {
return new StatsdMetricWriter(
statsdProperties.getPrefix(),
statsdProperties.getHost(),
statsdProperties.getPort()
);
}
}
Note: I am using spring boot version 1.3.0.M4
Enjoy! (see the public metrics logged in console as dropwizard metrics)
#Configuration
#EnableMetrics
#EnableScheduling
public class MetricsReporter extends MetricsConfigurerAdapter {
#Autowired private SystemPublicMetrics systemPublicMetrics;
private MetricRegistry metricRegistry;
#Scheduled(fixedDelay = 5000)
void exportPublicMetrics() {
for (Metric<?> metric : systemPublicMetrics.metrics()) {
Counter counter = metricRegistry.counter(metric.getName());
counter.dec(counter.getCount());
counter.inc(Double.valueOf(metric.getValue().toString()).longValue());
}
}
#Override
public void configureReporters(MetricRegistry metricRegistry) {
this.metricRegistry = metricRegistry;
ConsoleReporter.forRegistry(metricRegistry).build().start(10, TimeUnit.SECONDS);
}
}

Resources