本文價(jià)值在于 包掃描的原理探究和實(shí)現(xiàn)
創(chuàng)新互聯(lián)公司是一家專(zhuān)注于做網(wǎng)站、網(wǎng)站制作與策劃設(shè)計(jì),龍門(mén)網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)公司做網(wǎng)站,專(zhuān)注于網(wǎng)站建設(shè)十多年,網(wǎng)設(shè)計(jì)領(lǐng)域的專(zhuān)業(yè)建站公司;建站業(yè)務(wù)涵蓋:龍門(mén)等地區(qū)。龍門(mén)做網(wǎng)站價(jià)格咨詢(xún):13518219792
一、背景
項(xiàng)目開(kāi)發(fā)中,使用Netty做服務(wù)端,保持長(zhǎng)連接與客戶(hù)端(agent)通訊。Netty服務(wù)端需要根據(jù)不同消息類(lèi)型,加載對(duì)應(yīng)的Processer(消息處理器)對(duì)消息進(jìn)行處理。問(wèn)題就出現(xiàn)了,Processer會(huì)隨著消息業(yè)務(wù)類(lèi)型增多進(jìn)行擴(kuò)展,每一次增加Processer都需要手動(dòng)new出來(lái)一個(gè)實(shí)例,放到Map里(key為消息類(lèi)型碼,value為Processer實(shí)例),供調(diào)度程序(ProcesserManager)根據(jù)端消息類(lèi)型調(diào)度,顯然這是件很麻煩的一件事,不僅操作瑣碎,也不符合低耦合、模塊化的設(shè)計(jì)思想。
二、解決思路
我們所寫(xiě)的每一個(gè)Processer都是IProcessor這個(gè)接口的實(shí)現(xiàn):
public interface IProcessor {
void process(BaseMsgWrapper msg) throws Exception;
EventEnum getType();
default String getIpFromChannelContext(ChannelHandlerContext ctx){
String[] ipPort = ctx.channel().remoteAddress().toString().split(":");
return ipPort[0].substring(1);
}
}
其中:
void process(BaseMsgWrapper msg) 為消息處理方法
void getIpFromChannelContext (BaseMsgWrapper msg) 為獲取客戶(hù)端ip的默認(rèn)方法
假如我們?cè)贜etty服務(wù)端啟動(dòng)時(shí),能獲取該接口的所有實(shí)現(xiàn)類(lèi),然后把這些實(shí)現(xiàn)類(lèi)分別new出來(lái),放到Map中,那么這個(gè)工作就可以自動(dòng)化掉了。
最終實(shí)現(xiàn)的效果就是 消息處理器只要 implements IProcessor接口,就會(huì)被自動(dòng)加載調(diào)用,而不再需要手動(dòng)寫(xiě)到Map中。這樣就將ProcesserManager 與 Processer解耦開(kāi)了。
為此,IProcessor接口需要增加一個(gè)方法
EventEnum getType();
即需要Processer表明自己對(duì)應(yīng)的消息類(lèi)型,沒(méi)這個(gè)方法之前,我們都是在put進(jìn)Map的時(shí)候,手動(dòng)把消息類(lèi)型寫(xiě)進(jìn)去的(可以想象之前的做法多么的low)
三、實(shí)現(xiàn)過(guò)程
想法是很好,但實(shí)現(xiàn)不是那么容易,踩了很多坑。
首先是網(wǎng)上查資料,看看其他人都怎么做的,有沒(méi)有做好的輪子。
(Java – 獲取指定接口的所有實(shí)現(xiàn)類(lèi)或獲取指定類(lèi)的所有繼承類(lèi))
這篇博客提供的大致思路:
1) 獲取當(dāng)前線程的ClassLoader
2) 通過(guò)ClassLoader獲取當(dāng)前工作目錄,對(duì)目錄下的文件進(jìn)行遍歷掃描。
3) 過(guò)濾出以.class為后綴的類(lèi)文件,并加載類(lèi)到list中
4) 對(duì)list中所有類(lèi)進(jìn)行校驗(yàn),判斷是否為指定接口的實(shí)現(xiàn)類(lèi),并排除自身。
5) 返回所有符合條件的類(lèi)。
這個(gè)思路是對(duì)的,但是考慮不全,不能拿來(lái)工程應(yīng)用,另外博文中提供的源碼應(yīng)該只是一個(gè)實(shí)驗(yàn)代碼,有不少缺陷。
1)這個(gè)方?jīng)]有考慮不同的文件格式。當(dāng)程序打成jar包,發(fā)布運(yùn)行時(shí),上述的這種遍歷file的操作 就失效了。
2)局限性。只能掃描到當(dāng)前方法的同級(jí)目錄及其子目錄。無(wú)法覆蓋整個(gè)模塊。
3)遍歷文件的邏輯太啰嗦,可以簡(jiǎn)化。
4)通過(guò)ClassLoader獲取當(dāng)前工作目錄時(shí),使用了“…/bin/”這么一個(gè)固定的目錄名。
Enumeration enumeration = classLoader.getResources("…/bin/" + path)
事實(shí)上,不同的IDE(主要是eclipse 和 idea)項(xiàng)目的資源目錄,在這一點(diǎn)上是不同的。
(獲取全部子類(lèi)或接口的全部實(shí)現(xiàn))
這篇博客考慮到了在運(yùn)行環(huán)境中,需要通過(guò)JarFile工具類(lèi)進(jìn)行單獨(dú)處理。
局限性:
需要手動(dòng)指定要掃描的Jar文件或目錄,沒(méi)有通過(guò)ClassLoader 自動(dòng)獲取當(dāng)前運(yùn)行的上下文。
此外classLoader.getResource 獲得的 資源目錄 是個(gè)URL對(duì)象,如何轉(zhuǎn)換成JarFile對(duì)象 花費(fèi)了我不少時(shí)間求索:
JarURLConnection jarURLConnection = (JarURLConnection)url.openConnection();
JarFile jarFile = jarURLConnection.getJarFile();
綜合上述思路和自己的試驗(yàn)研究,得出獲取接口所有實(shí)現(xiàn)類(lèi)的算法流程如下:
四、代碼實(shí)現(xiàn)
package com.hikvision.hummer.pandora.gateway.proc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
獲取接口的所有實(shí)現(xiàn)類(lèi) 理論上也可以用來(lái)獲取類(lèi)的所有子類(lèi)
查詢(xún)路徑有限制,只局限于接口所在模塊下,比如pandora-gateway,而非整個(gè)pandora(會(huì)遞歸搜索該文件夾下所以的實(shí)現(xiàn)類(lèi))
路徑中不可含中文,否則會(huì)異常。若要支持中文路徑,需對(duì)該模塊代碼中url.getPath() 返回值進(jìn)行urldecode.
Created by wangzhen3 on 2017/6/23.
*/
public class ClassUtil {
private static final Logger LOG = LoggerFactory.getLogger(ClassUtil.class);
public static ArrayList getAllClassByInterface(Class clazz) {
ArrayList list = new ArrayList<>();
// 判斷是否是一個(gè)接口
if (clazz.isInterface()) {
try {
ArrayList allClass = getAllClass(clazz.getPackage().getName());
/**
* 循環(huán)判斷路徑下的所有類(lèi)是否實(shí)現(xiàn)了指定的接口 并且排除接口類(lèi)自己
/
for (int i = 0; i < allClass.size(); i++) {
/*
* 判斷是不是同一個(gè)接口
*/
// isAssignableFrom:判定此 Class 對(duì)象所表示的類(lèi)或接口與指定的 Class
// 參數(shù)所表示的類(lèi)或接口是否相同,或是否是其超類(lèi)或超接口
if (clazz.isAssignableFrom(allClass.get(i))) {
if (!clazz.equals(allClass.get(i))) {
// 自身并不加進(jìn)去
list.add(allClass.get(i));
}
}
}
} catch (Exception e) {
LOG.error(“出現(xiàn)異常{}”,e.getMessage());
throw new RuntimeException(“出現(xiàn)異常”+e.getMessage());
}
}
LOG.info(“class list size :”+list.size());
return list;
}
/**
從一個(gè)指定路徑下查找所有的類(lèi)
@param packagename
*/
private static ArrayList getAllClass(String packagename) {
LOG.info(“packageName to search:”+packagename);
List classNameList = getClassName(packagename);
ArrayList list = new ArrayList<>();
for(String className : classNameList){
try {
list.add(Class.forName(className));
} catch (ClassNotFoundException e) {
LOG.error(“l(fā)oad class from name failed:”+className+e.getMessage());
throw new RuntimeException(“l(fā)oad class from name failed:”+className+e.getMessage());
}
}
LOG.info(“find list size :”+list.size());
return list;
}
/**
獲取某包下所有類(lèi)
@param packageName 包名
@return 類(lèi)的完整名稱(chēng)
*/
public static List getClassName(String packageName) {
List fileNames = null;
ClassLoader loader = Thread.currentThread().getContextClassLoader();
String packagePath = packageName.replace(".", “/”);
URL url = loader.getResource(packagePath);
if (url != null) {
String type = url.getProtocol();
LOG.debug("file type : " + type);
if (type.equals(“file”)) {
String fileSearchPath = url.getPath();
LOG.debug(“fileSearchPath: “+fileSearchPath);
fileSearchPath = fileSearchPath.substring(0,fileSearchPath.indexOf(”/classes”));
LOG.debug("fileSearchPath: "+fileSearchPath);
fileNames = getClassNameByFile(fileSearchPath);
} else if (type.equals(“jar”)) {
try{
JarURLConnection jarURLConnection = (JarURLConnection)url.openConnection();
JarFile jarFile = jarURLConnection.getJarFile();
fileNames = getClassNameByJar(jarFile,packagePath);
}catch (java.io.IOException e){
throw new RuntimeException(“open Package URL failed:”+e.getMessage());
}
}else{
throw new RuntimeException("file system not support! cannot load MsgProcessor!");
}
}
return fileNames;
}
/**
從項(xiàng)目文件獲取某包下所有類(lèi)
@return 類(lèi)的完整名稱(chēng)
*/
private static List getClassNameByFile(String filePath) {
List myClassName = new ArrayList();
File file = new File(filePath);
File[] childFiles = file.listFiles();
for (File childFile : childFiles) {
if (childFile.isDirectory()) {
myClassName.addAll(getClassNameByFile(childFile.getPath()));
} else {
String childFilePath = childFile.getPath();
if (childFilePath.endsWith(".class")) {
childFilePath = childFilePath.substring(childFilePath.indexOf("\classes") + 9, childFilePath.lastIndexOf("."));
childFilePath = childFilePath.replace("\", “.”);
myClassName.add(childFilePath);
}
}
}
return myClassName;
}
/**
從jar獲取某包下所有類(lèi)
@return 類(lèi)的完整名稱(chēng)
*/鄭州人流醫(yī)院哪家好 http://www.89906662.com/
private static List getClassNameByJar(JarFile jarFile ,String packagePath) {
List myClassName = new ArrayList();
try {
Enumeration entrys = jarFile.entries();
while (entrys.hasMoreElements()) {
JarEntry jarEntry = entrys.nextElement();
String entryName = jarEntry.getName();
//LOG.info(“entrys jarfile:”+entryName);
if (entryName.endsWith(".class")) {
entryName = entryName.replace("/", “.”).substring(0, entryName.lastIndexOf("."));
myClassName.add(entryName);
//LOG.debug(“Find Class :”+entryName);
}
}
} catch (Exception e) {
LOG.error(“發(fā)生異常:”+e.getMessage());
throw new RuntimeException(“發(fā)生異常:”+e.getMessage());
}
return myClassName;
}
}
五、項(xiàng)目應(yīng)用
接口IProcessor
*/
public interface IProcessor {
void process(BaseMsgWrapper msg) throws Exception;
EventEnum getType();
default String getIpFromChannelContext(ChannelHandlerContext ctx){
String[] ipPort = ctx.channel().remoteAddress().toString().split(":");
return ipPort[0].substring(1);
}
}
接口實(shí)現(xiàn)類(lèi)HeartBeatMsgProcessor, 主要 加了注解@Component
@Component
public class HeartBeatMsgProcessor implements IProcessor {
private static final HikGaLogger logger = HikGaLoggerFactory.getLogger(HeartBeatMsgProcessor.class);
@Override
public EventEnum getType(){
return EventEnum.HEART_BEAT;
}
private BaseMsg bmsg = new BaseMsg( "requestId-null", EventEnum.HEART_BEAT.getEventType(),1L, Constants.ASYN_INVOKE,
"pong", "uuid-null", Constants.ZH_CN);
@Override
public void process(BaseMsgWrapper msg) throws Exception {
Assert.notNull(msg);
logger.debug("ping from [{}]", msg.getCtx().channel().remoteAddress().toString());
msg.getCtx().writeAndFlush(bmsg);
}
}
調(diào)用ClassUtil 獲取接口的所有類(lèi),并根據(jù)查找到的類(lèi)從spring容器中取出bean.
private ProcessorManager(){
List classList = ClassUtil.getAllClassByInterface(IProcessor.class);
LOG.info("processor num :"+classList.size());
for(Class classItem : classList){
IProcessor msgProcessor = null;
try{
msgProcessor = (IProcessor) AppContext.getBean(classItem);
processorMap.put(msgProcessor.getType(),msgProcessor);
}catch (Exception e){
LOG.error("加載腳本處理器:[{}]失敗:[{}]!",classItem.getName(),e.getMessage());
throw new RuntimeException("加載腳本處理器"+classItem.getName()+"失敗");
}
LOG.info("加載腳本處理器成功:[{}] MsgType:[{}] ", classItem.getName(), msgProcessor.getType());
}
LOG.info("腳本處理器加載完成!");
}
代碼中AppContext是對(duì)springContext 的封裝,實(shí)現(xiàn)了ApplicationContextAware接口,目的是從Spring容器取出指定類(lèi)的實(shí)例。代碼見(jiàn)附錄1.
六、更進(jìn)一步
本文通過(guò)研究Java接口實(shí)現(xiàn)類(lèi)的自動(dòng)掃描加載,達(dá)成接口與調(diào)度程序的解耦,拓展了Java接口在代碼解耦方面的應(yīng)用價(jià)值。
雖然是獲取接口所有實(shí)現(xiàn)類(lèi),但對(duì)獲取類(lèi)的所有子類(lèi),同樣適用。
另外基于此功能,可以通過(guò)反射分析掃描上來(lái)的Class, 獲知哪些類(lèi)使用了自定義注解,然后應(yīng)用注解處理器,完成注解處理器的自動(dòng)執(zhí)行。
七、最簡(jiǎn)單的實(shí)現(xiàn)方法
ApplicationContext 的 getBeansOfType 方法已經(jīng)封裝了該實(shí)現(xiàn),可以直接調(diào)用。
AppContext 見(jiàn)附錄1
IProcessor 為接口。Map processorBeanMap 為返回值,key 為beanName ,value為 bean.
接口IProcessor實(shí)現(xiàn)類(lèi)上 要加上注解@Component
Map processorBeanMap = null;
try {
processorBeanMap = AppContext.getContext().getBeansOfType(IProcessor.class);
}catch (BeansException e){
throw new RuntimeException(“加載腳本處理器Bean失敗!”);
}
調(diào)用AppContext 時(shí),AppContex 必須已經(jīng)被容器優(yōu)先注入,否則可能會(huì)出現(xiàn)applicaitonContext未注入的報(bào)錯(cuò)。
可以在調(diào)用類(lèi)上,加上注解 @DependsOn(“appContext”) 來(lái)控制appContext 優(yōu)先加載。
八.借助ServiceLoader類(lèi)
ServiceLoader是JDK自帶的一個(gè)類(lèi)加載器,位于java.util包當(dāng)中,作為 A simple service-provider loading facility. 具體使用方式如下:
1.在META-INF/services/目錄下用你的接口全路徑名稱(chēng)命名一個(gè)文件(不加后綴),然后在該文件中一行一個(gè)添加你的接口實(shí)現(xiàn)類(lèi)的全路徑名。
2.通過(guò)load方法來(lái)加載出所有的接口實(shí)現(xiàn)類(lèi)
附錄1 AppContext
package com.hikvision.hummer.pandora.common.context;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class AppContext implements ApplicationContextAware {
private static ApplicationContext context = null;
/**
* 實(shí)現(xiàn)ApplicationContextAware接口的context注入函數(shù), 將其存入靜態(tài)變量
*/
public void setApplicationContext(ApplicationContext context) {
AppContext.setContext(context);
}
/**
* 取得存儲(chǔ)在靜態(tài)變量中的ApplicationContext.
*/
public static ApplicationContext getContext() {
if (context == null)
throw new IllegalStateException("applicaitonContext未注入,請(qǐng)?jiān)赼pplicationContext.xml中定義AppContext");
return context;
}
/**
* 存儲(chǔ)靜態(tài)變量中的ApplicationContext.
*/
public static void setContext(ApplicationContext context) {
AppContext.context = context;
}
/**
* 從靜態(tài)變量ApplicationContext中取得Bean, 自動(dòng)轉(zhuǎn)型為所賦值對(duì)象的類(lèi)型
*/
@SuppressWarnings("unchecked")
public static T getBean(String name) {
if (context == null)
throw new IllegalStateException("applicaitonContext未注入,請(qǐng)?jiān)赼pplicationContext.xml中定義AppContext");
try {
return (T) context.getBean(name);
} catch (BeansException e) {
e.printStackTrace();
}
return (T) null;
}
/**
* 從靜態(tài)變量ApplicationContext中取得Bean, 自動(dòng)轉(zhuǎn)型為所賦值對(duì)象的類(lèi)型
*/
@SuppressWarnings("unchecked")
public static T getBean(Class tClass) {
if (context == null)
throw new IllegalStateException("applicaitonContext未注入,請(qǐng)?jiān)赼pplicationContext.xml中定義AppContext");
try {
return context.getBean(tClass);
} catch (BeansException e) {
e.printStackTrace();
}
return null;
}
}