基於Spring的包含特定註解bean的package掃描工具

本文出處:http://blog.csdn.net/chaijunkun/article/details/23921547,轉載請註明。由於本人不定期會整理相關博文,會對相應內容作出完善。因此強烈建議在原始出處查看此文。


Spring框架爲Java開發提供了很多便利的工具和編程方式,最近在研究LDAP認證,多數技術問題都已經搞定,但是針對LDAP的ODM(Object-Directory Mapping,也就是LDAP層面的ORM)還有些不足。


問題描述:

Spring項目中有一個名爲Spring LDAP的子項目,可以簡化查詢邏輯,但是其中的LDAPTemplate需要做手動的Mapping,另外在查詢時默認使用的是類似於SQL中的select *,這樣也許會造成很多的網絡流量浪費和性能下降。如果需指定返回哪些字段,必須輸入一個String[]。這樣做的結果就是,同樣的一個查詢,既要指定字段的String[],又要將返回的字段一個一個地Mapping到Bean屬性上,一旦將來字段增加或者減少,需要維護兩個地方,增加了出錯的機率。於是想到了能否像JPA的註解方式那樣來配置ODM,後來查閱相關資料,還真有——使用OdmManagerImplFactoryBean。

首先需要在被映射的對象上增加Entry註解,然後在Bean屬性上增加對應的Attribute註解就完成了映射。問題出來了,除了要加入註解外,還要將這些Bean加入到OdmManagerImplFactoryBean的managedClasses屬性中,通知管理器哪些Bean屬於受管Bean。這有點像早期的Spring對於Hibernate的支持。配置AnnotationSessionFactoryBean的時候需要設置annotatedClasses一樣,不過從Spring 2.5.6開始增加了packagesToScan參數設置,它的作用是從指定的包下面掃描全部帶有Entity、Embeddable、MappedSuperclass、org.hibernate.annotations.Entity.class註解的Bean,並進行管理。這樣做的好處就是你把需要ODM的Bean都統一放到同一個package下,然後讓配置去自動掃描,這樣在你增加或減少Bean的時候不用再去關心配置中哪些Bean是受管的。

代碼:

首先聲明,這個代碼是受org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean中scanPackages方法的啓發,借鑑了其中的大部分代碼,先貼出來:

package net.csdn.blog.chaijunkun.config;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.ClassUtils;


public class LoadPackageClasses {
	
	protected final Log logger = LogFactory.getLog(getClass());
	
	private static final String RESOURCE_PATTERN = "/**/*.class";
	
	private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
	
	private List<String> packagesList= new LinkedList<String>();
	
	private List<TypeFilter> typeFilters = new LinkedList<TypeFilter>();
	
	private Set<Class<?>> classSet= new HashSet<Class<?>>();
	
	/**
	 * 構造函數
	 * @param packagesToScan 指定哪些包需要被掃描,支持多個包"package.a,package.b"並對每個包都會遞歸搜索
	 * @param annotationFilter 指定掃描包中含有特定註解標記的bean,支持多個註解
	 */
	public LoadPackageClasses(String[] packagesToScan, Class<? extends Annotation>... annotationFilter){
		if (packagesToScan != null) {
			for (String packagePath : packagesToScan) {
				this.packagesList.add(packagePath);
			}
		}
		if (annotationFilter != null){
			for (Class<? extends Annotation> annotation : annotationFilter) {
				typeFilters.add(new AnnotationTypeFilter(annotation, false));
			}
		}
	}
	
	/**
	 * 將符合條件的Bean以Class集合的形式返回
	 * @return
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public Set<Class<?>> getClassSet() throws IOException, ClassNotFoundException {
		this.classSet.clear();
		if (!this.packagesList.isEmpty()) {
				for (String pkg : this.packagesList) {
					String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
							ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN;
					Resource[] resources = this.resourcePatternResolver.getResources(pattern);
					MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
					for (Resource resource : resources) {
						if (resource.isReadable()) {
							MetadataReader reader = readerFactory.getMetadataReader(resource);
							String className = reader.getClassMetadata().getClassName();
							if (matchesEntityTypeFilter(reader, readerFactory)) {
								this.classSet.add(Class.forName(className));
							}
						}
					}
				}
		}
		//輸出日誌
		if (logger.isInfoEnabled()){
			for (Class<?> clazz : this.classSet) {
				logger.info(String.format("Found class:%s", clazz.getName()));
			}
		}
		return this.classSet;
	}
	
	

	/**
	 * 檢查當前掃描到的Bean含有任何一個指定的註解標記
	 * @param reader
	 * @param readerFactory
	 * @return
	 * @throws IOException
	 */
	private boolean matchesEntityTypeFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException {
		if (!this.typeFilters.isEmpty()) {
			for (TypeFilter filter : this.typeFilters) {
				if (filter.match(reader, readerFactory)) {
					return true;
				}
			}
		}
		return false;
	}

}

接下來我們就可以來配置了(以下配置爲Spring的applicationContext.xml配置節選):

<bean id="loadPackageClasses" class="net.csdn.blog.chaijunkun.config.LoadPackageClasses">
	<constructor-arg value="net.csdn.blog.chaijunkun.ldap.entity" />
	<constructor-arg>
		<list>
			<value>org.springframework.ldap.odm.annotations.Entry</value>
		</list>
	</constructor-arg>
</bean>
<bean id="odmManager" class="org.springframework.ldap.odm.core.impl.OdmManagerImplFactoryBean">
	<property name="converterManager" ref="converterManager" />
	<property name="contextSource" ref="contextSource" />
	<property name="managedClasses">
		<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
			<property name="targetObject">
				<ref local="loadPackageClasses" />
			</property>
			<property name="targetMethod">
				<value>getClassSet</value>
			</property>
		</bean>
	</property>
</bean>

在上述配置中,指定了掃描的包爲net.csdn.blog.chaijunkun.ldap.entity,然後指定了篩選條件是包含org.springframework.ldap.odm.annotations.Entry註解(指定的註解必須在Class級,不能是property級和method級,也就是Bean頭部的註解)的所有Bean。

因爲獲取篩選出類的集合要注入到OdmManagerImplFactoryBean中的managedClasses屬性,類型爲Set<Class<?>>,所以我們需要調用getClassSet()方法,用其返回值進行注入。於是使用了一個MethodInvokingFactoryBean來實現。

實驗結果表明,加入了這個組件之後確實達到了預期的效果。另外由於LoadPackageClasses本身配置上很靈活,可以用於篩選任何帶有特定註解的Bean,所以其他類似的場合也可以使用。當然,我更希望OdmManagerImplFactoryBean中能自帶package掃描的配置,這樣會讓我們省好多事。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章