This website requires JavaScript.

从零开始实现一个简易的 Java MVC 框架(二)--实现 Bean 容器

项目准备

首先确保你拥有以下环境或者工具

  • 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 两个依赖就可以完成前面几个功能的实现,其他需要的依赖等到后面需要的时候再加。

接着把项目一些基本的包结构创建一下,如下图

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 容器

0条评论
avatar