概述
假设我们要从一个 ES 索引(相当于一张DB表)查询数据,ES表有 order_no, order_type, state 等字段, 而应用对象则有属性 orderNo, orderType, state等。这样,就会面临“将应用对象的属性与ES字段对应起来”的问题。
固然可以通过注释来说明,不过这样显得比较生硬。因为注释并不起实际作用,代码里还得写一套映射关系,就会存在注释与代码不一致的情况。 那么,是否可以将这种对应关系的注释用代码形式来解决呢? Java 注解可以解决这个问题。
实现
定义注解
首先定义注解类。注解类需要提供对应的ES字段名 name、类型 type 以及是否必传 required。
@Retention 指明注解在何时起作用,这里是在运行时。
@Target 指明注解应用于何种对象,这里应用于字段。
@Retention (RetentionPolicy.RUNTIME) @Target (ElementType.FIELD) @Documented public @interface EsField { /** * 对应的ES字段名 */ String name(); /** * 对应的ES字段的值类型 * @return */ String type() default "" ; /** * 是否必传 */ boolean required() default false ; } |
应用领域对象
接着,将注解应用到应用领域对象。为简洁,应用领域对象只有四个字段。
@Data public class CustomerDomain implements DomainSearch { /** 店铺ID */ @EsField (name= "shop_id" , required = true ) private Long shopId; /** 订单编号 */ @EsField (name= "order_no" ) private String orderNo; /** 订单状态 */ @EsField (name= "state" , type= "list" ) private List<Integer> state; /** 订单类型 */ @EsField (name= "order_type" , type= "list" ) private List<Integer> orderType; } |
注解解析器
接着,需要提供注解解析器,将对应的映射关系转成ES查询对象的一部分。
使用接口的默认方法来实现,是为了支持不同的业务类自动可以转化为ES查询串;
注解解析器需要使用Java反射机制,来获取相应的字段,以及字段上的注解定义,然后根据字段的类型、值、注解定义来做相应处理;
使用反射来处理字段时,由于字段一般是私有的,因此必须先设置为可访问的,处理完成后还原为不可访问(earnrupees);
EsField field = f.getAnnotation(EsField.class) 用来获取字段上的注解信息(name, type, required);Object value = f.get(customerDomain) 用来获取字段的值;字段的其他类型信息可以通过 Field 的方法拿到。
public interface DomainSearch { Log logger = LogFactory.getLog(DomainSearch. class ); default String toEsQuery() { Object customerDomain = this ; EsQuery esQuery = new EsQuery(); Field[] fields = this .getClass().getDeclaredFields(); for (Field f: fields) { try { if (Modifier.isStatic(f.getModifiers())) { continue ; } f.setAccessible( true ); Object value = f.get(customerDomain); if (f.getAnnotation(EsField. class ) != null ) { EsField field = f.getAnnotation(EsField. class ); if (field.required() && value == null ) { throw new RuntimeException( "field '" + field + "' is required. value is null" ); } if (isNeedOmitted(value)) { f.setAccessible( false ); continue ; } if ((value instanceof List) && ((List)value).size() == 1 ) { // 针对 List 中单个值做优化查询 esQuery = esQuery.addTermFilter(field.name(), ((List)value).get( 0 )); } else { esQuery = esQuery.addTermFilter(field.name(), value); } } f.setAccessible( false ); } catch (Exception ex) { logger.error( "failed to build es query for field: " + f.getName(), ex); throw new RuntimeException(ex.getCause()); } } return esQuery.toJsonString(); } /** * 判断是否需要忽略该字段的查询 * @param value 字段值 * @return 是否要忽略 */ default boolean isNeedOmitted(Object value) { if (value == null ) { return true ; } // 空字符串搜索值忽略 if ((value instanceof String) && StringUtils.isBlank(value.toString())) { return true ; } // 空列表串忽略 if ((value instanceof List) && ((List)value).isEmpty()) { return true ; } return false ; } } |
查询对象
ES查询对象将所有生成的查询条件转化为ES可以接受的查询字符串。
public class EsQuery { private static int DEFAULT_SIZE = 100 ; private final Map<String, Object> termFilter; private final Map<String, Range> rangeFilter; private final Map<String, Match> matchFilter; private int size; private String orderBy = null ; private String order = null ; // query 查询语法, 是否需要 filtered, filter 这两层 // 5.x 版本不再需要这两层 private boolean isNeedFilterLayer = true ; private Integer from; private final Map<String, Object> mustNotTermFilter; private final Map<String,Object> shouldTermFilter; private Integer shouldMatchMinimum; private List<String> includes; private List<String> excludes; public EsQuery() { this .termFilter = new HashMap<>(); this .rangeFilter = new HashMap(); this .matchFilter = new HashMap(); this .mustNotTermFilter = new HashMap<>(); this .shouldTermFilter = new HashedMap(); this .size = DEFAULT_SIZE; this .includes = new ArrayList<>(); this .excludes = new ArrayList<>(); } public EsQuery addTermFilter(String key, Object value) { this .termFilter.put(key, value); return this ; } public EsQuery addMustNotTermFilter(String key, Object value) { this .mustNotTermFilter.put(key, value); return this ; } public EsQuery addAllMustNotTermFilter(Map<String,Object> mustNot) { if (mustNot != null && !mustNot.isEmpty()) { this .mustNotTermFilter.putAll(mustNot); } return this ; } public EsQuery addShouldTermFilter(String key, Object value) { this .shouldTermFilter.put(key, value); return this ; } public EsQuery addAllShouldTermFilter(Map<String,Object> should) { if (should != null && !should.isEmpty()) { this .shouldTermFilter.putAll(should); } return this ; } public EsQuery addRangeFilter(String key, long gte, long lte){ this .rangeFilter.put(key, new Range(gte, lte)); return this ; } public EsQuery addMatchFilter(String key, Match value) { this .matchFilter.put(key, value); return this ; } public EsQuery addIncludeFields(List<String> includes) { this .includes.addAll(includes); return this ; } public EsQuery addExcludeFields(List<String> excludes) { this .excludes.addAll(excludes); return this ; } @Override public String toString() { return toJsonString(); } public String toJsonString() { Map<String, Object> finalQuery = new HashMap<>(); Map<String, Object> queryMap = new HashMap<>(); Map<String, Object> filteredMap = new HashMap<>(); Map<String, Object> filterMap = new HashMap<>(); Map<String, Object> boolMap = new HashMap<>(); List<Object> mustList = obtainTermFilterList( this .termFilter); List<Object> mustNotList = obtainTermFilterList( this .mustNotTermFilter); List<Object> shouldList = obtainTermFilterList( this .shouldTermFilter); if (! this .rangeFilter.isEmpty()){ for (Map.Entry<String, Range> e: this .rangeFilter.entrySet()){ Map<String, Object> rangeMap = new HashMap<>(); Map<String, Object> rangeEntityMap = new HashMap<>(); rangeEntityMap.put(e.getKey(), e.getValue().toMap()); rangeMap.put(Constant.range, rangeEntityMap); mustList.add(rangeMap); } } if (! this .matchFilter.isEmpty()){ this .matchFilter.forEach( (key, match) -> { Map<String, String> matchEntityMap = new HashMap<>(); Map<String, Map> matchMap = new HashMap<>(); Map<String, Map> subMatchMap = new HashMap<>(); matchEntityMap.put(Constant.query, match.getQuery()); matchEntityMap.put(Constant.should_minum, match.getMinimumShouldMatch()); matchMap.put(key, matchEntityMap); subMatchMap.put(Constant.match, matchMap); mustList.add(subMatchMap); }); } boolMap.put(Constant.must, mustList); if (!mustNotList.isEmpty()) boolMap.put(Constant.mustNot, mustNotList); if (!shouldList.isEmpty()) { // 有 minimum_should_match 不带过滤器 boolMap.put(Constant.should, shouldList); boolMap.put(Constant.should_minum, shouldMatchMinimum); queryMap.put(Constant.bool, boolMap); } else { if (isNeedFilterLayer) { filterMap.put(Constant.bool, boolMap); filteredMap.put(Constant.filter, filterMap); queryMap.put(Constant.filtered, filteredMap); } else { queryMap.put(Constant.bool, boolMap); } } finalQuery.put(Constant.query, queryMap); Map<String, Object> orderMap = new HashMap<>(); Map<String, Object> orderItem = new HashMap<>(); if (order != null && orderBy != null ){ orderItem.put(Constant.order, this .order); orderMap.put( this .orderBy, orderItem); finalQuery.put(Constant.sort, orderMap); } Map<String,Object> source = new HashMap<>(); if (!includes.isEmpty()) { source.put(Constant.includes, this .includes); } if (!excludes.isEmpty()) { source.put(Constant.excludes, this .excludes); } if (!source.isEmpty()) { finalQuery.put(Constant.source, source); } finalQuery.put(Constant.size, this .size); if (from != null ) { finalQuery.put(Constant.from, from.intValue()); } return JSON.toJSONString(finalQuery); } public List<Object> obtainTermFilterList(Map<String, Object> termFilter) { List<Object> termFilterList = new ArrayList<>(); for (Map.Entry<String, Object> e: termFilter.entrySet()){ Map<String, Object> termMap = new HashMap<>(); Map<String, Object> itemMap = new HashMap<>(); itemMap.put(e.getKey(), e.getValue()); if (e.getValue() instanceof List){ termMap.put(Constant.terms, itemMap); } else { termMap.put(Constant.term, itemMap); } termFilterList.add(termMap); } return termFilterList; } public String getOrderBy() { return orderBy; } public void setOrderBy(String orderBy) { this .orderBy = orderBy; } public String getOrder() { return order; } public void setOrder(String order) { this .order = order; } public int getSize() { return size; } public void setSize( int size) { this .size = size; } public Integer getFrom() { return from; } public void setFrom(Integer from) { this .from = from; } public Map<String, Object> getTermFilter() { return Collections.unmodifiableMap(termFilter); } public Map<String, Range> getRangeFilter() { return Collections.unmodifiableMap(rangeFilter); } public Map<String, Object> getMustNotTermFilter() { return Collections.unmodifiableMap(mustNotTermFilter); } public Map<String, Object> getShouldTermFilter() { return Collections.unmodifiableMap(shouldTermFilter); } public Map<String, Match> getMatchFilter() { return matchFilter; } public void setShouldMatchMinimum(Integer shouldMatchMinimum) { this .shouldMatchMinimum = shouldMatchMinimum; } public Integer getShouldMatchMinimum() { return shouldMatchMinimum; } public Map<String, Object> getRangeMap(String key) { return Collections.unmodifiableMap(rangeFilter.get(key).toMap()); } public List<String> getIncludes() { return Collections.unmodifiableList(includes); } public boolean isNeedFilterLayer() { return isNeedFilterLayer; } public void setNeedFilterLayer( boolean needFilterLayer) { isNeedFilterLayer = needFilterLayer; } @Override public boolean equals(Object o) { // for you to write } @Override public int hashCode() { // for you to write } |
小结
通过ES搜索示例,展示了如何运用注解自动化处理领域对象属性与底层ES存储字段之间的对应关系。实际上,如果想为应用对象或组件添加某种说明或注释,不妨先想想是否可以通过注解自动化处理。注解亦可用于框架自动处理对象与组件的集成(allaboutz)。Spring框架的Resource, Component, AOP,以及 Plugin 化设计思想等都是好的应用例子。