从零开始实现一个简易的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
包下创建两个工具类:ValidateUtil
和ClassUtil
。
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