How to get StateContext in StateMachineListener and how to config states to implement my statechart? - spring

My First Question:
In my StateMachineConfiguration.class.
#Bean
public StateMachineListener<CompanyStatus, CompanyEvents> listener() {
return new StateMachineListenerAdapter<CompanyStatus, CompanyEvents>() {
#Override
public void transition(Transition<CompanyStatus, CompanyEvents> transition) {
if(transition.getTarget().getId() == CompanyStatus.COMPANY_CREATED) {
logger.info("公司创建,发送消息到用户服务和菜单服务");
// how to get stateContext in there?
StateContext stateContext;
Message message = new Message.Builder<String>().messageType(CompanyStatus.COMPANY_CREATED.toString()).build();
messageSender.sendToUaa(message);
messageSender.sendToRes(message);
}
}
};
}
In my service.
log.debug("Request to save Company : {}", companyDTO);
Company company = companyMapper.toCmpy(companyDTO);
company = companyRepository.save(company);
stateMachine.sendEvent(MessageBuilder
.withPayload(CompanyEvents.COMPANY_CREATE)
.setHeader("companyId", company.getId())
.build());
return companyMapper.toCmpyDTO(company);
How I can get message header[companyId] in listener?
My Second Question:
statechart

In StateMachineListener you could use its stateContext method which gives you access to StateContext. StateContext then have access to message headers via its getMessageHeaders.
Original listener interface didn't expose that much so we had to add new method which exposes context which were introduced to machine later than listener interface were created. This because we need not to break things and we generally like to be backward compatibility.

Related

Set permissions/authentication for spring-cloud-stream message consumer so it passes #PreAuthorize checks

I consume messages from spring-cloud-stream through a Consumer<MyMessage> Implementation. As part of the message handling I need to access methods that are protected with #PreAuthorize security-checks. By default the Consumer run unauthenticated so message-handling fails.
Consumer:
#Bean
public Consumer<MyMessage> exampleMessageConsumer(MyMessageConsumer consumer) {
return consumer::handleMessage;
}
Secured Method:
#PreAuthorize("hasAuthority('ROLE_ADMIN') or hasAuthority('ROLE_USER')")
public void doSomething() { ... }
I dont just want to bypass security, so what is the easiest way to authenticate my Consumer so it passes the check?
EDIT: we are using google pubsub as a binder
For the Kafka binder:
Add an #EventListener to listen for ConsumerStartedEvents; you can then add the authentication to the security context via the SecurityContextHolder; this binds it to the thread; the same thread is used to call the listener.
I found two possible solutions to my problem
use springs RunAs support (baeldung) to add permissions to a security context for a specific method. If i do this i need to add ROLE_RUN_AS_USER to my secured methods. At scale this would complicated annotations a lot.
Manually change the security context before executing the handler method and return it to its original state afterwards.
I went with the second option. I would have liked a transparent solution but there does not appear to be one.
To make this work i created a class that wraps a functional interface with the changing code and returns it.
public class RunAs {
#FunctionalInterface
public interface RunAsMethod {
void runWithException() throws Throwable;
}
public static <T> Consumer<T> createWriteConsumer(Consumer<T> originalConsumer) {
return message -> runWithWritePermission(() -> originalConsumer.accept(message));
}
public static void runWithWritePermission(final RunAsMethod func) {
final Authentication originalAuthentication = SecurityContextHolder.getContext().getAuthentication();
final AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(
"system",
originalAuthentication != null ? originalAuthentication.getPrincipal() : "system",
AuthorityUtils.createAuthorityList("ROLE_ADMIN", "SCOPE_write")
);
SecurityContextHolder.getContext().setAuthentication(token);
try {
func.runWithException();
} catch (Throwable e) {
throw new RuntimeException("exception during method with altered permissions", e);
} finally {
SecurityContextHolder.getContext().setAuthentication(originalAuthentication);
}
}
}

Webflux Controller 'return Object instead of Mono'

Hello I am new to Webflux I follow a tutorial for building reactive microservices. In my project I faced the following problem.
I want to create a crud api for the product service and the following is the Create method
#Override
public Product createProduct(Product product) {
Optional<ProductEntity> productEntity = Optional.ofNullable(repository.findByProductId(product.getProductId()).block());
productEntity.ifPresent((prod -> {
throw new InvalidInputException("Duplicate key, Product Id: " + product.getProductId());
}));
ProductEntity entity = mapper.apiToEntity(product);
Mono<Product> newProduct = repository.save(entity)
.log()
.map(mapper::entityToApi);
return newProduct.block();
}
The problem is that when I call this method from postman I get the error
"block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3" but when I use a StreamListener this call works ok. The stream Listener gets events from a rabbit-mq channel
StreamListener
#EnableBinding(Sink.class)
public class MessageProcessor {
private final ProductService productService;
public MessageProcessor(ProductService productService) {
this.productService = productService;
}
#StreamListener(target = Sink.INPUT)
public void process(Event<Integer, Product> event) {
switch (event.getEventType()) {
case CREATE:
Product product = event.getData();
LOG.info("Create product with ID: {}", product.getProductId());
productService.createProduct(product);
break;
default:
String errorMessage = "Incorrect event type: " + event.getEventType() + ", expected a CREATE or DELETE event";
LOG.warn(errorMessage);
throw new EventProcessingException(errorMessage);
}
}
}
I Have two questions.
Why this works with The StreamListener and not with a simple request?
Is there a proper way in webflux to return the object of the Mono or we always have to return a Mono?
Your create method would want to look more like this and you would want to return a Mono<Product> from your controller rather than the object alone.
public Mono<Product> createProduct(Product product) {
return repository.findByProductId(product.getProductId())
.switchIfEmpty(Mono.just(mapper.apiToEntity(product)))
.flatMap(repository::save)
.map(mapper::entityToApi);
}
As #Thomas commented you are breaking some of the fundamentals of reactive coding and not getting the benefits by using block() and should read up on it more. For example the reactive mongo repository you are using will be returning a Mono which has its own methods for handling if it is empty without needing to use an Optional as shown above.
EDIT to map to error if entity already exists otherwise save
public Mono<Product> createProduct(Product product) {
return repository.findByProductId(product.getProductId())
.hasElement()
.filter(exists -> exists)
.flatMap(exists -> Mono.error(new Exception("my exception")))
.then(Mono.just(mapper.apiToEntity(product)))
.flatMap(repository::save)
.map(mapper::entityToApi);
}

Micrometer filter is ignored with CompositeMeterRegistry

I use Spring Boot 2.1.2.RELEASE, and I try to use Micrometer with CompositeMeterRegistry. My goal is to publish some selected meters to ElasticSearch. The code below shows my sample config. The problem is, that the filter is completely ignored (so all metrics are sent to ElasticSearch), although I can see in the logs that it was processed ("filter reply of meter ..." lines).
Strangely, if I define the MeterFilter as a Spring bean, then it's applied to ALL registries (however, I want it to be applied only on "elasticMeterRegistry").
Here is a sample configuration class:
#Configuration
public class AppConfiguration {
#Bean
public ElasticConfig elasticConfig() {
return new ElasticConfig() {
#Override
#Nullable
public String get(final String k) {
return null;
}
};
}
#Bean
public MeterRegistry meterRegistry(final ElasticConfig elasticConfig) {
final CompositeMeterRegistry registry = new CompositeMeterRegistry();
registry.add(new SimpleMeterRegistry());
registry.add(new JmxMeterRegistry(new JmxConfig() {
#Override
public Duration step() {
return Duration.ofSeconds(10);
}
#Override
#Nullable
public String get(String k) {
return null;
}
}, Clock.SYSTEM));
final ElasticMeterRegistry elasticMeterRegistry = new ElasticMeterRegistry(elasticConfig, Clock.SYSTEM);
elasticMeterRegistry.config().meterFilter(new MeterFilter() {
#Override
public MeterFilterReply accept(Meter.Id id) {
final MeterFilterReply reply =
id.getName().startsWith("logback")
? MeterFilterReply.NEUTRAL
: MeterFilterReply.DENY;
log.info("filter reply of meter {}: {}", id.getName(), reply);
return reply;
}
});
registry.add(elasticMeterRegistry);
return registry;
}
}
So, I expect ElasticSearch to receive only "logback" metrics, and JMX to receive all metrics.
UPDATE:
I have played with filters and found a "solution", but I don't really understand why the code above doesn't work.
This works:
elasticMeterRegistry.config().meterFilter(new MeterFilter() {
#Override
public MeterFilterReply accept(Meter.Id id) {
final MeterFilterReply reply =
id.getName().startsWith("logback")
? MeterFilterReply.ACCEPT
: MeterFilterReply.DENY;
log.info("filter reply of meter {}: {}", id.getName(), reply);
return reply;
}
});
The difference is: I return ACCEPT instead of NEUTRAL.
Strangely, the following code does not work (ES gets all metrics):
elasticMeterRegistry.config().meterFilter(
MeterFilter.accept(id -> id.getName().startsWith("logback")));
But this works:
elasticMeterRegistry.config().meterFilter(
MeterFilter.accept(id -> id.getName().startsWith("logback")));
elasticMeterRegistry.config().meterFilter(
MeterFilter.deny());
CONCLUSION:
So, it seems that instead of NEUTRAL, the filter should return ACCEPT. But for meters not starting with "logback", my original filter (with NEUTRAL) returns DENY. Then why are those metrics published to ElasticSearch registry?
Can someone explain this?
This is really a composite of questions. I'll just point out a few points.
For the MeterRegistry bean you defined, Spring Boot will auto-configure an ElasticMeterRegistry bean as there's no ElasticMeterRegistry bean. Instead of creating a CompositeMeterRegistry bean on your own, just define a custom ElasticMeterRegistry bean which is applied the MeterFilter you want and let Spring Boot create one (CompositeMeterRegistry bean) for you.
For MeterFilterReply, ACCEPT will accept the meter immediately, DENY will deny the meter immediately, and NEUTRAL will postpone the decision to next filter(s). Basically meters will be accepted unless there's any DENY.

How to mimic SimpMessagingTemplate.convertAndSendToUser using RabbitTemplate?

So I've been reading about Spring Message Relay (Spring Messaging stuff) capability with a RabbitMQ broker. What I want to achieve is as follows:
Have a service (1), which acts as a message relay between rabbitmq and a browser. This works fine now. I'm using MessageBrokerRegistry.enableStompBrokerRelay to do that.
Have another service (2) on the back-end, which will send a message to a known queue onto RabbitMQ and have that message routed to a specific user. As a sender, I want to have a control over who the message gets delivered to.
Normally, you'd use SimpMessagingTemplate to do that. Problem is though, that the origin of the message doesn't actually have access to that template, as it's not acting as a relay, it's not using websockets and it doesn't hold mapping of queue names to session ids.
One way I could think of doing it, is writing a simple class on the service 1, which will listen on all queues and forward them using simp template. I fell however this is not an ideal way to do it, and I feel like there might be already a way to do it using Spring.
Can you please advise?
This question got me thinking about the same dilemma I was facing. I have started playing with a custom UserDestinationResolver that arrives at a consistent topic naming scheme that uses just the username and not the session ID used by the default resolver.
That lets me subscribe in JS to "/user/exchange/amq.direct/current-time" but send via a vanilla RabbitMQ application to "/exchange/amqp.direct/users.me.current-time" (to a user named "me").
The latest source code is here and I am "registering" it as a #Bean in an existing #Configuration class that I had.
Here's the custom UserDestinationResolver itself:
public class ConsistentUserDestinationResolver implements UserDestinationResolver {
private static final Pattern USER_DEST_PREFIXING_PATTERN =
Pattern.compile("/user/(?<name>.+?)/(?<routing>.+)/(?<dest>.+?)");
private static final Pattern USER_AUTHENTICATED_PATTERN =
Pattern.compile("/user/(?<routing>.*)/(?<dest>.+?)");
#Override
public UserDestinationResult resolveDestination(Message<?> message) {
SimpMessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, SimpMessageHeaderAccessor.class);
final String destination = accessor.getDestination();
final String authUser = accessor.getUser() != null ? accessor.getUser().getName() : null;
if (destination != null) {
if (SimpMessageType.SUBSCRIBE.equals(accessor.getMessageType()) ||
SimpMessageType.UNSUBSCRIBE.equals(accessor.getMessageType())) {
if (authUser != null) {
final Matcher authMatcher = USER_AUTHENTICATED_PATTERN.matcher(destination);
if (authMatcher.matches()) {
String result = String.format("/%s/users.%s.%s",
authMatcher.group("routing"), authUser, authMatcher.group("dest"));
UserDestinationResult userDestinationResult =
new UserDestinationResult(destination, Collections.singleton(result), result, authUser);
return userDestinationResult;
}
}
}
else if (accessor.getMessageType().equals(SimpMessageType.MESSAGE)) {
final Matcher prefixMatcher = USER_DEST_PREFIXING_PATTERN.matcher(destination);
if (prefixMatcher.matches()) {
String user = prefixMatcher.group("name");
String result = String.format("/%s/users.%s.%s",
prefixMatcher.group("routing"), user, prefixMatcher.group("dest"));
UserDestinationResult userDestinationResult =
new UserDestinationResult(destination, Collections.singleton(result), result, user);
return userDestinationResult;
}
}
}
return null;
}
}

Spring WS (DefaultWsdl11Definition) HTTP status code with void

We have a (working) SOAP web service based on Spring WS with DefaultWsdl11Definition.
This is basically what it looks like:
#Endpoint("name")
public class OurEndpoint {
#PayloadRoot(namespace = "somenamespace", localPart = "localpart")
public void onMessage(#RequestPayload SomePojo pojo) {
// do stuff
}
}
It is wired in Spring and it is correctly processing all of our SOAP requests. The only problem is that the method returns a 202 Accepted. This is not what the caller wants, he'd rather have us return 204 No Content (or if that is not possible an empty 200 OK).
Our other endpoints have a valid response object, and do return 200 OK. It seems void causes 202 when 204 might be more appropriate?
Is it possible to change the response code in Spring WS? We can't seem to find the correct way to do this.
Things we tried and didn't work:
Changing the return type to:
HttpStatus.NO_CONTENT
org.w3c.dom.Element <- not accepted
Adding #ResponseStatus <- this is for MVC, not WS
Any ideas?
Instead of what I wrote in the comments it is possibly the easiest to create a delegation kind of solution.
public class DelegatingMessageDispatcher extends MessageDispatcher {
private final WebServiceMessageReceiver delegate;
public DelegatingMessageDispatcher(WebServiceMessageReceiver delegate) {
this.delegate = delegate;
}
public void receive(MessageContext messageContext) throws Exception {
this.delegate.receive(messageContext);
if (!messageContext.hasResponse()) {
TransportContext tc = TransportContextHolder.getTransportContext();
if (tc != null && tc.getConnection() instanceof HttpServletConnection) {
((HttpServletConnection) tc.getConnection()).getHttpServletResponse().setStatus(200);
}
}
}
}
Then you need to configure a bean named messageDispatcher which would wrap the default SoapMessageDispatcher.
#Bean
public MessageDispatcher messageDispatcher() {
return new DelegatingMessageDispatcher(soapMessageDispatcher());
}
#Bean
public MessageDispatcher soapMessageDispatcher() {
return new SoapMessageDispatcher();
}
Something like that should do the trick. Now when response is created (In the case of a void return type), the status as you want is send back to the client.
When finding a proper solutions we've encountered some ugly problems:
Creating custom adapters/interceptors is problematic because the handleResponse method isn't called by Spring when you don't have a response (void)
Manually setting the status code doesn't work because HttpServletConnection keeps a boolean statusCodeSet which doesn't get updated
But luckily we managed to get it working with the following changes:
/**
* If a web service has no response, this handler returns: 204 No Content
*/
public class NoContentInterceptor extends EndpointInterceptorAdapter {
#Override
public void afterCompletion(MessageContext messageContext, Object o, Exception e) throws Exception {
if (!messageContext.hasResponse()) {
TransportContext tc = TransportContextHolder.getTransportContext();
if (tc != null && tc.getConnection() instanceof HttpServletConnection) {
HttpServletConnection connection = ((HttpServletConnection) tc.getConnection());
// First we force the 'statusCodeSet' boolean to true:
connection.setFaultCode(null);
// Next we can set our custom status code:
connection.getHttpServletResponse().setStatus(204);
}
}
}
}
Next we need to register this interceptor, this can be easily done using Spring's XML:
<sws:interceptors>
<bean class="com.something.NoContentInterceptor"/>
</sws:interceptors>
A big thanks to #m-deinum for pointing us in the right direction!
To override the afterCompletion method really helped me out in the exact same situation. And for those who use code based Spring configuration, here´s how one can add the interceptor for a specific endpoint.
Annotate the custom interceptor with #Component, next register the custom interceptor to a WsConfigurerAdapter like this:
#EnableWs
#Configuration
public class EndpointConfig extends WsConfigurerAdapter {
/**
* Add our own interceptor for the specified WS endpoint.
* #param interceptors
*/
#Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(new PayloadRootSmartSoapEndpointInterceptor(
new NoContentInterceptor(),
"NAMESPACE",
"LOCAL_PART"
));
}
}
NAMESPACE and LOCAL_PART should correspond to the endpoint.
If someone ever wanted to set custom HTTP status when returning non-void response, here is solution:
Spring Boot WS-Server - Custom Http Status

Resources