Spring Cloud Sleuth: Propagate traceId to other spring application - java

I have some spring service that can submit some AWS batch job. This is simple spring batch job that invoke requst to external service. And i want to propagate traceId that generated in my service by including "org.springframework.cloud:spring-cloud-starter-sleuth" lib into classpath , to this job and add "TraceRestTemplateInterceptor" interceptor to external request initilaized with this traceId.
How can i do that? How can i initilaze interceptor which will put existing traceId from application parameter, environment, properties?
Or may be need to create some configuration beans?
UPDATE:
Simplified example:
#SpringBootApplication
public class DemoApplication implements CommandLineRunner {
Logger logger = LoggerFactory.getLogger(DemoApplication.class);
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
//#Autowired
//RestTemplate restTemplate;
#Override
public void run(String... args) {
logger.info("Hello, world!");
//restTemplate.getForObject("some_url", String.class);
}
}
File application.properties:
x-b3-traceId=98519d97ce87553d
File build.gradle:
dependencies {
implementation('org.springframework.cloud:spring-cloud-starter-sleuth')
}
Output:
INFO [-,,,] 15048 --- [ main] com.example.demo.DemoApplication : Hello, world!
First of all, I want to see here traceId which initilized in application.properties. Secondly, when uncomment resttemplate clause, this traceId propagated into request.
Is it possible?

Resolved this issue only by manually putting into request HEADER key "X-B3-TRACEID" with corresponding value, which is inserted by external application as system property when submits target spring boot application. And manually inserting this key in MDC. Example, this snipet from spring boot application that must get traceId and propagate:
#Bean
public void setTraceIdToMDC(#Value("${x.b3.traceid}") String traceId) {
MDC.put("x-b3-traceId", traceId);
}
#Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
#Bean
public CommandLineRunner commandLineRunnerer(RestTemplate restTemplate, #Value("${x.b3.traceid}") String traceId) {
return args -> {
MultiValueMap<String, String> header = new LinkedMultiValueMap<>();
header.add("X-B3-TRACEID", traceId);
HttpEntity httpEntity = new HttpEntity(header);
logger.info("Execute some request"); //<-- prints expected traceId
restTemplate.exchange("some_url", HttpMethod.GET, httpEntity, String.class);
};
}

You can get the bean:
#Autowired
private Tracer tracer;
And get the traceId with
tracer.getCurrentSpan().traceIdString();

Just add the dependency to the classpath and set rest template as a bean. That's enough.

As spring sleuth doesn't support Webservicetemplate by default, here is an example of how to use spring cloud sleuth with Webservicetemplate,
if service A sends a request to service B,
At first you'll send the trace id in the header of the sent request by the below code
#Service
public class WebServiceMessageCallbackImpl implements WebServiceMessageCallback {
#Autowired
private Tracer tracer;
public void doWithMessage(WebServiceMessage webServiceMessage) throws TransformerException {
Span span = tracer.currentSpan();
String traceId = span.context().traceId();
SoapMessage soapMessage = (SoapMessage) webServiceMessage;
SoapHeader header = soapMessage.getSoapHeader();
StringSource headerSource = new StringSource("<traceId>" + traceId + "</traceId>");
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(headerSource, header.getResult());
}
}
then in service B, you'll create an interceptor, then read the trace id from the header of the coming request, then put this trace id in the MDC like in the below code
#Slf4j
#Component
public class HttpInterceptor2 extends OncePerRequestFilter {
private final String traceId = "traceId";
#Autowired
private Tracer tracer;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String payload = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String traceId = traceId(payload);
MDC.put("traceId", traceId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove(traceId);
}
}
private String traceId(String payload) {
StringBuilder token = new StringBuilder();
if (payload.contains(traceId)) {
int index = payload.indexOf(traceId);
while (index < payload.length() && payload.charAt(index) != '>') {
index++;
}
index++;
for (int i = index; ; i++) {
if (payload.charAt(i) == '<') {
break;
}
token.append(payload.charAt(i));
}
}
if (token.toString().trim().isEmpty()) {
token.append(traceId());
}
return token.toString().trim();
}
private String traceId() {
Span span = tracer.currentSpan();
String traceId = span.context().traceId();
return traceId;
}
}

Related

Method Annotated with #Async with a Rest Template call in a Springboot application

I have a problem with #Async and Rest Template call; here is my Main Application class, with a task executor Bean and EnableAsync Annotation
#SpringBootApplication
#ComponentScan({"org.***"})
#EnableAspectJAutoProxy
#EnableAsync
#EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class,HibernateJpaAutoConfiguration.class})
#EnableFeignClients(basePackages = {"org.service.feign"})
public class MainApplication extends SpringBootServletInitializer {
/**
* <p>main.</p>
*
* #param args an array of {#link java.lang.String} objects.
*/
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
#Bean(name = "threadPoolTaskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix(“CustomLookup-");
executor.initialize();
return executor;
}
/**
* Configure.
*
* #param application the application
* #return the spring application builder
*/
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(MainApplication.class);
}
/**
* <p>requestContextListener.</p>
*
* #return a {#link org.springframework.web.context.request.RequestContextListener} object.
*/
#Bean
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
}
this is my my test service with Async annotation that I call in a Rest Controller:
#Service
public class TestService {
#Async("threadPoolTaskExecutor")
public ResponseEntity<Object> test()
throws Exception{
PagedRequest<SearchRequest> request = new PagedRequest<SearchRequest>();
SearchRequest filters = new Request();
filters.setCod(“abcdeg");
request.setFilters(filters);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new RestInterceptor())); // here I set a custom headers
final HttpHeaders theJsonHeader = new HttpHeaders();
theJsonHeader.setContentType(MediaType.APPLICATION_JSON);
final MultiValueMap<String, Object> theMultipartRequest = new LinkedMultiValueMap<>();
ObjectMapper objectMapper = new ObjectMapper();
String someJsonString = objectMapper.writeValueAsString(request);
theMultipartRequest.add("request", new HttpEntity<>(someJsonString, theJsonHeader));
ResponseEntity<Object> response = null;
final HttpEntity<PagedRequest<SearchRequest>> theHttpEntity = new HttpEntity<>(request, theJsonHeader);
String path = “http://...”; //url removed for privacy
response = restTemplate.postForEntity(path, theHttpEntity, Object.class);
return response;
}
}
This service return a nullPointer on the rest template; this is the stacktrace
java.lang.NullPointerException: null
at org.springframework.web.client.DefaultResponseErrorHandler.hasError(DefaultResponseErrorHandler.java:61) ~[spring-web-5.2.3.RELEASE.jar:5.2.3.RELEASE]
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:773) ~[spring-web-5.2.3.RELEASE.jar:5.2.3.RELEASE]
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:743) ~[spring-web-5.2.3.RELEASE.jar:5.2.3.RELEASE]
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:677) ~[spring-web-5.2.3.RELEASE.jar:5.2.3.RELEASE]
at org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:452) ~[spring-web-5.2.3.RELEASE.jar:5.2.3.REL
Here the code of my RestIntercept that i add in my RestTemplate
public class RestInterceptor implements ClientHttpRequestInterceptor {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes == null)
return null;
HttpServletRequest req = requestAttributes.getRequest();
if (req == null || req.getHeader(PrevConstants.USER_KEY)==null || req.getHeader(PrevConstants.JWT_HEADER_NAME)==null)
return null;
String userKey = req.getHeader(PrevConstants.USER_KEY);
String jwt = req.getHeader(PrevConstants.JWT_HEADER_NAME);
if (jwt == null) {
jwt = "custom jwt";
}
else if ( !jwt.startsWith("Bearer")) { jwt = "Bearer " + jwt; }
request.getHeaders().set(PrevConstants.USER_KEY, userKey);
request.getHeaders().set(PrevConstants.JWT_HEADER_NAME, jwt);
ClientHttpResponse response = execution.execute(request, body);
return response;
}
}
but if i remove, #EnableAsync and #Async the simple Rest Template works perfectly.
When i pass the HttpServlet request, the Eclipse Debug show this:
What's the problem? I don't know.
Thank you for the responses
RequestContextHolder holds the context for the request being handled by the current thread. When you use #Async your interceptor is called on a different thread to the one that is handling the request. As a result, RequestContextHolder.getRequestAttributes() returns null and your interceptor then returns a null response. To comply with the contract of ClientHttpRequestInterceptor#intercept it has to return a non-null value so this null response causes a failure.
If you want to use #Async, you'll have to retrieve the RequestAttributes in your REST Controller and then pass them into your TestService as a parameter to the test method. You could then create your RestInterceptor with the attributes, rather than it using RequestContextHolder to access them:
#Async("threadPoolTaskExecutor")
public ResponseEntity<Object> test(RequestAttributes requestAttributes) throws Exception {
// …
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new RestInterceptor(requestAttributes)));
// …
}

Enable logging in spring boot actuator health check API

I am using Spring boot Actuator API for my project the having a health check endpoint, and enabled it by :
management.endpoints.web.base-path=/
management.endpoints.web.path-mapping.health=healthcheck
Mentioned here
Now I want to enable log in my application log file when ever the status of this above /healthcheck fails and print the entire response from this end point.
What is the correct way to achieve this?
Best way is to extend the actuator endpoint with #EndpointWebExtension. You can do the following;
#Component
#EndpointWebExtension(endpoint = HealthEndpoint.class)
public class HealthEndpointWebExtension {
private HealthEndpoint healthEndpoint;
private HealthStatusHttpMapper statusHttpMapper;
// Constructor
#ReadOperation
public WebEndpointResponse<Health> health() {
Health health = this.healthEndpoint.health();
Integer status = this.statusHttpMapper.mapStatus(health.getStatus());
// log here depending on health status.
return new WebEndpointResponse<>(health, status);
}
}
More about actuator endpoint extending here, at 4.8. Extending Existing Endpoints
The above answers did not work for me. I implemented the below and it works. When you view [myhost:port]/actuator/health from your browser the below will execute. You can also add healthCheckLogger to your readiness/liveness probes so it executes periodically.
#Slf4j
#Component
public class HealthCheckLogger implements HealthIndicator
{
#Lazy
#Autowired
private HealthEndpoint healthEndpoint;
#Override
public Health health()
{
log.info("DB health: {}", healthEndpoint.healthForPath("db"));
log.info("DB health: {}", healthEndpoint.healthForPath("diskSpace"));
return Health.up().build();
}
}
Extending the HealthEndpoint using a EndpointWebExtension does not work with newer Spring versions. It's not allowed to override the existing (web-) extension or re-register another one.
Another solution is using a Filter. The following implementation logs if the health check fails:
public class HealthLoggingFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(HealthLoggingFilter.class);
#Override
public void init(FilterConfig filterConfig) {
// nothing to do
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ContentCachingResponseWrapper responseCacheWrapperObject = new ContentCachingResponseWrapper((HttpServletResponse) response);
chain.doFilter(request, responseCacheWrapperObject);
int status = ((HttpServletResponse) response).getStatus();
if (status >= 400) { // unhealthy
byte[] responseArray = responseCacheWrapperObject.getContentAsByteArray();
String responseStr = new String(responseArray, responseCacheWrapperObject.getCharacterEncoding());
LOG.warn("Unhealthy. Health check returned: {}", responseStr);
}
responseCacheWrapperObject.copyBodyToResponse();
}
#Override
public void destroy() {
// nothing to do
}
}
The Filter can be registered for the actuator/health route using FilterRegistrationBean:
#Bean
public FilterRegistrationBean<HealthLoggingFilter > loggingFilter(){
FilterRegistrationBean<HealthLoggingFilter > registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new HealthLoggingFilter ());
registrationBean.addUrlPatterns("/actuator/health");
return registrationBean;
}
If using Webflux this worked for me sample in Kotlin
#Component
#EndpointWebExtension(endpoint = HealthEndpoint::class)
class LoggingReactiveHealthEndpointWebExtension(
registry: ReactiveHealthContributorRegistry,
groups: HealthEndpointGroups
) : ReactiveHealthEndpointWebExtension(registry, groups) {
companion object {
private val logger = LoggerFactory.getLogger(LoggingReactiveHealthEndpointWebExtension::class.java)
}
override fun health(
apiVersion: ApiVersion?,
securityContext: SecurityContext?,
showAll: Boolean,
vararg path: String?
): Mono<WebEndpointResponse<out HealthComponent>> {
val health = super.health(apiVersion, securityContext, showAll, *path)
return health.doOnNext {
if (it.body.status == UP) {
logger.info("Health status: {}, {}", it.body.status, ObjectMapper().writeValueAsString(it.body))
}
}
}
}

Propagate HTTP header (JWT Token) over services using spring rest template

I have a microservice architecture, both of them securized by spring security an JWT tokens.
So, when I call my first microservice, I want to take the JWT token and send a request to another service using those credentials.
How can I retrieve the token and sent again to the other service?
Basically your token should be located in the header of the request, like for example: Authorization: Bearer . For getting it you can retrieve any header value by #RequestHeader() in your controller:
#GetMapping("/someMapping")
public String someMethod(#RequestHeader("Authorization") String token) {
}
Now you can place the token within the header for the following request:
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", token);
HttpEntity<RestRequest> entityReq = new HttpEntity<RestRequest>(request, headers);
Now you can pass the HttpEntity to your rest template:
template.exchange("RestSvcUrl", HttpMethod.POST, entityReq, SomeResponse.class);
Hope I could help
I've accomplished the task, creating a custom Filter
public class RequestFilter implements Filter{
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(RequestContext.REQUEST_HEADER_NAME);
if (token == null || "".equals(token)) {
throw new IllegalArgumentException("Can't retrieve JWT Token");
}
RequestContext.getContext().setToken(token);
chain.doFilter(request, response);
}
#Override
public void destroy() { }
#Override
public void init(FilterConfig arg0) throws ServletException {}
}
Then, setting in my config
#Bean
public FilterRegistrationBean getPeticionFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RequestFilter());
registration.addUrlPatterns("/*");
registration.setName("requestFilter");
return registration;
}
With that in mind, I've create another class with a ThreadLocal variable to pass the JWT token from the Controller to the Rest Templace interceptor
public class RequestContext {
public static final String REQUEST_HEADER_NAME = "Authorization";
private static final ThreadLocal<RequestContext> CONTEXT = new ThreadLocal<>();
private String token;
public static RequestContext getContext() {
RequestContext result = CONTEXT.get();
if (result == null) {
result = new RequestContext();
CONTEXT.set(result);
}
return result;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
public class RestTemplateInterceptor implements ClientHttpRequestInterceptor{
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String token = RequestContext.getContext().getToken();
request.getHeaders().add(RequestContext.REQUEST_HEADER_NAME, token);
return execution.execute(request, body);
}
}
Add interceptor to the config
#PostConstruct
public void addInterceptors() {
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
interceptors.add(new RestTemplateInterceptor());
restTemplate.setInterceptors(interceptors);
}
I think it is better to add the interceptor specifically to the RestTemplate, like this:
class RestTemplateHeaderModifierInterceptor(private val authenticationService: IAuthenticationService) : ClientHttpRequestInterceptor {
override fun intercept(request: org.springframework.http.HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse {
if (!request.headers.containsKey("Authorization")) {
// don't overwrite, just add if not there.
val jwt = authenticationService.getCurrentUser()!!.jwt
request.headers.add("Authorization", "Bearer $jwt")
}
val response = execution.execute(request, body)
return response
}
}
And add it to the RestTemplate like so:
#Bean
fun restTemplate(): RestTemplate {
val restTemplate = RestTemplate()
restTemplate.interceptors.add(RestTemplateHeaderModifierInterceptor(authenticationService)) // add interceptor to send JWT along with requests.
return restTemplate
}
That way, every time you need a RestTemplate you can just use autowiring to get it. You do need to implement the AuthenticationService still to get the token from the TokenStore, like this:
val details = SecurityContextHolder.getContext().authentication.details
if (details is OAuth2AuthenticationDetails) {
val token = tokenStore.readAccessToken(details.tokenValue)
return token.value
}
May be a little bit late but I think this is a common question, regarding
Spring Security 6.0.0 for web client there is a class called ServletBearerExchangeFilterFunction that you can use to read the token from the security context and inject it.
#Bean
public WebClient rest() {
return WebClient.builder()
.filter(new ServletBearerExchangeFilterFunction())
.build();
For RestTemplate there is no automatic way and is recommended use a filter
#Bean
RestTemplate rest() {
RestTemplate rest = new RestTemplate();
rest.getInterceptors().add((request, body, execution) -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return execution.execute(request, body);
}
if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) {
return execution.execute(request, body);
}
AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials();
request.getHeaders().setBearerAuth(token.getTokenValue());
return execution.execute(request, body);
});
return rest;
}

spring boot unit test assertion error

Working on a spring boot based Rest project I have a controller like this
which calls service and service layer call dao layer. Now I am writing unit test code for controllers. when I run this the error says
java.lang.AssertionError: expected:<201> but was:<415>
I don't know where I am doing wrong:
public class CustomerController {
private static final Logger LOGGER = LogManager.getLogger(CustomerController.class);
#Autowired
private CustomerServices customerServices;
#Autowired
private Messages MESSAGES;
#Autowired
private LMSAuthenticationService authServices;
#RequestMapping(value = "/CreateCustomer", method = RequestMethod.POST)
public Status createCustomer(#RequestBody #Valid Customer customer, BindingResult bindingResult) {
LOGGER.info("createCustomer call is initiated");
if (bindingResult.hasErrors()) {
throw new BusinessException(bindingResult);
}
Status status = new Status();
try {
int rows = customerServices.create(customer);
if (rows > 0) {
status.setCode(ErrorCodeConstant.ERROR_CODE_SUCCESS);
status.setMessage(MESSAGES.CUSTOMER_CREATED_SUCCESSFULLY);
} else {
status.setCode(ErrorCodeConstant.ERROR_CODE_FAILED);
status.setMessage(MESSAGES.CUSTOMER_CREATION_FAILED);
}
} catch (Exception e) {
LOGGER.info("Cannot Create the Customer:", e);
status.setCode(ErrorCodeConstant.ERROR_CODE_FAILED);
status.setMessage(MESSAGES.CUSTOMER_CREATION_FAILED);
}
return status;
}
}
The test for the CustomerController.
public class CustomerControllerTest extends ApplicationTest {
private static final Logger LOGGER = LogManager.getLogger(CustomerControllerTest.class);
#Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
#MockBean
private CustomerController customerController;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
Status status = new Status(200,"customer created successfully","success");
String customer = "{\"customerFullName\":\"trial8900\",\"customerPhoneNumber\": \"trial8900\", \"customerEmailID\": \"trial8900#g.com\",\"alternateNumber\": \"trial8900\",\"city\": \"trial8900\",\"address\":\"hsr\"}";
#Test
public void testCreateCustomer() throws Exception {
String URL = "http://localhost:8080/lms/customer/CreateCustomer";
Mockito.when(customerController.createCustomer(Mockito.any(Customer.class),(BindingResult) Mockito.any(Object.class))).thenReturn(status);
// execute
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post(URL)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON_UTF8)
.content(TestUtils.convertObjectToJsonBytes(customer))).andReturn();
LOGGER.info(TestUtils.convertObjectToJsonBytes(customer));
// verify
MockHttpServletResponse response = result.getResponse();
LOGGER.info(response);
int status = result.getResponse().getStatus();
LOGGER.info(status);
assertEquals(HttpStatus.CREATED.value(), status);
}
}
HTTP status 415 is "Unsupported Media Type". Your endpoint should be marked with an #Consumes (and possibly also #Produces) annotation specifying what kinds of media types it expects from the client, and what kind of media type it returns to the client.
Since I see your test code exercising your production code with MediaType.APPLICATION_JSON_UTF8, you should probably mark your endpoint as consuming and producing APPLICATION_JSON_UTF8.
Then you also need to make sure that there is nothing terribly wrong going on in your error handling, because in the process of catching the exceptions generated by your production code and generating HTTP responses, your error handling code may be generating something different, e.g. generating an error status response with a payload containing an HTML-formatted error message, which would have a content-type of "text/html", which would not be understood by your test code which expects json.
Use the below base test class for your setUp and converting json to string and string to json
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest(classes = Main.class)
#WebAppConfiguration
public abstract class BaseTest {
protected MockMvc mvc;
#Autowired
WebApplicationContext webApplicationContext;
protected void setUp() {
mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
protected String mapToJson(Object obj) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(obj);
}
protected <T> T mapFromJson(String json, Class<T> clazz)
throws JsonParseException, JsonMappingException, IOException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(json, clazz);
}
}
Also verify that your post call has happened or not check the below sample
Mockito.doNothing().when(customerServices).create(Mockito.any(Customer.class));
customerServices.create(customer);
Mockito.verify(customerServices, Mockito.times(1)).create(customer);
RequestBuilder requestBuilder = MockMvcRequestBuilders.post(URI)
.accept(MediaType.APPLICATION_JSON).content(inputInJson)
.contentType(MediaType.APPLICATION_JSON);
MvcResult mvcResult = mvc.perform(requestBuilder).andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
assertEquals(HttpStatus.OK.value(), response.getStatus());

Can Spring boot dynamically create end points based on the content of the property file?

So far I am creating end points like this:
#RequestMapping(value = "/test", method = RequestMethod.POST)
public #ResponseBody String indexPost(HttpServletRequest request, HttpServletResponse response)
throws Exception {
//Doing calculations
return "Result";
}
But I would like to reach the application.properties when the server starts, to read out the data stored like this:
methods: {
"endpointOne": "DBStoredProcedure1",
"endpointTwo": "DBStoredProcedure2"
}
So when my Spring Boot application starts, it would create all the POST endpoints based on the property file with the names of the first parameters (like "endpointOne"), and would call (and return the result of) the stored procedure which is defined as the second parameter (like "DBStoredProcedure1").
Is it possible to do?
Yes you can. A little bit differently though than you try to do it at the moment.
The best is to use a "PathVariable" - you find detailed information here:
https://spring.io/guides/tutorials/bookmarks/
http://javabeat.net/spring-mvc-requestparam-pathvariable/
Your method at the Controller class would look something like this:
#RequestMapping(value="/{endPoint}", method=RequestMethod.POST)
public String endPoint(#PathVariable String endPoint) {
//Your endPoint is now the one what the user would like to reach
//So you check if your property file contains this String - better to cache it's content
//If it does, you call the service with the regarding Stored Procedure.
String sPName = prop.getSPName(endPoint); //You need to implement it.
String answer = yourService.execute(sPName);
return answer;
}
Obviously you need to implement a method to handle those queries which are not found in the property file, but you get the idea.
You can use a wild card "/*" as the value in controller. So that all your endpoints would hit the same controller request method.
Below is the code sample.
#RequestMapping(value = "/*", method = RequestMethod.GET, headers="Accept=*/*", produces = { "application/json" })
public ResponseEntity<Object> getData(#RequestParam Map<String, String> reqParam, HttpServletRequest request)
throws WriteException, IOException{
MessageLogger.infoLog(EquityControllerImpl.class, GETQADTRSTOCKPRICELOGS,
ENTRY);
// Get Request URI
MessageLogger.infoLog("Request URI: " + request.getRequestURI());
String requestUri = request.getRequestURI();
//Read all request parameters
Map<String, String> requestParamMap = new HashMap<>();
for (Map.Entry<String, String> param: reqParam.entrySet()
) {
System.out.println("Parameter: " + param.getKey() + " ----> Value: " + param.getValue());
requestParamMap.put(param.getKey(),param.getValue());
}
}
Also you can define static swagger.json and use this in the swagger configuration.
#Configuration
#EnableSwagger2
#Import(SpringDataRestConfiguration.class)
public class SwaggerConfig {
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.finance.dataplatform.*"))
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build().apiInfo(getApiInfo());
}
private ApiInfo getApiInfo() {
return new ApiInfoBuilder().title("Investment Management").build();
}
private static Predicate<String> matchPathRegex(final String... pathRegexs) {
return new Predicate<String>() {
#Override
public boolean apply(String input) {
for (String pathRegex : pathRegexs) {
if (input.matches(pathRegex)) {
return true;
}
}
return false;
}
};
}
#Bean
WebMvcConfigurer configurer () {
return new WebMvcConfigurerAdapter() {
#Override
public void addResourceHandlers (ResourceHandlerRegistry registry) {
registry.addResourceHandler("/config/swagger.json")
.addResourceLocations("classpath:/config");
}
};
}
#Primary
#Bean
public SwaggerResourcesProvider swaggerResourcesProvider(InMemorySwaggerResourcesProvider defaultResourcesProvider) {
return () -> {
SwaggerResource wsResource = new SwaggerResource();
wsResource.setName("default");
wsResource.setSwaggerVersion("2.0");
wsResource.setLocation("/config/swagger.json");
//List<SwaggerResource> resources = new ArrayList<>(defaultResourcesProvider.get());
List<SwaggerResource> resources = new ArrayList<>();
resources.add(wsResource);
return resources;
};
}
}

Categories