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<Vip,   String>, QueryDslPredicateExecutor<Vip>,       QuerydslBinderCustomizer<Vip>
            , MongoRepository<Vip, String>{
  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<List<Vip>> listVip(@QuerydslPredicate(root =
            Vip.class) Predicate predicate,
                                                               Pageable pageable) throws
            URISyntaxException {
        log.debug ("[viplist]");
        Page<Vip> 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<TypeInformation<?>, EntityPath<?>> entityPaths;
    private AutowireCapableBeanFactory beanFactory;
//该类cache了系统中所有的domain类与repository名称的键值对map
    private Repositories repositories;
    public QuerydslBindings createBindingsFor(Class<? extends QuerydslBinderCustomizer<?>> 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<EntityPath<?>> findCustomizerForDomainType(
            Class<? extends QuerydslBinderCustomizer> 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<EntityPath<?>>) 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<EntityPath<?>> createQuerydslBinderCustomizer(
            Class<? extends QuerydslBinderCustomizer> 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<EntityPath<?>> {
        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 Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,967评论 19 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,958评论 6 342
  • 文章作者:Tyan博客:noahsnail.com 2.Introduction to the Spring Fr...
    SnailTyan阅读 5,424评论 7 56
  • 3.1. 核心概念 CrudRepository包含增删查改基础功能 PagingAndSortingReposi...
    titvax阅读 1,778评论 0 2
  • 他是我在高中时结识的一个人,那时候我高一,他留级到我们班,一开始很不合,还打过一架,后来因为有些事让我们成为了盆友...
    MC灬浅墨阅读 159评论 0 1