2

I have sandbox application microservices based on SpringBoot, SpringData JPA, Axon. I created 2 simple microservices: orders service and products service and trying to explore Axon Sagas. During Saga transaction I execute order create command, when it happens Saga emits product reserve event. This event is handled in product service and it fails with:

Exception in thread "CommandProcessor-0" com.thoughtworks.xstream.security.ForbiddenClassException: com.udemy.shared.command.ReserveProductCommand

How it can be fixed?

controller code in orders microservice:

@PostMapping
    public String createOrder(@Valid @RequestBody OrderDTO order) {
        CreateOrderCommand createOrderCommand = CreateOrderCommand.builder()
                .orderId(UUID.randomUUID().toString())
                .userId("27b95829-4f3f-4ddf-8983-151ba010e35b")
                .productId(order.getProductId())
                .quantity(order.getQuantity())
                .addressId(order.getAddressId())
                .orderStatus(OrderStatus.CREATED)
                .build();
        return commandGateway.sendAndWait(createOrderCommand);
    }

commands code:

@Builder
@Data
public class CreateOrderCommand {
    @AggregateIdentifier
    private final String orderId;
    private final String userId;
    private final String productId;
    private final int quantity;
    private String addressId;
    private final OrderStatus orderStatus;
}

@Data
@Builder
public class ReserveProductCommand {
    @AggregateIdentifier
    private String productId;
    private String orderId;
    private String userId;
    private int quantity;
}

saga code:

@Slf4j
@Saga
public class OrdersSaga {
    @Autowired
    private transient CommandGateway commandGateway;

    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderCreatedEvent event) {
        ReserveProductCommand reserveProductCommand = ReserveProductCommand.builder()
                .orderId(event.getOrderId())
                .productId(event.getProductId())
                .userId(event.getUserId())
                .quantity(event.getQuantity())
                .build();
        commandGateway.send(reserveProductCommand, (commandMessage, commandResultMessage) -> {
            if (commandResultMessage.isExceptional()) {
                log.error("Something went wrong during product reserve: " + commandResultMessage.exceptionResult().getMessage() );
            }
        });
        log.info("Created order command fired! Order id = " + event.getOrderId());
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void handle(ProductReservedEvent event) {
        log.info("Handling product reserve event for product with id = " + event.getProductId());

    }
}

handler where occurs error(products microservice):

@Slf4j
@Component
public class ProductEventHandler {
    ProductsRepository repository;

    @EventHandler
    public void on(ProductReservedEvent event) {
        ProductEntity updatedProduct = repository.findByProductId(event.getProductId());
        updatedProduct.setQuantity(event.getQuantity());
        log.info("Product reserved event was applied in event handler for product with id - " + event.getProductId());
        repository.save(updatedProduct);
    }
}

After googling I found out, that different Spring Boot version can have different xstream version and more relevant xstream produces this exception. I downgraded Spring Boot version in these services to 2.7.8, but this didnt help

my project's JDK and structure can be seen on screens

enter image description here

enter image description here

Sam Fisher
  • 746
  • 2
  • 10
  • 27

2 Answers2

1

I guess that you're on JDK17 or above, Sam. As it stands, XStream does not play nicely with newer JDK versions, as it relies very heavily on reflection. As that's being shut off, exceptions may occur.

Sadly enough, Axon Framework defaults to use the so-called XStreamSerializer. Changing this to something else would incur breaking changes for all Framework users. Hence, the default stuck.

To work around this in Spring Boot environments, the Framework wires a custom XStream instance for you, adding to XStream's security context the package name of your @SpringBootApplication annotated class. Axon does log a WARN message about this, though, as it is strongly advised to define the XStream security context yourself based on the objects you will be de-/serializing.

Regardless, by taking this route, 9 out of 10 scenarios are covered for de-/serialization. However, I assume that your ReserveProductCommand resides in a different package.

Well, even if it isn't, you're thus recommended to define XStream's security context yourself. Or, you can switch Axon Framework's Serializer from XML to JSON by defining the JacksonSerializer. Especially for your messages (Commands, Events, and Queries) it is advisable to use JSON's smaller format. This saves network traffic and storage.

As you may note, I am making some assumptions about your JDK version and package structuring. If my suggested solutions do not solve the predicament, be sure to leave a comment.

Steven
  • 6,936
  • 1
  • 16
  • 31
  • Hi, Steven. I added aditional screenshots with project jdk and structure. You are right - ReserveProductCommand is situated in a separate commonly-used by both serves(orders,products) package. May be amazon's jdk correto works badly with XStream? – Sam Fisher Mar 02 '23 at 07:34
  • I wouldn't assume the JDK to be the issue here. This time, it's simply the Framework's default solution to cover for the messages. However, your path to the `@SpringBootApplication` annotated does not align with the path of your messages. So, you have a good example of where you need to set XStream's security context. You can spot how Axon Framework sets the default config here: https://github.com/AxonFramework/AxonFramework/blob/master/spring-boot-autoconfigure/src/main/java/org/axonframework/springboot/autoconfig/XStreamAutoConfiguration.java – Steven Mar 03 '23 at 12:01
  • 1
    I understood the reason and found temporary solution. will describe it here. thanx! – Sam Fisher Mar 03 '23 at 13:59
1

Thanx to @Steven recommendations, I googled and found the solution. It's not very good, but as temporary solution it helps and can be updated according to concrete purposes. So basing on axon forum and this thread, in commonly used module I created Xstream config like:

@Configuration
public class AxonXstreamConfig {

    @Bean
    public XStream xStream() {
        XStream xStream = new XStream();
        xStream.addPermission(AnyTypePermission.ANY);

        return xStream;
    }
}

After that I imported it to to necessary service, for example:

@EnableDiscoveryClient
@SpringBootApplication
@Import({ AxonXstreamConfig.class })
public class ProductsApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProductsApplication.class, args);
    }
}

Now I dont get that exception, but we must take in consideration, that this line:

xStream.addPermission(AnyTypePermission.ANY);

switches off any xstream serialization security, so for real-life working examples this is a bad practice and I did it because I'm just playing in sandbox and I'm concentrated on Axon learning and don't want waste much time on other things, but on those link can be found different examples of xstream config, which will allow to make proper config.

Sam Fisher
  • 746
  • 2
  • 10
  • 27
  • 1
    Using XStream's `allowTypesByWildcard`, where you can, for example, use the package name of your project as the wildcard, should already be a little safer than simply allowing any. – Steven Mar 06 '23 at 08:20
  • 1
    I was stuck in the same situation in the project, Product service was getting exception. This solution worked for now. Thanks – iSankha007 Jun 01 '23 at 13:39