在EIP的概念中,數據格式的轉換是不可避免的一環,而作爲EIP理論實現者的Apache Camel在多年的發展迭代中也是給出了多種解決方案,本文將集中討論其中的一種實現方法 —— TypeConverter。
1. 概述
TypeConverter作爲Apache Camel中實現自動數據格式轉換的主要方式,瞭解其底層實現邏輯對我們實現自定義TypeConverter,以及更好地使用該功能都是大有裨益地。
2. 源碼解讀
本小節分爲兩個部分。
2.1 初始化
首先讓我們看看Apache Camel如何初始化TypeConverter相關組件的。
在之前的一篇博客 Apache Camel源碼研究之啓動 中,我們提到 Camel在啓動時候會調用CamelContext.forceLazyInitialization();
來將一些懶加載的基礎必要組件進行率先強制喚醒,而這其中正有本次的主角 —— TypeConverter組件。
通過上述截圖我們可以看到,DefaultTypeConverter
組件覆寫了基類的doStart()
方法,參與到Camel啓動生命週期中:
- 首先通過
loadCoreTypeConverters();
加載camel-core
組件中定義的TypeConverter。(實現類CoreTypeConverterLoader
[直接繼承自AnnotationTypeConverterLoader
],通過覆寫基類的findPackageNames()
方法指定掃描PACKAGE爲{"org.apache.camel.converter", "org.apache.camel.component.bean", "org.apache.camel.component.file"}
,好吧,其實往後看你們就知道,這個配置對於掃描@Converter並沒有起到作用,真正發揮作用的是CorePackageScanClassResolver
類,其在構造函數中顯式進行了添加。) - 然後通過
loadTypeConverters();
來加載外部組件擴展的TypeConverter。(實現類AnnotationTypeConverterLoader
)。常見的外部擴展有:- org.apache.camel.component.jackson.converter.JacksonTypeConverters
- org.apache.camel.component.cxf.converter.CxfPayloadConverter
- org.apache.camel.spring.converter.ResourceConverter
- org.apache.camel.component.http.RequestEntityConverter
- org.apache.camel.component.exec.ExecResultConverter
- org.apache.camel.component.jetty.JettyConverter
- org.apache.camel.http.common.HttpConverter
- org.apache.camel.component.cxf.converter.CxfConverter
關於以上兩次加載,最終的實現邏輯都將歸結於load(TypeConverterRegistry registry)
方法。
// AnnotationTypeConverterLoader.load() 實現自接口TypeConverterLoader
@Override
public void load(TypeConverterRegistry registry) throws TypeConverterLoaderException {
String[] packageNames;
// META_INF_SERVICES 值爲: "META-INF/services/org/apache/camel/TypeConverter" (相信部分讀者一定很眼熟)
LOG.trace("Searching for {} services", META_INF_SERVICES);
try {
// 查找進行@Converter掃描的package
// 子類 CoreTypeConverterLoader 正是通過覆寫該方法來實現"指定掃描路徑"的
packageNames = findPackageNames();
if (packageNames == null || packageNames.length == 0) {
throw new TypeConverterLoaderException("Cannot find package names to be used for classpath scanning for annotated type converters.");
}
} catch (Exception e) {
throw new TypeConverterLoaderException("Cannot find package names to be used for classpath scanning for annotated type converters.", e);
}
// if we only have camel-core on the classpath then we have already pre-loaded all its type converters
// but we exposed the "org.apache.camel.core" package in camel-core. This ensures there is at least one
// packageName to scan, which triggers the scanning process. That allows us to ensure that we look for
// META-INF/services in all the JARs.
if (packageNames.length == 1 && "org.apache.camel.core".equals(packageNames[0])) {
LOG.debug("No additional package names found in classpath for annotated type converters.");
// no additional package names found to load type converters so break out
return;
}
// 過濾掉 對於 org.apache.camel.core PACKAGE的掃描.
// now filter out org.apache.camel.core as its not needed anymore (it was just a dummy)
packageNames = filterUnwantedPackage("org.apache.camel.core", packageNames);
// 外部擴展者可能直接註冊Converter類, 而非PACKAGE, 這一步正是將這兩者進行拆分
// 1. PACKAGE 作爲返回值
// 2. CLASS 存放到作爲參數傳入的 classes 中.
// filter out package names which can be loaded as a class directly so we avoid package scanning which
// is much slower and does not work 100% in all runtime containers
Set<Class<?>> classes = new HashSet<Class<?>>();
packageNames = filterPackageNamesOnly(resolver, packageNames, classes);
if (!classes.isEmpty()) {
LOG.debug("Loaded " + classes.size() + " @Converter classes");
}
// 對上面過濾出的PACKAGE進行掃描, 找出被 @Converter 註解的類. 收集到上面定義的 classes 字段中
// if there is any packages to scan and load @Converter classes, then do it
if (packageNames != null && packageNames.length > 0) {
LOG.trace("Found converter packages to scan: {}", packageNames);
Set<Class<?>> scannedClasses = resolver.findAnnotated(Converter.class, packageNames);
if (scannedClasses.isEmpty()) {
throw new TypeConverterLoaderException("Cannot find any type converter classes from the following packages: " + Arrays.asList(packageNames));
}
LOG.debug("Found " + packageNames.length + " packages with " + scannedClasses.size() + " @Converter classes to load");
classes.addAll(scannedClasses);
}
// 至此所有的Converter註冊類收集完畢,開始進行註冊TypeConverter的操作
// load all the found classes into the type converter registry
for (Class<?> type : classes) {
if (LOG.isTraceEnabled()) {
LOG.trace("Loading converter class: {}", ObjectHelper.name(type));
}
// 加載 Converter 方法, 詳見下方說明
loadConverterMethods(registry, type);
}
// now clear the maps so we do not hold references
visitedClasses.clear();
visitedURIs.clear();
}
// ====================================================
// AnnotationTypeConverterLoader.loadConverterMethods()
protected void loadConverterMethods(TypeConverterRegistry registry, Class<?> type) {
// 避免重複掃描
if (visitedClasses.contains(type)) {
return;
}
visitedClasses.add(type);
try {
Method[] methods = type.getDeclaredMethods();
CachingInjector<?> injector = null;
for (Method method : methods) {
//
// this may be prone to ClassLoader or packaging problems when the same class is defined
// in two different jars (as is the case sometimes with specs).
// 註解 @Converter
if (ObjectHelper.hasAnnotation(method, Converter.class, true)) {
boolean allowNull = false;
if (method.getAnnotation(Converter.class) != null) {
allowNull = method.getAnnotation(Converter.class).allowNull();
}
injector = handleHasConverterAnnotation(registry, type, injector, method, allowNull);
} else if (ObjectHelper.hasAnnotation(method, FallbackConverter.class, true)) {// 註解 @FallbackConverter
boolean allowNull = false;
if (method.getAnnotation(FallbackConverter.class) != null) {
allowNull = method.getAnnotation(FallbackConverter.class).allowNull();
}
injector = handleHasFallbackConverterAnnotation(registry, type, injector, method, allowNull);
}
}
// 遞歸處理基類
Class<?> superclass = type.getSuperclass();
if (superclass != null && !superclass.equals(Object.class)) {
loadConverterMethods(registry, superclass);
}
} catch (NoClassDefFoundError e) {
LOG.warn("Ignoring converter type: " + type.getCanonicalName() + " as a dependent class could not be found: " + e, e);
}
}
// ====================================================
// AnnotationTypeConverterLoader.handleHasConverterAnnotation()
private CachingInjector<?> handleHasConverterAnnotation(TypeConverterRegistry registry, Class<?> type,
CachingInjector<?> injector, Method method, boolean allowNull) {
// 合格的方法簽名:
// 1. 有且只有一個參數
// 2. 有兩個參數, 第二個參數類型爲Exchange
// 3. 返回值不能爲Void
if (isValidConverterMethod(method)) {
int modifiers = method.getModifiers();
// 方法不能爲abstract, 必須爲public
if (isAbstract(modifiers) || !isPublic(modifiers)) {
LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: " + method
+ " as a converter method is not a public and concrete method");
} else {
Class<?> toType = method.getReturnType();
// 返回值不能爲Void
if (toType.equals(Void.class)) {
LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: "
+ method + " as a converter method returns a void method");
} else {
// 傳入參數中的第一個參數的類型爲 fromType
// 返回值參數的類型爲 toType
// 註冊爲 StaticMethodTypeConverter 實例
Class<?> fromType = method.getParameterTypes()[0];
if (isStatic(modifiers)) {
// 這裏會調用 `BaseTypeConverterRegistry.addTypeConverter`方法來將StaticMethodTypeConverter實例註冊到 BaseTypeConverterRegistry實例的typeMappings字段中(ConcurrentMap<TypeMapping, TypeConverter>類型)
// 而上述typeMappings字段類型中的KEY值爲TypeMapping類型, 其不出意料地覆寫了hashCode和equal方法
// 而更重要的是 BaseTypeConverterRegistry.addTypeConverter() 方法中,對於重複性的TypeMapping, 默認情況下是覆蓋Override(即後註冊的同類型Converter將覆前面註冊的), 但可以通過配置 context.getTypeConverterRegistry().setTypeConverterExists 來更改
registerTypeConverter(registry, method, toType, fromType,
new StaticMethodTypeConverter(method, allowNull));
} else {
if (injector == null) {
injector = new CachingInjector<Object>(registry, CastUtils.cast(type, Object.class));
}
registerTypeConverter(registry, method, toType, fromType,
new InstanceMethodTypeConverter(injector, method, registry, allowNull));
}
}
}
} else {
LOG.warn("Ignoring bad converter on type: " + type.getCanonicalName() + " method: " + method
+ " as a converter method should have one parameter");
}
return injector;
}
以上,就是Apache Camel如何在初始化時候將TypeConverter相關組件加載組裝完畢。
2.2 運行時
對於註冊的 TypeConverter
如何生效,Apache Camel提供瞭如下兩種方式:
//
context.getTypeConverter().convertTo(String.class, file);
// 其實最終還是回調了上述方法
message.getBody(String.class);
關於相關源碼,感興趣的讀者可以參見 BaseTypeConverterRegistry.doConvertTo(final Class<?> type, final Exchange exchange, final Object value, final boolean tryConvert)
方法,因爲涉及到的內容較多,貼出有點湊字數嫌疑。Apache Camel作了非常細緻的處理,將可以做到的轉換基本做到了極致。對於BaseTypeConverterRegistry.doConvertTo
中的四個參數:
- 第一個參數爲將要轉換到的類型
- 第三個參數爲將要被轉換的數據。
3. 自定義擴展
最後我們來寫個自定義的擴展示例:
- 編寫如下代碼:
@Converter
public class TypeConverter_Tests extends CamelTestSupport {
@Test
public void customConvert() throws Exception {
CamelTestUtil.defaultPrepareTest2(new RouteBuilder() {
@SuppressWarnings("deprecation")
@Override
public void configure() throws Exception {
from("stream:in?promptMessage=Enter something:")
.setBody(constant(new Person()))
.convertBodyTo(String.class)
.to("stream:err");
}
});
}
// =================================
@Converter
public static String toStr(Person person) throws IOException {
return "111"+ person.toString();
}
}
- 接着在
META-INF/services/org/apache/camel/TypeConverter
文件中填入TypeConverter_Tests
類的完整命名xxx.yyy.zzz.TypeConverter_Tests
。 - 執行單元測試,結果如下:
4. Links
- Office Site
- 《Apache Camel Developer’s CookBook》 P104
- 《Camel In Action》 P88