I'm migrating some REST applications to Spring Boot 3 and I'm trying to set an extra low cardinality key/value, based on the value of an incoming request header (X-Traffic-Type).
When doing it for webflux applications, everything works fine: I provide an extension of DefaultServerRequestObservationConvention and set the value based on the incoming header:
public class MyCustomServerRequestObservationConvention extends DefaultServerRequestObservationConvention {
#Override
public KeyValues getLowCardinalityKeyValues(final ServerRequestObservationContext context) {
return super.getLowCardinalityKeyValues(context).and(KeyValue.of(SupplyHubMetricsAutoConfiguration.TRAFFIC_TYPE_TAG, getTrafficType(context.getCarrier())));
}
private String getTrafficType(final ServerHttpRequest request) {
return Optional.ofNullable(request.getHeaders().getFirst("X-Traffic-Type"))
.map(String::toLowerCase)
.orElse("");
}
}
For the client side, I can simply get the traffic type from the parent observation on an extension of DefaultClientRequestObservationConvention:
public class MyCustomClientRequestObservationConvention extends DefaultClientRequestObservationConvention {
#Override
public KeyValues getLowCardinalityKeyValues(final ClientRequestObservationContext context) {
return KeyValues.of(method(context), status(context), exception(context), outcome(context), trafficType(context), qualifier(context));
}
protected KeyValue trafficType(ClientRequestObservationContext context) {
String trafficType = Optional.ofNullable(context.getParentObservation())
.map(ObservationView::getContextView)
.map(e -> e.getLowCardinalityKeyValue(TRAFFIC_TYPE_TAG))
.map(KeyValue::getValue)
.orElse(DEFAULT_TRAFFIC_TYPE);
return KeyValue.of(TRAFFIC_TYPE_TAG, trafficType);
}
}
Now, when I do the same for the MVC applications, I can set the value on the server observation but when I implement the client part by extending DefaultClientRequestObservationConvention the observation doesn't have a parent observation (which I'm expecting to be the server one):
public class MyCustomClientRequestObservationConvention extends DefaultClientRequestObservationConvention {
#Override
public KeyValues getLowCardinalityKeyValues(final ClientRequestObservationContext context) {
return KeyValues.of(method(context), status(context), exception(context), outcome(context), trafficType(context), qualifier());
}
protected KeyValue trafficType(ClientRequestObservationContext context) {
// context.getParentObservation() always returns null
String trafficType = Optional.ofNullable(context.getParentObservation())
.map(ObservationView::getContextView)
.map(e -> e.getLowCardinalityKeyValue(TRAFFIC_TYPE_TAG))
.map(KeyValue::getValue)
.orElse(DEFAULT_TRAFFIC_TYPE);
return KeyValue.of(TRAFFIC_TYPE_TAG, trafficType);
}
Am I doing something wrong here? what's the reason why webflux observations keep the hierarchy but mvc ones seems not to?
Related
I have an gRPC interceptor written in java
my gRPC interceptor looks like this
public class GrpcServerInterceptor implements ServerInterceptor {
#Override
public <R, T> ServerCall.Listener<R> interceptCall(ServerCall<R, T> call,
Metadata requestHeaders, ServerCallHandler<R, T> next) {
if(call == null || next == null)
return null;
if(call != null) {
String actionName = call.getMethodDescriptor().getBareMethodName();
String serviceName = call.getMethodDescriptor().getServiceName();
State.Holder.set(State.newBuilder().withControllerName(serviceName).withActionName(actionName).withFramework("grpc").build());
}
ServerCall.Listener<R> delegate = next.startCall(call, requestHeaders);
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<R>(delegate) {
#Override
public void onHalfClose() {
try {
super.onHalfClose();
} catch (Exception e) {
call.close(Status.INTERNAL
.withCause (e)
.withDescription("error message"), new Metadata());
}
}
};
}
}
I just want to unit test for above interceptor in junit.
I am facing issues around building ServerCall, Metaddata and ServerCallHandler Objects and passing them around.
I tried to create Server Call object like below in my unit test.
ServerCall serverCall = new ForwardingServerCall() {
#Override
protected ServerCall delegate() {
return null;
}
#Override
public MethodDescriptor getMethodDescriptor() {
return MethodDescriptor.newBuilder().
setType(MethodType.UNKNOWN).
setRequestMarshaller(ProtoUtils.marshaller((StudentRequest.getDefaultInstance()))).
setResponseMarshaller(ProtoUtils.marshaller(StudentResponse.getDefaultInstance())).
setFullMethodName(generateFullMethodName("com.test.cloud.sc.grpc.backend.service.StudentServiceImpl", "getStudentInfo")).
build();
}
};
But above codeblock has issues around setting Request and Response Marshaller.
How can i unit test all the scenarios for my interceptor with minimal code setup and I don't want to start grpc server at all ?
EDIT 1
How can i improve null check handling in gRPC interceptor ?
Many Thanks
Take a look at https://github.com/grpc/grpc-java/blob/master/api/src/test/java/io/grpc/ServerInterceptorsTest.java or https://github.com/grpc/grpc-java/blob/master/core/src/test/java/io/grpc/util/UtilServerInterceptorsTest.java to see how server interceptors are tested.
Regarding null, the grpc framework will not pass null in call or next so I don't see any reason to check. In any case consider using com.google.common.base.Preconditions.checkNotNull from Guava.
A JAVA project has been built with spring boot version 2.5.3.
Due to the “Spring4shell” (CVE-2022-22965) security risk, we have to take mitigation actions.
It's not possible to upgrade the Spring Boot version since several other dependencies are incompatible with the latest Spring Boot version.
So, it has been decided to apply a suggested workaround according to https://www.springcloud.io/post/2022-03/spring-framework-rce-early-announcement/#gsc.tab=0
According to the guide, there are 2 workarounds suggested.
Setting disallowedFields on WebDataBinder through an #ControllerAdvice method
#ControllerAdvice
#Order(Ordered.LOWEST_PRECEDENCE)
public class BinderControllerAdvice {
#InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
String[] denylist = new String[]{"class.*", "Class.*", "*.class.*", "*.Class.*"};
dataBinder.setDisallowedFields(denylist);
}
}
Extend RequestMappingHandlerAdapter to update the WebDataBinder:
#Bean
public WebMvcRegistrations mvcRegistrations() {
return new WebMvcRegistrations() {
#Override
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return new ExtendedRequestMappingHandlerAdapter();
}
};
}
private static class ExtendedRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
#Override
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> methods) {
return new ServletRequestDataBinderFactory(methods, getWebBindingInitializer()) {
#Override
protected ServletRequestDataBinder createBinderInstance(
Object target, String name, NativeWebRequest request) throws Exception {
ServletRequestDataBinder binder = super.createBinderInstance(target, name, request);
String[] fields = binder.getDisallowedFields();
List<String> fieldList = new ArrayList<>(fields != null ? Arrays.asList(fields) : Collections.emptyList());
fieldList.addAll(Arrays.asList("class.*", "Class.*", "*.class.*", "*.Class.*"));
binder.setDisallowedFields(fieldList.toArray(new String[] {}));
return binder;
}
};
}
}
Both of the approaches have been tried but none of them could reject the following request.
HOST:PORT/path?class.module.classLoader.URLs%5B0%5D=0
What can be the missing point of our approach?
Request will not be rejected by setting values to webDataBinder.setDisallowedFields().
WebDataBinder provides two methods setAllowedFields and setDisallowedFields to set the list of attribute names which can be and cannot be used in data binding process for model objects.
This validation will happen in org.springframework.validation.DataBinder::doBind method. inside the doBind method disallowed fields will be removed from the field list
#Component
public class WebSocketRegistration {
#Autowired
GenericWebApplicationContext context;
#Autowired
private IntegrationFlowContext flowContext;
public String registerServerEndpoint(String beanName, String endPoint) {
context.registerBean(beanName, ServerWebSocketContainer.class,
() -> new ServerWebSocketContainer(endPoint).withSockJs(),
beanDefinition -> beanDefinition.setAutowireCandidate(true));
return beanName;
}
public StandardIntegrationFlow webSocketFlow(String beanName) {
ServerWebSocketContainer serverWebSocketContainer = (ServerWebSocketContainer) context
.getBean(beanName);
WebSocketOutboundMessageHandler webSocketOutboundMessageHandler = new WebSocketOutboundMessageHandler(
serverWebSocketContainer);
StandardIntegrationFlow flow = IntegrationFlows.from("stringChannel")
.split(new AbstractMessageSplitter() {
#Override
protected Object splitMessage(Message<?> message) {
return serverWebSocketContainer
.getSessions()
.keySet()
.stream()
.map(s -> {
System.out.println(message.getPayload().toString() + " and key " + s);
return MessageBuilder.fromMessage(message)
.setHeader(SimpMessageHeaderAccessor.SESSION_ID_HEADER, s)
.build();
})
.collect(Collectors.toList());
}
})
.handle(webSocketOutboundMessageHandler).get();
String id = flowContext.registration(flow).register().getId();
System.out.println(id);
return flow;
}
}
This is sample code to register integration flow so that websocket can publish data to specified endpoint at runtime. i am not sure if spring websocket allows it but it does not throw any errors while i register a flow at runtime with different websocket urls.
The dynamic ServerWebSocketContainer bean registration is not possible at the moment.
It is fully tied with the application context initialization phase.
Even if you don't get any errors registering that bean via context.registerBean(), the HandlerMapping is not updated for new path and its mapping.
I would suggest to take a look into a generic ServerWebSocketContainer and some "selector" logic when you try to chose sessions from the container to decide to whom to send a message to.
Feel free to raise a GH issue to improve this WebSocket support in Spring Integration.
In fact we have now dynamic HTTP & WebFlux registration.
I've seen many a posts and Q&As on StackOverflow for custom error handling for a REST/MVC applications.
My situation is slightly different though. My application uses Java Messaging (with ActiveMQ and SpringIntegration) and
the method below is triggered as a response to a JMessage:
public BenefitVersion getVersionAt(LocalDate referenceDate) {
return versions.stream()
.filter(benefitVersion -> !benefitVersion.getStartDate().isAfter(referenceDate))
.reduce(maxBy(getBenefitVersionStartDateComparator()
.thenComparingLong(BenefitVersion::getId)))
.orElseThrow(() -> new IllegalBenefitVersionException(guid,
format("Benefit %s does not have active version on %s", guid, referenceDate)));
}
I've defined custom exception as below:
public class IllegalBenefitVersionException extends RuntimeException {
private UUID benefitId;
public IllegalBenefitVersionException(final UUID benefitId, final String message) {
super(message);
this.benefitId = benefitId;
}
}
And a custom handler for that exception:
#ControllerAdvice
public class IllegalBenefitVersionExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(IllegalBenefitVersionExceptionHandler.class);
private final DlqExceptionDetailService exceptionDetailService;
#Autowired
public IllegalBenefitVersionExceptionHandler(DlqExceptionDetailService exceptionDetailService) {
this.exceptionDetailService = exceptionDetailService;
}
#ExceptionHandler(IllegalBenefitVersionException.class)
public void handleException(Throwable t) {
IllegalBenefitVersionException exception = (IllegalBenefitVersionException) t;
exceptionDetailService.newWithBenefitIdAndSave(exception.getBenefitId(), t.getMessage());
LOGGER.error("From ExceptionHandler: {}", t.getMessage(), t);
}
}
But that never gets called. Is the reason because I use the #ControllerAdvice whereas there is no controller?
Or perhaps I need to add special component scan somewhere?
If so, how do I wire up a custom exception handler in a messaging-based, as opposed to, REST, application?
Background:
I am working on a java Spring REST microservice that needs to work with multiple identical back-end systems and multiple identical databases depending on the request parameters.
Basically I have 3 "brands". For each brand there is a set of downstream services and a database. I have no control over those.
My spring service will receive brand as a part of request and will need to call the right downstream services and use the correct database.
Previously I would deal with this by having a separate instance of the spring service for each of the brands. There would be a single property file for each brand and spring would use it to wire up beans. I would have separate URL's for each brand and there was no problem.
Some of my beans need to know about "brand" during creation as they are wrappers around connections downstream services. I.e. once the bean is created there won't be a way to switch it to be a "different brand".
Problem:
I would like to change this so that a single instance of my service can handle requests for any brand.
Requirements:
I was thinking about the following solution:
Have a general property file for non-branded stuff. Spring would wire any non-branded beans and keep them as singleton beans.
Have a property file with brand specific urls etc for each of the brands
Spring would create set of singleton beans for each of the brand using appropriate property file.
Next when the request comes in spring would read the request params and use bean specific for that brand.
Performance is important to me so I would like to reuse the beans as much as possible.
I would like to make this thing as transparent as possible so that people creating new beans don't have to worry about doing anything outside standard configuration/context class.
Does anyone know what would be the best solution to achieve this?
I think you can solve the problem injecting the service in every request with the right set of configurations and beans; possibly already existing in your Application Context.
Given:
$ curl http://localhost:8080/greetings/rodo && echo
Hi from brand1, rodo
$ curl -H "x-brand-name: brand1" http://localhost:8080/greetings/rodo
Hi from brand1, rodo
$ curl -H "x-brand-name: brand2" http://localhost:8080/greetings/rodo && echo
Hi from brand2, rodo
The following code would work:
-- application.yml --
brand1:
greetingPrefix: Hi from brand1,
brand2:
greetingPrefix: Hi from brand2,
-- DemoApplication.java --
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Configuration
class ServiceConfig {
#Bean
public GreetingService greetingServiceBrand1(Brand1Config config) {
return new GreetingService(config);
}
#Bean
public GreetingService greetingServiceBrand2(Brand2Config config) {
return new GreetingService(config);
}
}
#Configuration
class WebConfig implements WebMvcConfigurer {
#Autowired
private ApplicationContext applicationContext;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(greetingServiceResolver());
}
private GreetingServiceResolver greetingServiceResolver() {
GreetingService greetingServiceBrand1 = applicationContext.getBean("greetingServiceBrand1", GreetingService.class);
GreetingService greetingServiceBrand2 = applicationContext.getBean("greetingServiceBrand2", GreetingService.class);
return new GreetingServiceResolver(greetingServiceBrand1, greetingServiceBrand2);
}
}
}
#RestController
#RequestMapping("/greetings")
class GreetingController {
#GetMapping("/{name}")
public String get(GreetingService greetingService, #PathVariable String name) {
return greetingService.sayHi(name);
}
}
class GreetingServiceResolver implements HandlerMethodArgumentResolver {
private final GreetingService greetingServiceBrand1;
private final GreetingService greetingServiceBrand2;
public GreetingServiceResolver(GreetingService greetingServiceBrand1, GreetingService greetingServiceBrand2) {
this.greetingServiceBrand1 = greetingServiceBrand1;
this.greetingServiceBrand2 = greetingServiceBrand2;
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(GreetingService.class);
}
#Override
public Object resolveArgument(
MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory
) throws Exception {
String brand = nativeWebRequest.getHeader("x-brand-name");
return resolveGreetingService(brand);
}
private GreetingService resolveGreetingService(String brand) {
if ("brand2".equals(brand)) {
return greetingServiceBrand2;
}
return greetingServiceBrand1; // default
}
}
class GreetingService {
private BaseConfig config;
public GreetingService(BaseConfig config) {
this.config = config;
}
public String sayHi(String name) {
return config.getGreetingPrefix() + " " + name;
}
}
abstract class BaseConfig {
private String greetingPrefix;
public String getGreetingPrefix() {
return greetingPrefix;
}
public void setGreetingPrefix(String greetingPrefix) {
this.greetingPrefix = greetingPrefix;
}
}
#Configuration
#ConfigurationProperties("brand1")
class Brand1Config extends BaseConfig {
}
#Configuration
#ConfigurationProperties("brand2")
class Brand2Config extends BaseConfig {
}
As you can see, it's fundamental to pass the service to each controller method, write a resolver and inject the right set of dependencies depending on a parameter passed to the request, in this case via header.
Since your property files need to be declared statically anyway, you can just write all your different brand stuff in the same property file, like in a key-value format, that Spring can pick up as a list of configurations.
brandConfigs:
- brand: foo
property: foos
- brand2: bar
porperty: bars
Load all your connection beans to your downstream services on startup and just route to them according to your request param. Imo this seems to be the most straight forward and performant way. If some of these downstreams are used very rarely you can lazy load the beans on-demand, but probably this wouldn't make a sense unless you have thousands of different downstream routes.