從零開始實現一個簡易的Java MVC框架(二)--實現Bean容器

zzzzbw發表於2018-07-04

專案準備

首先確保你擁有以下環境或者工具

  • idea
  • java 8
  • maven 3.3.X
  • lombok外掛

然後我們建立一個maven工程,編寫pom.xml引入一些需要的依賴

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <slf4j-api.version>1.7.25</slf4j-api.version>
    <lombok.version>1.16.20</lombok.version>
</properties>
<dependencies>
    <!-- SLF4J -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>${slf4j-api.version}</version>
    </dependency>
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>
複製程式碼

目前只需要lombok和log4j兩個依賴就可以完成前面幾個功能的實現,其他需要的依賴等到後面需要的時候再加。

接著把專案一些基本的包結構建立一下,如下圖

從零開始實現一個簡易的Java MVC框架(二)--實現Bean容器

resources資料夾下的log4j.properties檔案為log4j輸出格式化引數,大家可以根據自己的喜好和需求編寫,我自己的只是為了方便除錯使用的,下面是我自己的。

### 設定###
log4j.rootLogger = debug,stdout
### 輸出資訊到控制抬 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %c %d{ISO8601} -- %p -- %m%n
複製程式碼

建立工具類

為了方便後續程式碼的編寫,我們先建立工具類。

com.zbw.util包下建立兩個工具類:ValidateUtilClassUtil

ValidateUtil主要負責屬性的驗證,這個類的完整程式碼就不貼了,就是檢查各種型別的值是否為空或者是否不為空。

/**
 * 驗證相關工具類
 */
public final class ValidateUtil {

    /**
     * Object是否為null
     */
    public static boolean isEmpty(Object obj) {
        return obj == null;
    }

    /**
     * String是否為null或""
     */
    public static boolean isEmpty(String obj) {
        return (obj == null || "".equals(obj));
    }
    
    ...

    /**
     * Object是否不為null
     */
    public static boolean isNotEmpty(Object obj) {
        return !isEmpty(obj);
    }

    /**
     * String是否不為null或""
     */
    public static boolean isNotEmpty(String obj) {
        return !isEmpty(obj);
    }

    ...
}
複製程式碼

ClassUtil主要是Class的一些相關操作。這其中除了一些類常用的例項反射等操作,還有一個重要方法就是getPackageClass(),這個方法會遞迴遍歷傳入的包名下的所有類檔案,並返回一個Set<Class<?>>。等一下在實現Bean容器的時候就會使用這個方法來掃描獲取對應包下的所有類檔案。

/**
 * 類操作工具類
 */
@Slf4j
public final class ClassUtil {

    /**
     * file形式url協議
     */
    public static final String FILE_PROTOCOL = "file";

    /**
     * jar形式url協議
     */
    public static final String JAR_PROTOCOL = "jar";

    /**
     * 獲取classLoader
     */
    public static ClassLoader getClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * 獲取Class
     */
    public static Class<?> loadClass(String className) {
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            log.error("load class error", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 例項化class
     */
    @SuppressWarnings("unchecked")
    public static <T> T newInstance(String className) {
        try {
            Class<?> clazz = loadClass(className);
            return (T) clazz.newInstance();
        } catch (Exception e) {
            log.error("newInstance error", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 例項化class
     */
    @SuppressWarnings("unchecked")
    public static <T> T newInstance(Class<?> clazz) {
        try {
            return (T) clazz.newInstance();
        } catch (Exception e) {
            log.error("newInstance error", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 設定類的屬性值
     */
    public static void setField(Field field, Object target, Object value) {
        setField(field, target, value, true);
    }

    /**
     * 設定類的屬性值
     */
    public static void setField(Field field, Object target, Object value, boolean accessible) {
        field.setAccessible(accessible);
        try {
            field.set(target, value);
        } catch (IllegalAccessException e) {
            log.error("setField error", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 獲取包下類集合
     */
    public static Set<Class<?>> getPackageClass(String basePackage) {
        URL url = getClassLoader()
                .getResource(basePackage.replace(".", "/"));
        if (null == url) {
            throw new RuntimeException("無法獲取專案路徑檔案");
        }
        try {
            if (url.getProtocol().equalsIgnoreCase(FILE_PROTOCOL)) {
                // 若為普通資料夾,則遍歷
                File file = new File(url.getFile());
                Path basePath = file.toPath();
                return Files.walk(basePath)
                        .filter(path -> path.toFile().getName().endsWith(".class"))
                        .map(path -> getClassByPath(path, basePath, basePackage))
                        .collect(Collectors.toSet());
            } else if (url.getProtocol().equalsIgnoreCase(JAR_PROTOCOL)) {
                // 若在 jar 包中,則解析 jar 包中的 entry
                JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
                return jarURLConnection.getJarFile()
                        .stream()
                        .filter(jarEntry -> jarEntry.getName().endsWith(".class"))
                        .map(ClassUtil::getClassByJar)
                        .collect(Collectors.toSet());
            }
            return Collections.emptySet();
        } catch (IOException e) {
            log.error("load package error", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 從Path獲取Class
     */
    private static Class<?> getClassByPath(Path classPath, Path basePath, String basePackage) {
        String packageName = classPath.toString().replace(basePath.toString(), "");
        String className = (basePackage + packageName)
                .replace("/", ".")
                .replace("\\", ".")
                .replace(".class", "");
        return loadClass(className);
    }

    /**
     * 從jar包獲取Class
     */
    private static Class<?> getClassByJar(JarEntry jarEntry) {
        String jarEntryName = jarEntry.getName();
        // 獲取類名
        String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
        return loadClass(className);
    }
}

複製程式碼

實現Bean容器

現在開始可以實現Bean容器了。

基礎註解

在spring中我們總是用各種註解去標註我們的元件,如controller等。所以我們也要先寫一些註解來標註一些必要的元件。在zbw.core包下再建立一個annotation包,然後再建立四個最基本的元件.

// Component註解,用於標記元件
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}
// Controller註解,用於標記Controller層的元件
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
// Repository註解,用於標記Dao層的元件
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Repository {
}
// Service註解,用於標記Service層的元件
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}
複製程式碼

這四個註解都是隻能標註在類上的,他們實際上沒有任何作用,只是用來標記這個類的,我們在後面的類集合中就可以很方便的獲取和區分被這些註解標記的類。

BeanContainer

Bean容器實際上就是存放所有Bean的地方,即Class以及相關資訊對應其實體的容器,為什麼稱之為'Bean'呢,因為在spring中,定義Class資訊和例項的東西叫BeanDefinition。這是一個介面,他有一個模板類AbstractBeanDefinition,這裡面就有一個beanClass變數存放Class類和propertyValues變數存放類屬性,以及很多類相關引數和初始化之類的引數。大家可以去spring中看看,spring的所有都是依賴於這個Bean生成的,可以說這是spring的基石。

瞭解到這個以後接下來就可以開始編寫Bean容器了,在zbw.core包下建立一個類叫BeanContainer

/**
 * Bean容器
 */
@Slf4j
public class BeanContainer {
    /**
     * 存放所有Bean的Map
     */
    private final Map<Class<?>, Object> beanMap = new ConcurrentHashMap<>();

    /**
     * 獲取Bean例項
     */
    public Object getBean(Class<?> clz) {
        if (null == clz) {
            return null;
        }
        return beanMap.get(clz);
    }

    /**
     * 獲取所有Bean集合
     */
    public Set<Object> getBeans() {
        return new HashSet<>(beanMap.values());
    }

    /**
     * 新增一個Bean例項
     */
    public Object addBean(Class<?> clz, Object bean) {
        return beanMap.put(clz, bean);
    }

    /**
     * 移除一個Bean例項
     */
    public void removeBean(Class<?> clz) {
        beanMap.remove(clz);
    }

    /**
     * Bean例項數量
     */
    public int size() {
        return beanMap.size();
    }

    /**
     * 所有Bean的Class集合
     */
    public Set<Class<?>> getClasses() {
        return beanMap.keySet();
    }

    /**
     * 通過註解獲取Bean的Class集合
     */
    public Set<Class<?>> getClassesByAnnotation(Class<? extends Annotation> annotation) {
        return beanMap.keySet()
                .stream()
                .filter(clz -> clz.isAnnotationPresent(annotation))
                .collect(Collectors.toSet());
    }

    /**
     * 通過實現類或者父類獲取Bean的Class集合
     */
    public Set<Class<?>> getClassesBySuper(Class<?> superClass) {
        return beanMap.keySet()
                .stream()
                .filter(superClass::isAssignableFrom)
                .filter(clz -> !clz.equals(superClass))
                .collect(Collectors.toSet());
    }
}

複製程式碼

我們不需要像spring那樣存放很多的資訊,所以用一個Map來儲存Bean的資訊就好了。Map的Key為Class類,Value為這個Class的例項Object。配合getBean(),addBean()等方法就可以很方便的操作Class和它的例項。

然而現在這個Map裡還沒有存放任何的Bean資料,所以編寫一個loadBeans()方法來初始化載入Bean。

首先在BeanContainer中新增一個變數isLoadBean和一個常量BEAN_ANNOTATION

//BeanContainer
...

/**
* 是否載入Bean
*/
private boolean isLoadBean = false;

/**
* 載入bean的註解列表
*/
private static final List<Class<? extends Annotation>> BEAN_ANNOTATION 
= Arrays.asList(Component.class, Controller.class, Service.class, Repository.class);

...
複製程式碼

然後編寫loadBeans()方法去載入被BEAN_ANNOTATION中的註解類註解的類,以及對應的例項。通過剛才的ClassUtil.getPackageClass(basePackage)獲取我們專案下所有的Class,然後判斷該Class是否被BEAN_ANNOTATION中註解類註解,如果有就說明該Class是一個Bean,對其例項化並且放入Map中。

//BeanContainer
...


/**
* 掃描載入所有Bean
*/
public void loadBeans(String basePackage) {
    if (isLoadBean()) {
        log.warn("bean已經載入");
        return;
    }

    Set<Class<?>> classSet = ClassUtil.getPackageClass(basePackage);
    classSet.stream()
        .filter(clz -> {
            for (Class<? extends Annotation> annotation : BEAN_ANNOTATION) {
                if (clz.isAnnotationPresent(annotation)) {
                    return true;
                }
            }
            return false;
        })
        .forEach(clz -> beanMap.put(clz, ClassUtil.newInstance(clz)));
    isLoadBean = true;
}

/**
* 是否載入Bean
*/
public boolean isLoadBean() {
    return isLoadBean;
}
...
複製程式碼

最後,為了能夠保證整個專案全域性Bean的唯一性,我們要保證這個BeanContainer是唯一的,將該類單例化。

通過lombok的註解@NoArgsConstructor(access = AccessLevel.PRIVATE)生成私有建構函式,再用內部列舉生成唯一的BeanContainer例項。

@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BeanContainer {
	/**
     * 獲取Bean容器例項
     */
    public static BeanContainer getInstance() {
        return ContainerHolder.HOLDER.instance;
    }
    
    ...
    
   private enum ContainerHolder {
        HOLDER;
        private BeanContainer instance;

        ContainerHolder() {
            instance = new BeanContainer();
        }
    }
}
複製程式碼

至此,這個Bean容器就完成了。我們可以通過loadBeans()方法初始化Bean,然後可以通過getBean(),addBean()removeBean()等方法去操作這個Bean,為後面的IOC,AOP等功能打下基礎。


原始碼地址:doodle

原文地址:從零開始實現一個簡易的Java MVC框架--實現Bean容器

相關文章