2

I tried to implement the polymorphism in the service layer.
and injecting component was working in the controller.
But It doesn't worked trying to use validation api with @Valid @Validated

@Validated
public interface SearchService<K, V> {
    V search(@NotNull @Valid K key);
}
@Service
public class UserSearchService implements SearchService<Email, UserDto> {
    private final UserDao userDao;
    private final Converter<User, UserDto> converter;

    public UserSearchService(UserDao userDao, Converter<User, UserDto> converter) {
        this.userDao = userDao;
        this.converter = converter;
    }

    @Override
    public UserDto search(Email email) {
        try {
            User entity = userDao.findByEmail(email.get());
            return converter.convert(entity);
        } catch (NoResultException noResultException) {
            throw new NotExistDataException("user not found", email.get());
        }
    }
}
@RestController
@Validated
public class UserSearchController {
    private final SearchService<Email, UserDto> searchService;

    public UserSearchController(SearchService<Email, UserDto> searchService) {
        this.searchService = searchService;
    }

    @GetMapping("api/user")
    public UserDto handleSearchingUserByEmail(@RequestParam @Valid Email email) {
        return searchService.search(email);
    }
}
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userSearchController' defined in file [C:\Users\younggon\Desktop\studylog\target\classes\io\zerogone\controller\api\UserSearchController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'io.zerogone.service.search.SearchService<?, ?>' available: expected single matching bean but found 2: blogSearchService,userSearchService

and spring is matching this service too

@Service
public class BlogSearchService implements SearchService<BlogName, BlogDto> {
    private final BlogDao blogDao;
    private final Converter<Blog, BlogDto> converter;

    public BlogSearchService(BlogDao blogDao, Converter<Blog, BlogDto> converter) {
        this.blogDao = blogDao;
        this.converter = converter;
    }

    @Override
    public BlogDto search(BlogName blogName) {
        try {
            Blog entity = blogDao.findByName(blogName.get());
            return converter.convert(entity);
        } catch (NoResultException noResultException) {
            throw new NotExistDataException("blog not found", blogName.get());
        }
    }
}

Actually, @Qualifier can be used to solve it. but I wonder why. help me :(


update.

I tested autowiring works well without @Qualifier and @Validated

public interface SearchService<K, V> {
    V search(K key);
}
@RestController
@Validated
public class UserSearchController {
    private final SearchService<Email, UserDto> searchService;

    public UserSearchController(SearchService<Email, UserDto> searchService) {
        this.searchService = searchService;
    }

    @GetMapping("api/user")
    public UserDto handleSearchingUserByEmail(@RequestParam @Valid Email email) {
        return searchService.search(email);
    }
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebConfiguration.class, DatabaseConfiguration.class}, loader = AnnotationConfigWebContextLoader.class)
@WebAppConfiguration
public class UserSearchControllerTest {
    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void setUp() throws Exception {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void handleSearchingUserByEmail() throws Exception {
        mockMvc.perform(get("/api/user")
                .param("email", "dudrhs571@gmail.com"))
                .andExpect(status().isOk())
                .andDo(print());
    }
}

result is here.

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /api/user
       Parameters = {email=[dudrhs571@gmail.com]}
          Headers = {}

Handler:
             Type = io.zerogone.controller.api.UserSearchController
           Method = public io.zerogone.model.dto.UserDto io.zerogone.controller.api.UserSearchController.handleSearchingUserByEmail(io.zerogone.model.Email)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[application/json;charset=UTF-8]}
     Content type = application/json;charset=UTF-8
             Body = {"id":1,"name":"zeroGone","nickName":"zeroGone","email":"dudrhs571@gmail.com","imageUrl":"url","blogs":[{"id":1,"name":"studylog","introduce":"web platform for team blog","imageUrl":"/img/blog-default.png","members":null,"invitationKey":null}]}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

But it deosn't works after adding @Validated to SearchService interface

@Validated
public interface SearchService<K, V> {
    V search(@NotNull @Valid K key);
}
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userSearchController' defined in file [C:\Users\younggon\Desktop\studylog\target\classes\io\zerogone\controller\api\UserSearchController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'io.zerogone.service.search.SearchService<?, ?>' available: expected single matching bean but found 2: blogSearchService,userSearchService

I guess Spring is autowiring generic type for UserSearchService, BlogSearchService

Actually it works

but after adding @Validated it doesn't works

and testing directly get UserSearchService.class doesnt't works too.

@Test
public void getUserSearchService() {
     Assert.assertEquals(UserSearchService.class, webApplicationContext.getBean(UserSearchService.class).getClass());
}
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'io.zerogone.service.search.UserSearchService' available
  1. I wonder Autowiring for SearchService type is worked by distinguish generic type
  2. And why after adding @Validated it doesn't work?

update2.

I know it works well my code in spring boot app
I hope my spring project works well
Here's my project structure summary

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.zerogone</groupId>
    <artifactId>studylog</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>

        <spring.version>4.3.30.RELEASE</spring.version>
        <apache.commons.version>1.4</apache.commons.version>
        <jackson.version>2.12.2</jackson.version>
    </properties>

    <dependencies>
        <!--mvc start-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>${apache.commons.version}</version>
        </dependency>
        <!--mvc end-->
        <!--test start-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <!--test end-->
        <!--database start-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>${apache.commons.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.4.29.Final</version>
        </dependency>
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!--database end-->
        <!--log start-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <!--log end-->
        <!--validation start-->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.13.Final</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.el</artifactId>
            <version>3.0.0</version>
        </dependency>
        <!--validation start-->
    </dependencies>
</project>
@EnableWebMvc
@Configuration
@ComponentScan(basePackages = "io.zerogone")
public class WebConfiguration extends WebMvcConfigurerAdapter {
    private static final int TEN_MEGA_BYTE = 10 * 1024 * 1024;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        registry.addResourceHandler("/img/**").addResourceLocations("/img/");
        registry.addResourceHandler("/js/**").addResourceLocations("/js/");
    }

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/WEB-INF/views/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }

    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
        multipartResolver.setMaxUploadSize(TEN_MEGA_BYTE);
        multipartResolver.setMaxUploadSizePerFile(TEN_MEGA_BYTE);
        return multipartResolver;
    }

    @Bean
    public ConversionServiceFactoryBean conversionServiceFactory(Set<Converter<?, ?>> converters) {
        ConversionServiceFactoryBean conversionServiceFactory = new ConversionServiceFactoryBean();
        conversionServiceFactory.setConverters(converters);
        return conversionServiceFactory;
    }

    @Bean
    public ConversionService conversionService(ConversionServiceFactoryBean factory) {
        return factory.getObject();
    }

    @Bean
    public ConfigurableWebBindingInitializer webBindingInitializer(ConversionService conversionService) {
        ConfigurableWebBindingInitializer configurableWebBindingInitializer = new ConfigurableWebBindingInitializer();
        configurableWebBindingInitializer.setConversionService(conversionService);
        return configurableWebBindingInitializer;
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}
public class ApplicationInitializer implements WebApplicationInitializer {
    private static final String ROOT_PACKAGE = "io.zerogone";

    private static final String DISPATCHER_NAME = "dispatcher";
    private static final int DISPATCHER_LOAD_NUMBER = 1;
    private static final String DISPATCHER_MAPPING_URL = "/";

    @Override
    public void onStartup(ServletContext servletContext) {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.scan(ROOT_PACKAGE);

        servletContext.addListener(new ContextLoaderListener(context));

        ServletRegistration.Dynamic appServlet = servletContext.addServlet(DISPATCHER_NAME, new DispatcherServlet(new GenericWebApplicationContext()));
        appServlet.setLoadOnStartup(DISPATCHER_LOAD_NUMBER);
        appServlet.addMapping(DISPATCHER_MAPPING_URL);
    }
}

and Tell me necessary information for help
Thanks for any help

zeroGone
  • 41
  • 5
  • So it all works until you add @Valid and @Validated? – Ilya Sazonov May 14 '21 at 18:27
  • yes. read updated this – zeroGone May 15 '21 at 06:29
  • It's a very interesting question, it's weird I'm the only one upvoting it. What if you add Valid and Validated to implementation, instead of interface? – Ilya Sazonov May 15 '21 at 07:09
  • if Adding `@Valid` and `@Validated` to implementation, it throws `Caused by: javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method UserSearchService#search(Email) redefines the configuration of SearchService#search(Object).`. So I must to add `@Valid` and `@Validated` to interface – zeroGone May 15 '21 at 08:17
  • Did you try to put `@Validated` annotation upon method, and not class? – Alex May 15 '21 at 08:35
  • yeah. I tried @Validated on both method or parameter. And it doesn't validate parameter of method in service layer. I don’t' know why it doenst' validate it – zeroGone May 15 '21 at 09:02
  • Adding `@Validate` to method or parameter, spring autowired it without `@Qualifier` – zeroGone May 15 '21 at 09:06
  • I have a very good suspicion, can you share the project structure that you have, and the classes in each package? – Boug May 15 '21 at 09:14

2 Answers2

2

It solved!!! I knew why spring throws exception at autowiring.

This problem is caused by my validation configuration.

@EnableWebMvc
@Configuration
@ComponentScan(basePackages = "io.zerogone")
public class WebConfiguration extends WebMvcConfigurerAdapter {
   
    //etc 

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() { 
        return new MethodValidationPostProcessor(); // this is
    }
}

MethodValidationPostProcessor creates proxy beans that attached @Validated.

This proxy beans based jdk-based proxy.
So when Spring is autowiring, It may be confused because it lost SearchService implementation information.

Read this.
https://github.com/spring-projects/spring-boot/issues/17000

and my project's Spring boot version works well because it based on CGLib proxy.

This problem solved after MethodValidationPostProcessor

@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
    MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
    postProcessor.setProxyTargetClass(true); // it configures based on CGLib proxy 
    return postProcessor;
}

Therefore, I can autowiring without @Qualifier for generic implementation.


Things to read further.
What is the difference between JDK dynamic proxy and CGLib?
https://www.baeldung.com/spring-autowire-generics

zeroGone
  • 41
  • 5
1

I found your error very interesting and created the same project that you have. It seems that for me the spring boot app can start without any errors even with @Validated annotation

enter image description here

So something else is going on in your project

Boug
  • 4,133
  • 2
  • 7
  • 15
  • Autowired is not necessary, since Spring automatically injects final fields . And it can use parameters for generics as qualifiers. But doesn't do it in this case apparently. The question is why – Ilya Sazonov May 14 '21 at 19:46
  • `Autowired is not necessary, since Spring automatically injects final fields` That is wrong on this specific case here. Here it seems that only 1 constructor exists which autowires this field. According to spring if only 1 constructor is available then this constructor is automatically used as if it had the Autowired. It is not the final keyword that autowires it. – Boug May 14 '21 at 19:56
  • https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-autowired-annotation – Boug May 14 '21 at 19:58
  • why it works before adding @Validated? – zeroGone May 15 '21 at 06:30
  • Thanks for exercising my project. I know it doesn't works because my project isn't spring boot app. so why my Spring project (not boot) is going to wrong – zeroGone May 15 '21 at 09:53