Spring data结合QueryDsl查询Mongo的customize方法未加载的一个坑

最近做的产品基于spring boot, mongo进行开发,由于前端需要进行比较复杂的查询,因此引入了dsl相关包,版本信息如下:

com.querydsl:querydsl-mongodb:jar:4.1.4,
org.springframework.boot:spring-boot-devtools:jar:1.4.1.RELEASE,
 org.springframework:spring-context-support:jar:4.3.3.RELEASE

并定义了dsl相关的接口,如下所示:

public interface VipRepository extends CrudRepository, QueryDslPredicateExecutor,       QuerydslBinderCustomizer
            , MongoRepository{
  Override
    default public void customize(QuerydslBindings bindings,  Vip root) {
        log.debug ("[VipRepository]from customize");
        bindings.bind (String.class)
                .first ((StringPath path, String value) -> path.containsIgnoreCase (value));
    }
}

Resource中注入该dao:

@RestController
@RequestMapping("/v1/vip")
public class VipResource {

    private final Logger log = LoggerFactory.getLogger (VipResource.class);

    @Inject
    private VipRepository vipRepository;

@RequestMapping(value = "/vip/list", method = RequestMethod.GET,
            produces = MediaType.APPLICATION_JSON_VALUE)
    @Timed
    @Secured(AuthoritiesConstants.ADMIN)
    public ResponseEntity> listVip(@QuerydslPredicate(root =
            Vip.class) Predicate predicate,
                                                               Pageable pageable) throws
            URISyntaxException {
        log.debug ("[viplist]");
        Page page = vipRepository.findAll (predicate, pageable);
        log.debug ("[listSimMonthGprs] page total elements: {}", page.getTotalElements ());
        HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders (page,
                "/v1/vip/list");
        return new ResponseEntity<> (results, headers, HttpStatus.OK);
    }
...

但在测试中发现,在调用/vip/list时,

有时会调用VipRepository中的customize方法,有时却不会。

实在奇怪。尝试在VipRepository添加日志,或者加断点调试,添加serializable接口都是一样的效果,只有spring context启动后第一次调用该接口加载不上,以后也都加载不上。
于是尝试从spring data,querydsl的源码进行调试。在未深入研究spring data和querydsl源码的情况下如何加断点呢?观察到在customize方法的参数中引入有QuerydslBindings这个绑定接口,结合之前对querydsl的研究,该接口完成domain类和Q类之前的参数绑定,于是利用IDE的功能找到该接口的实现类QuerydslBindingsFactory,该类源码如下:

public class QuerydslBindingsFactory implements ApplicationContextAware {
    private final EntityPathResolver entityPathResolver;
    private final Map, EntityPath> entityPaths;
    private AutowireCapableBeanFactory beanFactory;
//该类cache了系统中所有的domain类与repository名称的键值对map
    private Repositories repositories;
    public QuerydslBindings createBindingsFor(Class> customizer,
            TypeInformation domainType) {

        Assert.notNull(domainType, "Domain type must not be null!");

        EntityPath path = verifyEntityPathPresent(domainType);

        QuerydslBindings bindings = new QuerydslBindings();
        findCustomizerForDomainType(customizer, domainType.getType()).customize(bindings, path);

        return bindings;
    }

    /**
     * Tries to detect a Querydsl query type for the given domain type candidate via the configured
     * {@link EntityPathResolver}.
     * 
     * @param candidate must not be {@literal null}.
     * @throws IllegalStateException to indicate the query type can't be found and manual configuration is necessary.
     */
    private EntityPath verifyEntityPathPresent(TypeInformation candidate) {

        EntityPath path = entityPaths.get(candidate);

        if (path != null) {
            return path;
        }

        Class type = candidate.getType();

        try {
            path = entityPathResolver.createPath(type);
        } catch (IllegalArgumentException o_O) {
            throw new IllegalStateException(
                    String.format(INVALID_DOMAIN_TYPE, candidate.getType(), QuerydslPredicate.class.getSimpleName()), o_O);
        }

        entityPaths.put(candidate, path);
        return path;
    }

    /**
     * Obtains the {@link QuerydslBinderCustomizer} for the given domain type. Will inspect the given annotation for a
     * dedicatedly configured one or consider the domain types's repository.
     * 
     * @param annotation
     * @param domainType
     * @return
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private QuerydslBinderCustomizer> findCustomizerForDomainType(
            Class customizer, Class domainType) {

        if (customizer != null && !QuerydslBinderCustomizer.class.equals(customizer)) {
            return createQuerydslBinderCustomizer(customizer);
        }

        if (repositories != null && repositories.hasRepositoryFor(domainType)) {

            Object repository = repositories.getRepositoryFor(domainType);

            if (repository instanceof QuerydslBinderCustomizer) {
                return (QuerydslBinderCustomizer>) repository;
            }
        }

        return NoOpCustomizer.INSTANCE;
    }

    /**
     * Obtains a {@link QuerydslBinderCustomizer} for the given type. Will try to obtain a bean from the
     * {@link org.springframework.beans.factory.BeanFactory} first or fall back to create a fresh instance through the
     * {@link org.springframework.beans.factory.BeanFactory} or finally falling back to a plain instantiation if no
     * {@link org.springframework.beans.factory.BeanFactory} is present.
     * 
     * @param type must not be {@literal null}.
     * @return
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    private QuerydslBinderCustomizer> createQuerydslBinderCustomizer(
            Class type) {

        if (beanFactory == null) {
            return BeanUtils.instantiateClass(type);
        }

        try {
            return beanFactory.getBean(type);
        } catch (NoSuchBeanDefinitionException e) {
            return beanFactory.createBean(type);
        }
    }
/**
*
**/
    private static enum NoOpCustomizer implements QuerydslBinderCustomizer> {
        INSTANCE;
        @Override
        public void customize(QuerydslBindings bindings, EntityPath root) {}
    }
}

This class will be invoked before entering resource method. It is charge of generating predicate instance from request parameters.
At first, calling createBindingsFor method. In the method, invoking verifyEntityPathPresent to get path. Then call findCustomizerForDomainType to check if dsl repository exists.
Pay attention to this sentence:

Object repository = repositories.getRepositoryFor(domainType);

This sentence gets repository object according to domainType. When using Vip to query, I found it return another repository for Vip not DSL repository.

到这个地方,大家就比较清楚原因了:因为系统中针对同一个domain定了两个repository,而spring在加载时使用domain class作为key,repository name作为值,只能随机cache一个repository,这也是有时可以调用到customize,而有时不可以的原因。

找到原因后,该问题就很好解决了,把两个repository接口融合到一起即可。

你可能感兴趣的:(Spring data结合QueryDsl查询Mongo的customize方法未加载的一个坑)