spring-data方法名解析对象PartTree

840 阅读8分钟

须知

  • 源码部分去除了无关紧要的方法。例如:get相关方法
  • 源码依赖:spring-data-commons-1.13.11.RELEASE

简介

  • PartTree是spring-data抽象出的用于表示自定义方法名的类。一个自定义方法对应一个PartTree对象,可以用于基于其API来构建查询,而不必为每次查询执行解析方法名称

整体结构图

源码

**
 * PartTree将repository方法命名字符串解析成对象。使用domain class来校验Part(属性节点)都引用合法的属性。
 * PartTree 可以用于基于其API来构建查询,而不必为每次查询执行解析方法名称
 */
public class PartTree implements Iterable<OrPart> {
	/*
	 * 我们寻找以下模式:关键字,后跟
	 *  具有小写字母变体的大写字母。 或 BASIC LATIN Unicode块中没有的其他任何字母(例如中文,韩文,日文等)。
	 *
	 * @see <a href="http://www.regular-expressions.info/unicode.html">http://www.regular-expressions.info/unicode.html</a>
	 * @see <a href="http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#ubc">Pattern</a>
	 */
	// %s: 为String.format的占位符
	//(?=pattern): 非获取匹配,正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。例如,“Windows(?=95|98|NT|2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
	// \\p{Lu} : 具有小写字母变体的大写字母
	// \\P{InBASIC_LATIN}:U+0000–U+007F
	private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\P{InBASIC_LATIN}))";
	private static final String QUERY_PATTERN = "find|read|get|query|stream";
	private static final String COUNT_PATTERN = "count";
	private static final String EXISTS_PATTERN = "exists";
	private static final String DELETE_PATTERN = "delete|remove";
	//前缀的正则,包括:find/count/exists/delete等
	private static final Pattern PREFIX_TEMPLATE = Pattern.compile( //
			"^(" + QUERY_PATTERN + "|" + COUNT_PATTERN + "|" + EXISTS_PATTERN + "|" + DELETE_PATTERN + ")((\\p{Lu}.*?))??By");

	/**
	 * 主语,例如: "findDistinctUserByNameOrderByAge" 的主语为 "DistinctUser".
	 */
	private final Subject subject;

	/**
	 * 谓语, 例如: "findDistinctUserByNameOrderByAge" 的谓语为 "NameOrderByAge".
	 */
	private final Predicate predicate;

	/**
	 * 将字符串解析成PartTree
	 * 
	 * @param domainClass 实体类class。用于检查各个部分,确保引用正确的属性
	 */
	public PartTree(String source, Class<?> domainClass) {
		Matcher matcher = PREFIX_TEMPLATE.matcher(source);
		if (!matcher.find()) {
			this.subject = new Subject(null);
			this.predicate = new Predicate(source, domainClass);
		} else {
			this.subject = new Subject(matcher.group(0));
			this.predicate = new Predicate(source.substring(matcher.group().length()), domainClass);
		}
	}
    
	public Iterator<OrPart> iterator() {
		return predicate.iterator();
	}


	public Sort getSort() {
		OrderBySource orderBySource = predicate.getOrderBySource();
		return orderBySource == null ? null : orderBySource.toSort();
	}

	public boolean isDistinct() {
		return subject.isDistinct();
	}

	public Boolean isCountProjection() {
		return subject.isCountProjection();
	}

	public Boolean isExistsProjection() {
		return subject.isExistsProjection();
	}

	public Boolean isDelete() {
		return subject.isDelete();
	}

	public boolean isLimiting() {
		return getMaxResults() != null;
	}

	/**
	 * 返回要返回的最大结果数;如果不受限制,则返回null
	 */
	public Integer getMaxResults() {
		return subject.getMaxResults();
	}

	/**
	 * 获取全部的Part
	 */
	public Iterable<Part> getParts() {
		List<Part> result = new ArrayList<Part>();
		for (OrPart orPart : this) {
			for (Part part : orPart) {
				result.add(part);
			}
		}
		return result;
	}

	/**
	 * 获取给定Type的全部Part
	 */
	public Iterable<Part> getParts(Type type) {

		List<Part> result = new ArrayList<Part>();

		for (Part part : getParts()) {
			if (part.getType().equals(type)) {
				result.add(part);
			}
		}

		return result;
	}

	@Override
	public String toString() {
		OrderBySource orderBySource = predicate.getOrderBySource();
		return String.format("%s%s", StringUtils.collectionToDelimitedString(predicate.nodes, " or "),
				orderBySource == null ? "" : " " + orderBySource);
	}

	/**
	 * 按照给定的关键词分割给定的文本
	 * 
	 * @param text 要切分的文本
	 * @param keyword 关键字
	 * @return 切分之后的数组
	 */
	private static String[] split(String text, String keyword) {
		Pattern pattern = Pattern.compile(String.format(KEYWORD_TEMPLATE, keyword));
		return pattern.split(text);
	}

	/**
	 * 查询的主语对象。 例如:findDistinctUserByNameOrderByAge解析出的主语是DistinctUser
	 */
	private static class Subject {
		//Distinct关键字
		private static final String DISTINCT = "Distinct";
		//匹配count的正则表达式
		private static final Pattern COUNT_BY_TEMPLATE = Pattern.compile("^count(\\p{Lu}.*?)??By");
		//匹配exists的正则表达式
		private static final Pattern EXISTS_BY_TEMPLATE = Pattern.compile("^(" + EXISTS_PATTERN + ")(\\p{Lu}.*?)??By");
		//匹配删除(delete或remove)的正则表达式
		private static final Pattern DELETE_BY_TEMPLATE = Pattern.compile("^(" + DELETE_PATTERN + ")(\\p{Lu}.*?)??By");
		//top n的正则字符串
		private static final String LIMITING_QUERY_PATTERN = "(First|Top)(\\d*)?";
 		//查找top N的正则表达式
		private static final Pattern LIMITED_QUERY_TEMPLATE = Pattern.compile("^(" + QUERY_PATTERN + ")(" + DISTINCT + ")?"
				+ LIMITING_QUERY_PATTERN + "(\\p{Lu}.*?)??By");

		private final boolean distinct;
		private final boolean count;
		private final boolean exists;
		private final boolean delete;
		//最大要返回的数量
		private final Integer maxResults;

		public Subject(String subject) {
			this.distinct = subject == null ? false : subject.contains(DISTINCT);
			this.count = matches(subject, COUNT_BY_TEMPLATE);
			this.exists = matches(subject, EXISTS_BY_TEMPLATE);
			this.delete = matches(subject, DELETE_BY_TEMPLATE);
			this.maxResults = returnMaxResultsIfFirstKSubjectOrNull(subject);
		}

		private Integer returnMaxResultsIfFirstKSubjectOrNull(String subject) {
			if (subject == null) {
				return null;
			}
			Matcher grp = LIMITED_QUERY_TEMPLATE.matcher(subject);
			if (!grp.find()) {
				return null;
			}
			return StringUtils.hasText(grp.group(4)) ? Integer.valueOf(grp.group(4)) : 1;
		}

		//...省略部分无关紧要的方法,例如:get相关的方法

		private final boolean matches(String subject, Pattern pattern) {
			return subject == null ? false : pattern.matcher(subject).find();
		}
	}

	/**
	 * 查询的谓语对象。例如:findDistinctUserByNameOrderByAge解析出的谓语部分为NameOrderByAge
	 */
	private static class Predicate {
		//所有忽略大小写的正则表达式
		private static final Pattern ALL_IGNORE_CASE = Pattern.compile("AllIgnor(ing|e)Case");
  		//排序的关键字OrderBy
		private static final String ORDER_BY = "OrderBy";
		//OrPart列表。
		private final List<OrPart> nodes = new ArrayList<OrPart>();
		private final OrderBySource orderBySource;
		private boolean alwaysIgnoreCase;

		public Predicate(String predicate, Class<?> domainClass) {
			String[] parts = split(detectAndSetAllIgnoreCase(predicate), ORDER_BY);
			if (parts.length > 2) {
				throw new IllegalArgumentException("OrderBy must not be used more than once in a method name!");
			}
			// 构建OrPart类型的nodes
			buildTree(parts[0], domainClass);
  			//构建排序对象
			this.orderBySource = parts.length == 2 ? new OrderBySource(parts[1], domainClass) : null;
		}

		private String detectAndSetAllIgnoreCase(String predicate) {
			Matcher matcher = ALL_IGNORE_CASE.matcher(predicate);
			if (matcher.find()) {
				alwaysIgnoreCase = true;
				//删除AllIgnoreCase关键字
				predicate = predicate.substring(0, matcher.start()) + predicate.substring(matcher.end(), predicate.length());
			}

			return predicate;
		}

		private void buildTree(String source, Class<?> domainClass) {
			String[] split = split(source, "Or");
			for (String part : split) {
				nodes.add(new OrPart(part, domainClass, alwaysIgnoreCase));
			}
		}

		public Iterator<OrPart> iterator() {
			return nodes.iterator();
		}

		public OrderBySource getOrderBySource() {
			return orderBySource;
		}
	}
    
	/**
	 * Or关键字切割之后的对象。持有多个由And切割之后的Part
	 */
	public static class OrPart implements Iterable<Part> {

		private final List<Part> children = new ArrayList<Part>();

		OrPart(String source, Class<?> domainClass, boolean alwaysIgnoreCase) {
			String[] split = split(source, "And");
			for (String part : split) {
				if (StringUtils.hasText(part)) {
					children.add(new Part(part, domainClass, alwaysIgnoreCase));
				}
			}
		}

		public Iterator<Part> iterator() {
			return children.iterator();
		}

		@Override
		public String toString() {
			return StringUtils.collectionToDelimitedString(children, " and ");
		}
	}

}

相关类

Part

源码

/**
 * 方法名的单个部分对象. 包含:操作类型,例如:IN。和 属性名(支持嵌套)
 */
public class Part {
	//是否大小写敏感正则
	private static final Pattern IGNORE_CASE = Pattern.compile("Ignor(ing|e)Case");
	// 属性对象
	private final PropertyPath propertyPath;
	// 操作类型
	private final Part.Type type;
	//大小写忽略的类型,默认是不忽略
	private IgnoreCaseType ignoreCase = IgnoreCaseType.NEVER;

	public Part(String source, Class<?> clazz) {
		this(source, clazz, false);
	}

	/**
	 * @param source repository经过or切割后的部分,再经过and切分出的方法名。例如:findByCategoryIdInAndDeletedStatus的CategoryIdIn和DeletedStatus
	 * @param clazz 实体类
	 * @param alwaysIgnoreCase 是否都忽略大小写
	 */
	public Part(String source, Class<?> clazz, boolean alwaysIgnoreCase) {
		// 如果source中含有大小写敏感关键字,则去除该关键字,并设置ignoreCase = IgnoreCaseType.ALWAYS
		String partToUse = detectAndSetIgnoreCase(source);
		if (alwaysIgnoreCase && ignoreCase != IgnoreCaseType.ALWAYS) {
			//如果接口参数指定忽略大小写,source中不含有忽略大小写的关键字,则设置忽略大小写类型为:在可忽略情况下进行忽略大小写
			this.ignoreCase = IgnoreCaseType.WHEN_POSSIBLE;
		}
  		// 遍历所有支持的type,如果partToUse是以type支持的关键字结尾的,则设置成该类型。如果都没匹配上,默认为SIMPLE_PROPERTY
		this.type = Type.fromProperty(partToUse);
   		// 设置方法名上的属性路径对象
  		// type.extractProperty会返回去除关键字后,首字符小写的可能属性名
		this.propertyPath = PropertyPath.from(type.extractProperty(partToUse), clazz);
	}

	private String detectAndSetIgnoreCase(String part) {
		// 匹配单个字段是否指定忽略大小写
		Matcher matcher = IGNORE_CASE.matcher(part);
		String result = part;
		if (matcher.find()) {
			ignoreCase = IgnoreCaseType.ALWAYS;
			result = part.substring(0, matcher.start()) + part.substring(matcher.end(), part.length());
		}
		return result;
	}
	
	//... 省略部分无关紧要的方法

	/**
	 * 方法名称部分所属的关键字类型。例如:CategoryIdIn对应的类型为IN。
	 */
	public static enum Type {
		BETWEEN(2, "IsBetween", "Between"), IS_NOT_NULL(0, "IsNotNull", "NotNull"), IS_NULL(0, "IsNull", "Null"), LESS_THAN(
				"IsLessThan", "LessThan"), LESS_THAN_EQUAL("IsLessThanEqual", "LessThanEqual"), GREATER_THAN("IsGreaterThan",
				"GreaterThan"), GREATER_THAN_EQUAL("IsGreaterThanEqual", "GreaterThanEqual"), BEFORE("IsBefore", "Before"), AFTER(
				"IsAfter", "After"), NOT_LIKE("IsNotLike", "NotLike"), LIKE("IsLike", "Like"), STARTING_WITH("IsStartingWith",
				"StartingWith", "StartsWith"), ENDING_WITH("IsEndingWith", "EndingWith", "EndsWith"), NOT_CONTAINING(
				"IsNotContaining", "NotContaining", "NotContains"), CONTAINING("IsContaining", "Containing", "Contains"), NOT_IN(
				"IsNotIn", "NotIn"), IN("IsIn", "In"), NEAR("IsNear", "Near"), WITHIN("IsWithin", "Within"), REGEX(
				"MatchesRegex", "Matches", "Regex"), EXISTS(0, "Exists"), TRUE(0, "IsTrue", "True"), FALSE(0, "IsFalse",
				"False"), NEGATING_SIMPLE_PROPERTY("IsNot", "Not"), SIMPLE_PROPERTY("Is", "Equals");

		// 需显示指定类型的列表,因为顺序很重要。例如:IS_NULL, IS_NOT_NULL
		private static final List<Part.Type> ALL = Arrays.asList(IS_NOT_NULL, IS_NULL, BETWEEN, LESS_THAN, LESS_THAN_EQUAL,
				GREATER_THAN, GREATER_THAN_EQUAL, BEFORE, AFTER, NOT_LIKE, LIKE, STARTING_WITH, ENDING_WITH, NOT_CONTAINING,
				CONTAINING, NOT_IN, IN, NEAR, WITHIN, REGEX, EXISTS, TRUE, FALSE, NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY);

		public static final Collection<String> ALL_KEYWORDS;
		static {
  			//所有支持的操作关键字
			List<String> allKeywords = new ArrayList<String>();
			for (Type type : ALL) {
				allKeywords.addAll(type.keywords);
			}
			ALL_KEYWORDS = Collections.unmodifiableList(allKeywords);
		}

		private final List<String> keywords;
		private final int numberOfArguments;

		/**
		 * @param numberOfArguments 关键字对应的变量个数 
		 * @param keywords 关键字
		 */
		private Type(int numberOfArguments, String... keywords) {
			this.numberOfArguments = numberOfArguments;
			this.keywords = Arrays.asList(keywords);
		}

		private Type(String... keywords) {
			this(1, keywords);
		}
 		
		//...忽略部分无关紧要的方法

		/**
		 * 解析出rawProperty对应的关键字类型
		 */
		public static Part.Type fromProperty(String rawProperty) {
 			//按规则顺序遍历所有类型进行匹配
			for (Part.Type type : ALL) {
				if (type.supports(rawProperty)) {
					return type;
				}
			}
  			//默认是SIMPLE_PROPERTY
			return SIMPLE_PROPERTY;
		}

		/**
		 * 是否是该类型支持的关键字。如果传入的属性是以类型支持的关键字结尾的,则认为是支持的。否则即为不支持
		 */
		protected boolean supports(String property) {
			if (keywords == null) {
				return true;
			}
			for (String keyword : keywords) {
				if (property.endsWith(keyword)) {
					return true;
				}
			}

			return false;
		}

		/**
		 * 解析出属性名。先将变量首字符小写,然后去除操作关键字。例如:extractProperty("CategoryIdIn") = categoryId
		 */
		public String extractProperty(String part) {
  			//将首字符小写
			String candidate = StringUtils.uncapitalize(part);
			for (String keyword : keywords) {
				if (candidate.endsWith(keyword)) {
					return candidate.substring(0, candidate.length() - keyword.length());
				}
			}
			return candidate;
		}
	}

	/**
	 * 支持忽略大小写的类型。针对String类型而言,如果不是String类型,则不支持忽略大小写
	 */
	public enum IgnoreCaseType {

		/**
		 * 不要忽略大小写
		 */
		NEVER,

		/**
		 * 总是忽略大小写,如果无法忽略大小写,则报错
		 */
		ALWAYS,

		/**
		 * 在可以忽略大小写的情况下忽略,否则不做任何处理
		 */
		WHEN_POSSIBLE
	}
}

PropertyPath

源码

/**
 * 实体属性路径抽象出的类
 */
public class PropertyPath implements Iterable<PropertyPath> {

	private static final String PARSE_DEPTH_EXCEEDED = "Trying to parse a path with depth greater than 1000! This has been disabled for security reasons to prevent parsing overflows.";

	private static final String DELIMITERS = "_\\.";
	private static final String ALL_UPPERCASE = "[A-Z0-9._$]+";
	private static final Pattern SPLITTER = Pattern.compile("(?:[%s]?([%s]*?[^%s]+))".replaceAll("%s", DELIMITERS));
	// 实体类型的信息。包括:属性名,属性类型等
	private final TypeInformation<?> owningType;
	// 属性名
	private final String name;
	// 属性的实际类型信息。如果是集合类,则返回集合对象的类型。如果是map类,则返回value的类型
	private final TypeInformation<?> type;
	// 属性的类型是否是集合类
	private final boolean isCollection;
	// 下一个属性节点(嵌套)
	private PropertyPath next;

	PropertyPath(String name, Class<?> owningType) {
		this(name, ClassTypeInformation.from(owningType), Collections.<PropertyPath> emptyList());
	}

	/**
	 * 创建一个PropertyPath叶子节点(没有嵌套节点)
	 * 
	 * @param base 之前已经找到的PropertyPath
	 */
	PropertyPath(String name, TypeInformation<?> owningType, List<PropertyPath> base) {
		String propertyName = name.matches(ALL_UPPERCASE) ? name : StringUtils.uncapitalize(name);
   		// 属性的类型对象
		TypeInformation<?> propertyType = owningType.getProperty(propertyName);

		if (propertyType == null) {
			throw new PropertyReferenceException(propertyName, owningType, base);
		}
		// 实体类型
		this.owningType = owningType;
  		// 是否是集合。例如:Collection家族相关类或Array等
		this.isCollection = propertyType.isCollectionLike();
  		// 属性的实际类型。如果是集合类,则返回集合对象的类型。如果是map类,则返回value的类型
		this.type = propertyType.getActualType();
		// 属性名
		this.name = propertyName;
	}

	/**
	 * 解析出PropertyPath
	 * 
	 * @param source 字符串属性
	 * @param type 实体类型
	 * @return
	 */
	public static PropertyPath from(String source, Class<?> type) {
		return from(source, ClassTypeInformation.from(type));
	}

	public static PropertyPath from(String source, TypeInformation<?> type) {
		List<String> iteratorSource = new ArrayList<String>();
		// "_"是一个保留字符。可以通过该字符显示指定字符切分的顺序。例如:findByAddress_ZipCode
		Matcher matcher = SPLITTER.matcher("_" + source);

		while (matcher.find()) {
			iteratorSource.add(matcher.group(1));
		}

		Iterator<String> parts = iteratorSource.iterator();

		PropertyPath result = null;
		// 当前属性的 path栈,用于存储嵌套的所有path
		Stack<PropertyPath> current = new Stack<PropertyPath>();

		while (parts.hasNext()) {
			if (result == null) {
				result = create(parts.next(), type, current);
				current.push(result);
			} else {
				current.push(create(parts.next(), current));
			}
		}

		return result;
	}

	private static PropertyPath create(String source, Stack<PropertyPath> base) {

		PropertyPath previous = base.peek();

		PropertyPath propertyPath = create(source, previous.type, base);
		previous.next = propertyPath;
		return propertyPath;
	}

	/**
	 * 工厂方法:为给定的source和类型信息 创建对应的PropertyPath对象。它将检查给定source的 驼峰命名 部分,并从整个部分开始遍历,然后从右侧切掉部分。 只要找到给定类的有效属性,就将遍历尾部以找到刚刚找到的类的次要属性,依此类推
	 */
	private static PropertyPath create(String source, TypeInformation<?> type, List<PropertyPath> base) {
		return create(source, type, "", base);
	}

	/**
	 * 尝试通过首先尝试给定的源来查找{@link PropertyPath}链。 如果失败,它将在驼峰案例边界处(从右侧开始)将源分开,并尝试从计算的头部中查找{@link PropertyPath},并重新组合新的尾部和其他尾部。
	* 解析算法首先将整个part(AddressZipCode)解释为属性,并使用该名称(uncapitalized)检查域类的属性。如果算法成功,就使用该属性。如果不是,就拆分右侧驼峰部分的信号源到头部和尾部,并试图找出相应的属性,在我们的例子中是AddressZip和Code。如果算法找到一个具有头部的属性,那么它需要尾部,并从那里继续构建树,然后按照刚刚描述的方式将尾部分割。如果第一个分割不匹配,就将分割点移动到左边(Address、ZipCode),然后继续。
	* 官方说明文档:https://docs.spring.io/spring-data/jpa/docs/2.1.5.RELEASE/reference/html/#repositories.query-methods.query-property-expressions
	 */
	private static PropertyPath create(String source, TypeInformation<?> type, String addTail, List<PropertyPath> base) {
		// 最大允许的路径深度
		if (base.size() > 1000) {
			throw new IllegalArgumentException(PARSE_DEPTH_EXCEEDED);
		}

		PropertyReferenceException exception = null;
		PropertyPath current = null;

		try {
			// 用给定的source创建一个path对象
			current = new PropertyPath(source, type, base);

			if (!base.isEmpty()) {
				// 栈顶的下一个属性path指向新建的path
				base.get(base.size() - 1).next = current;
			}

			List<PropertyPath> newBase = new ArrayList<PropertyPath>(base);
			newBase.add(current);

			if (StringUtils.hasText(addTail)) {
				current.next = create(addTail, current.type, newBase);
			}

			return current;

		} catch (PropertyReferenceException e) {
			// 属性名不合法
            
			if (current != null) {
				throw e;
			}

			exception = e;
		}
		// 匹配结尾处的驼峰命名
		// \\p{Lu}+:1个或多个大写字符
		// \\p{Ll}*:任意个小写字符
		Pattern pattern = Pattern.compile("\\p{Lu}+\\p{Ll}*$");
		Matcher matcher = pattern.matcher(source);

		if (matcher.find() && matcher.start() != 0) {

			int position = matcher.start();
  			// 头部
			String head = source.substring(0, position);
  			// 尾部
			String tail = source.substring(position);

			try {
				return create(head, type, tail + addTail, base);
			} catch (PropertyReferenceException e) {
				throw e.hasDeeperResolutionDepthThan(exception) ? e : exception;
			}
		}

		throw exception;
	}
}

总结

属性名解析算法

  • 假设: 一个Person实体对象里面有一个Address属性里面包含一个ZipCode属性。在这种情况下,方法名为:
    List<Person> findByAddressZipCode(ZipCode zipCode);
  • 解析算法: 首先将整个part(AddressZipCode)解释为属性,并使用该名称(首字符小写)检查域类的属性。如果算法成功,就使用该属性。如果不是,就拆分右侧驼峰部分的信号源到头部和尾部,并试图找出相应的属性,在我们的例子中是AddressZip和Code。如果算法找到一个具有头部的属性,那么它需要尾部,并从那里继续构建树,然后按照刚刚描述的方式将尾部分割。如果第一个分割不匹配,就将分割点移动到左边(Address、ZipCode),然后继续。
  • 算法的缺点:可能会选择错误的属性。假设Person该类也具有一个addressZip属性。该算法将在第一个拆分回合中匹配,选择错误的属性,然后失败(因为addressZip可能的类型没有code属性)
    • 解决方案:使用下划线"_" 手动指定遍历点。例如:findByAddress_ZipCode。
    • 注意:官方文档强烈建议遵循以下标准Java命名约定(即,在属性名称中不使用下划线,而使用驼峰大小写)。所以非特殊情况,建议还是使用驼峰命名法即可

自定义方法名解析成PartTree的简单流程

参考