Search code examples
javaspring-bootgradlemapstruct

Spring Boot doesn't find my Java bean mapper


I'm getting an error when I try to start Spring Boot using a Java bean mapper. I'm using Eclipse on Windows, Gradle to build. This is just a learning project I'm using to learn these components.

I'm listening to an ActiveMQ Artemis queue, using the incoming data to call a web service, then saving the order response in a MondoDB. All the components are working with the exception of the mapper converting the api response to a MongoDB entity.

Can anyone see what I'm doing wrong here? It's something with how I'm injecting the OrderMapper, but I'm not sure at this point. This is the Spring output:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.5)

2021-04-30 14:16:15.860  INFO 2320 --- [           main] c.b.R.RestClientPocApplication           : Starting RestClientPocApplication using Java 11.0.9 on PSPLT-F7VYYY2 with PID 2320 (C:\Users\Bwarrick\Workspaces\Java\RESTClientPOC\bin\main started by Bwarrick in C:\Users\Bwarrick\Workspaces\Java\RESTClientPOC)
2021-04-30 14:16:15.863  INFO 2320 --- [           main] c.b.R.RestClientPocApplication           : No active profile set, falling back to default profiles: default
2021-04-30 14:16:16.405  INFO 2320 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data MongoDB repositories in DEFAULT mode.
2021-04-30 14:16:16.567  INFO 2320 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 157 ms. Found 1 MongoDB repository interfaces.
2021-04-30 14:16:16.991  INFO 2320 --- [           main] org.mongodb.driver.cluster               : Cluster created with settings {hosts=[192.168.56.102:27017], mode=SINGLE, requiredClusterType=UNKNOWN, serverSelectionTimeout='30000 ms'}
2021-04-30 14:16:17.064  INFO 2320 --- [68.56.102:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:1, serverValue:49}] to 192.168.56.102:27017
2021-04-30 14:16:17.064  INFO 2320 --- [68.56.102:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:2, serverValue:50}] to 192.168.56.102:27017
2021-04-30 14:16:17.065  INFO 2320 --- [68.56.102:27017] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=192.168.56.102:27017, type=STANDALONE, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=9, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=23420200}
2021-04-30 14:16:17.332  WARN 2320 --- [           main] onfigReactiveWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'artemisConsumer' defined in file [C:\Users\Bwarrick\Workspaces\Java\RESTClientPOC\bin\main\com\benwarrick\RESTClientPOC\jms\ArtemisConsumer.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.benwarrick.RESTClientPOC.service.OrderMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
2021-04-30 14:16:17.355  INFO 2320 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-04-30 14:16:17.370 ERROR 2320 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 1 of constructor in com.benwarrick.RESTClientPOC.jms.ArtemisConsumer required a bean of type 'com.benwarrick.RESTClientPOC.service.OrderMapper' that could not be found.


Action:

Consider defining a bean of type 'com.benwarrick.RESTClientPOC.service.OrderMapper' in your configuration.

Here is my component:

package com.benwarrick.RESTClientPOC.jms;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jms.annotation.JmsListener;
    import org.springframework.stereotype.Component;
    
    import com.benwarrick.RESTClientPOC.persistance.OrderEntity;
    import com.benwarrick.RESTClientPOC.persistance.OrderRepository;
    import com.benwarrick.RESTClientPOC.service.CoinBaseClientServiceImpl;
    import com.benwarrick.RESTClientPOC.service.OrderMapper;
    import com.benwarrick.RESTClientPOC.service.OrderResponse;
    import com.benwarrick.RESTClientPOC.service.Price;
    import com.benwarrick.RESTClientPOC.service.Prices;
    import reactor.core.publisher.Mono;
    import reactor.core.publisher.Flux;
    
    @Component
    public class ArtemisConsumer {
    
        private final OrderRepository orderRepository; 
        private final OrderMapper orderMapper;
        
        @Autowired
        public ArtemisConsumer(OrderRepository orderRepository, OrderMapper orderMapper) {
            this.orderRepository = orderRepository; 
            this.orderMapper = orderMapper; 
        }
        
        
        @JmsListener(destination = "test.topic::test.queue")
        public void receive(String msg){
            System.out.println("Got Message: " + msg);
            
            CoinBaseClientServiceImpl client = new CoinBaseClientServiceImpl();
            
            Mono<OrderResponse>orderCreate = client.createOrder("market", "USD", "BTC", "5");
            orderCreate.log().subscribe(
                    successValue -> processResponse(successValue),
                    error -> System.err.println(error.getMessage()),
                    () -> System.out.println("mono consumed")
                    );
        }
        
        public void processResponse(OrderResponse orderResponse) {
            
            System.out.println(orderResponse.getOrderID() + " " + orderResponse.getSellingCurrency() + orderResponse.getBuyingCurrency() + " Qty: " 
                    + orderResponse.getBoughtQty() + " Type: " + orderResponse.getOrderType()) ;
            try {
            OrderEntity entity = orderMapper.apiResponseToEntity(orderResponse); 
            OrderEntity newEntity = orderRepository.save(entity); 
            System.out.println("Test: " + newEntity.getBoughtQuantity()); 
            }
            catch(Exception e) {
                System.out.println("Exception: " + e.toString()) ;
            }
        }
    }

Here is my main process:

package com.benwarrick.RESTClientPOC;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class RestClientPocApplication {
    private static final Logger LOG = LoggerFactory.getLogger(RestClientPocApplication.class);

    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = 
        SpringApplication.run(RestClientPocApplication.class, args);
        String mongoDBHost = ctx.getEnvironment().getProperty("spring.data.mongodb.host");
        String mongoDbPort = ctx.getEnvironment().getProperty("spring.data.mongodb.port");
        LOG.info("Connected to MongoDb: " + mongoDBHost + ":" + mongoDbPort); 
    }

}

And here is the bean that isn't being found:

package com.benwarrick.RESTClientPOC.service;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;

import com.benwarrick.RESTClientPOC.persistance.OrderEntity;

@Mapper (componentModel = "spring")
public interface OrderMapper {
    
    @Mappings({
        @Mapping(target = "id", ignore = true),
        @Mapping(target = "version", ignore = true),
        @Mapping(target = "orderId", source="orderID"),
        @Mapping(target = "orderID", source="boughtQty")
    })
    OrderEntity apiResponseToEntity(OrderResponse api); 
}

And my build.graddle

plugins {
    id 'org.springframework.boot' version '2.4.5'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.benwarrick'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

ext {
    mapstructVersion = "1.4.2.Final"
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-artemis'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    
    implementation("org.mapstruct:mapstruct:${mapstructVersion}")
    compileOnly "org.mapstruct:mapstruct-processor:${mapstructVersion}"
    annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
    testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
    
    implementation('org.springframework.boot:spring-boot-starter-data-mongodb')
    testImplementation('de.flapdoodle.embed:de.flapdoodle.embed.mongo')
}

test {
    useJUnitPlatform()
}

Solution

  • For MapStruct you need to correctly set up annotation processor so that IDE (Eclipse in your case) and build tool, Gradle that is play well together.

    You can read on MapStruct page uder IDE support about it. Now I didn't manage to get it working without issues that way.

    Plugin to the rescue! Now what I recommend you is that you use following Gradle plugin that can set up Eclipse IDE in a proper way for you. The plugin: https://plugins.gradle.org/plugin/com.diffplug.eclipse.apt

    I see you know already how to include the plugin, just in case the code that you have to add to build.gradle:

    plugins {
        ...
        id 'com.diffplug.eclipse.apt' version "3.29.1"
    }
    

    And then run the command from your project that will set up Eclipse:

    ./gradlew eclipseJdtApt eclipseFactorypath eclipseJdt
    

    From within Eclipse, you now have to run right-click the project and select Gradle / Refresh Gradle Project.

    enter image description here

    Afterwards, Project / Clean. With this clean build, the annotation-processor should be running.

    I hope Eclipse Buildship will pick this up and make it easier to support his feature.