1、前言
因为这是我设想要写的一系列文章的第一篇。所以我先说明一下我为什么要重复造轮子。
在这里造轮子的目的不是为了造出比前人更出色的轮子来,而是通过造轮子,学习轮子内部的结构及相关原理。甚至去模仿前人轮子上的优点,吸收这些优点。
这一系列文章初步估计应该包括:IoC和依赖注入、AOP、ORM、Servlet容器(tomcat)等。
2、IoC和依赖注入的概念
Inverse of Control,控制反转。
IoC主要功能是依赖关系的转移。应用的本身不负责依赖对象的创建和维护,而是由第三方容器负责。控制权就由应用转移到了外部容器。
IoC的主要功能由控制反转来解释并不是很好理解。所以提出了新的概念Dependency Injection.
DI依赖注入,调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以转移对某一接口实现类的依赖。也就是在运行期,由外部容器动态地将所依赖的对象注入到组件中去。
最常用的IoC和DI框架没有之一:Spring。
Spring IoC的介绍和使用: http://blog.csdn.net/jrainbow/article/details/9091577
3、源码结构
本文中使用的是通过注解(annotation)的方式来对需要进行IoC和DI的类进行管理。
我要看大图:http://img.blog.csdn.net/20160218111901359
4、实现过程
4.1 工具类
首先我们需要两个工具类:类加载器ClassLoader 和反射工厂类:
- 类加载器
此类加载器通过getClassSet(String packageName)方法获取到packageName路径下及Jar包中的类的集合
package xyz.letus.framework.ioc;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 类加载器
* @ClassName: ClassLoader
* @Description: TODO
* @author 潘广伟(笨笨)
* @date 2015年9月14日
*
*/
public class ClassLoader {
public static final Logger LOGGER = LoggerFactory.getLogger(ClassLoader.class);
/**
* 获取线程上的类加载器
* @Title: getClassLoader
* @Description: TODO
* @param @return
* @return java.lang.ClassLoader
* @throws
*/
public static java.lang.ClassLoader getClassLoader(){
return Thread.currentThread().getContextClassLoader();
}
/**
* 加载类
* @Title: loadClass
* @Description: TODO
* @param @param className
* @param @param isInitialized
* @param @return
* @return Class<?>
* @throws
*/
public static Class<?> loadClass(String className,boolean isInitialized){
Class<?> clazz = null;
try {
clazz = Class.forName(className, isInitialized, getClassLoader());
} catch (ClassNotFoundException e) {
LOGGER.error("load class failure", e);
throw new RuntimeException(e);
}
return clazz;
}
/**
* 获取包下所有的类
* @Title: getClassSet
* @Description: TODO
* @param @param packageName
* @param @return
* @return Set<Class<?>>
* @throws
*/
public static Set<Class<?>> getClassSet(String packageName){
Set<Class<?>> classes = new HashSet<Class<?>>();
try {
Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".", "/"));
while(urls.hasMoreElements()){
URL url = urls.nextElement();
if(url != null){
String protocol = url.getProtocol();
if(protocol.equals("file")){
String packagePath = url.getPath().replace("%20", " ");
addCommonClass(classes, packagePath, packageName);
}else if(protocol.equals("jar")){
addJarClasses(classes, url);
}
}
}
} catch (IOException e) {
LOGGER.error("get class set failure", e);
e.printStackTrace();
throw new RuntimeException(e);
}
return classes;
}
/**
* 把jar包中所有的类加入集合
* @Title: addJarClasses
* @Description: TODO
* @param @param classes
* @param @param url
* @param @throws IOException
* @return void
* @throws
*/
private static void addJarClasses(Set<Class<?>> classes, URL url)
throws IOException {
JarURLConnection connetion = (JarURLConnection) url.openConnection();
if(connetion != null){
JarFile jar = connetion.getJarFile();
if(jar != null){
Enumeration<JarEntry> enties = jar.entries();
while(enties.hasMoreElements()){
JarEntry entry = enties.nextElement();
String entryName = entry.getName();
if(entryName.endsWith(".class")){
String className = entryName.substring(0, entryName.lastIndexOf('.')).replaceAll("/", ".");
doAddClass(classes, className);
}
}
}
}
}
/**
* 把文件夹中的所有类加入集合
* @Title: addCommonClass
* @Description: TODO
* @param @param classes
* @param @param packagePath
* @param @param packageName
* @return void
* @throws
*/
private static void addCommonClass(Set<Class<?>> classes,String packagePath,String packageName){
File[] files = new File(packagePath).listFiles(new FileFilter() {
public boolean accept(File file) {
// TODO Auto-generated method stub
return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
}
});
for(File file : files){
String fileName = file.getName();
if(file.isFile()){
String className = packageName + '.' + fileName.substring(0, fileName.lastIndexOf('.'));
doAddClass(classes, className);
}else{
String subPackagetPath = packagePath + "/" + fileName;
String subPackagetName = packageName + "/" + fileName;
addCommonClass(classes, subPackagetPath, subPackagetName);
}
}
}
/**
* 加入类到集合
* @Title: doAddClass
* @Description: TODO
* @param @param classes
* @param @param className
* @return void
* @throws
*/
private static void doAddClass(Set<Class<?>> classes,String className){
classes.add(loadClass(className, false));
}
}
反射工厂
此工厂的功能是:
1、在运行期创建类对象
2、运行期为对象的属性赋值
3、运行期调用对象的方法
package xyz.letus.framework.ioc;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 反射工厂
* @ClassName: ReflectionFactory
* @Description: TODO
* @author 潘广伟(笨笨)
* @date 2015年9月19日
*
*/
public class ReflectionFactory {
public static final Logger LOGGER = LoggerFactory.getLogger(ReflectionFactory.class);
/**
* 创建实例
* @Title: newInstance
* @Description: TODO
* @param @param clazz
* @param @return
* @return Object
* @throws
*/
public static Object newInstance(Class<?> clazz){
Object instance = null;
try {
instance = clazz.newInstance();
} catch (Exception e) {
LOGGER.error("new instatnce failure", e);
throw new RuntimeException(e);
}
return instance;
}
/**
* 调用方法
* @Title: invokeMethod
* @Description: TODO
* @param @param obj
* @param @param method
* @param @param args
* @param @return
* @return Object
* @throws
*/
public static Object invokeMethod(Object obj,Method method,Object...args){
Object result = null;
try {
method.setAccessible(true);
result = method.invoke(obj, args);
} catch (Exception e) {
LOGGER.error("invoke method failure", e);
throw new RuntimeException(e);
}
return result;
}
/**
* 设置成员变量值
* @Title: setField
* @Description: TODO
* @param @param obj
* @param @param field
* @param @param value
* @return void
* @throws
*/
public static void setField(Object obj,Field field,Object value){
try {
field.setAccessible(true);
field.set(obj, value);
} catch (Exception e) {
LOGGER.error("set field failure", e);
throw new RuntimeException(e);
}
}
}
4.2 注解类
声明两个注解类:Component和Inject
- Component
这是一个类注解,加上这样注解的类会在运行期交由IoC容器扫描并自动创建实例。
package xyz.letus.framework.ioc.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 组件描述注解
* @ClassName: Component
* @Description: TODO
* @author 潘广伟(笨笨)
* @date 2015年9月19日
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}
- Inject
这是一个属性注解,加上此注解的属性,会在运行期由IoC容器进行DI操作,为属性赋值。
package xyz.letus.framework.ioc.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 依赖注入注解
* @ClassName: Controller
* @Description: TODO
* @author 潘广伟(笨笨)
* @date 2015年9月19日
*
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
String value() default "";
}
4.3 扫描托管的类
ClassFactory类查找所有basePackage包下的类,并判断这些类是否有标注@Component注解。带此注解的类是需要我们的IoC容器管理的类。
这里我们获取到的类集合是个一Map。如果用户在使用@Component注解的时候,有指定value值,我们后面在DI的时候会根据此值去查询并赋值。如果没指定value,则会使用缺省的类名。
package xyz.letus.framework.ioc;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import xyz.letus.framework.ioc.annotation.Component;
/**
* 类操作助手
* @ClassName: ClassHelper
* @Description: TODO
* @author 潘广伟(笨笨)
* @date 2015年9月19日
*
*/
public class ClassFactory {
/**
* 获取交由容器管理的类
* @Title: getBeanClasses
* @Description: TODO
* @param @return
* @return Map<String, Class<?>>
* @throws
*/
public static Map<String, Class<?>> getBeanClasses(String basePackage){
Map<String, Class<?>> annotationClasses = new HashMap<String, Class<?>>();
Set<Class<?>> classes = ClassLoader.getClassSet(basePackage);
for(Class<?> clazz : classes){
if(clazz.isAnnotationPresent(Component.class)){
Component component = clazz.getAnnotation(Component.class);
String name = clazz.getSimpleName();
String value = component.value();
if(value.length() > 0){
name = value;
}
classes.add(clazz);
annotationClasses.put(name, clazz);
}
}
return annotationClasses;
}
}
4.4 创建实例及属性赋值
我们通过ClassFactory获取到所有托管的类后,我们可以ReflectionFactory来创建所有类的实例。
Object obj = ReflectionFactory.newInstance(entry.getValue());
package xyz.letus.framework.ioc;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* Bean助手类
* @ClassName: BeanHelper
* @Description: TODO
* @author 潘广伟(笨笨)
* @date 2015年9月19日
*
*/
public class BeanFactory {
private static final Map<String, Object> BEAN_MAP = new HashMap<String, Object>();
/**
* 创建所有托管的实例
* @Title: createInstance
* @Description: TODO
* @param @param packages
* @return void
* @throws
*/
public static void createInstance(List<String> packages){
for (String packagePath : packages) {
Map<String, Class<?>> beanClasses = ClassFactory
.getBeanClasses(packagePath);
for (Entry<String, Class<?>> entry : beanClasses.entrySet()) {
Object obj = ReflectionFactory.newInstance(entry.getValue());
BEAN_MAP.put(entry.getKey(), obj);
}
}
IocHelper.inject(BEAN_MAP);
}
/**
* 获取Bean实例
* @Title: getBean
* @Description: TODO
* @param @param name
* @param @return
* @return T
* @throws
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name){
if(!BEAN_MAP.containsKey(name)){
throw new RuntimeException("can not get bean by className:"+name);
}
return (T) BEAN_MAP.get(name);
}
}
实例化所有类后,由IocHelper类来判断这些对象中是否有带@Inject注解的属性,然后同样通过ReflectionFactory来为这些属性进行依赖注入(DI赋值)。
package xyz.letus.framework.ioc;
import java.lang.reflect.Field;
import java.util.Map;
import xyz.letus.framework.ioc.annotation.Inject;
/**
* 依赖注入助手类
* @ClassName: IocHelper
* @Description: TODO
* @author 潘广伟(笨笨)
* @date 2015年9月19日
*
*/
public class IocHelper {
/**
* 注入属性
* @Title: inject
* @Description: TODO
* @param
* @return void
* @throws
*/
public static void inject(Map<String, Object> beanMap){
/**
* 为类中的属性注入值
*/
for(Map.Entry<String, Object> entry : beanMap.entrySet()){
String name = entry.getKey();
Object beanInstance = entry.getValue();
Field[] beanFields = beanInstance.getClass().getDeclaredFields();
for(Field field : beanFields){
if(field.isAnnotationPresent(Inject.class)){
Inject inject = field.getAnnotation(Inject.class);
if(inject.value().length() > 0){
name = inject.value();
}
Class<?> fieldClazz = field.getType();
Object fieldInstance = beanMap.get(name);
if(fieldInstance != null){
ReflectionFactory.setField(beanInstance, field, fieldInstance);
}
}
}
}
}
}
4.4 资源管理器
虽然我们使用的是annotation的方式来进行管理配置信息,但像Spring一样,简单的资源文件可以使用框架更便捷与快速地工作。
我们这里使用的是Java资源文件来作为配置文件,当然如果我们有层次比较分明的配置信息时,我们也可以像Spring框架那样使用XML文件。
ResourceFactory对资源文件解析比较简单,我们现在仅仅解析scanPackage资源。这个资源告诉我们的框架需要托管的类放在哪个包路径下。当然,如果没有这个说明,我们也可以扫描整个项目下的类文件,这样效率明显比较低。
package xyz.letus.framework.context;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
/**
* 资源管理器
* @ClassName: ResourceFactory
* @Description: TODO
* @author 潘广伟(笨笨)
* @date 2016年2月17日
*
*/
public class ResourceFactory {
private static final List<String> SCAN_PACKAGES = new ArrayList<String>();
/**
* 解析资源文件(配置文件)
* @Title: parse
* @Description: TODO
* @param @param fileName
* @param @throws FileNotFoundException
* @param @throws IOException
* @return void
* @throws
*/
public static void parse(String fileName) throws FileNotFoundException, IOException {
InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName);
Properties properties = new Properties();
properties.load(stream);
Enumeration<?> e = properties.propertyNames();// 得到配置文件的名字
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
String value = properties.getProperty(key);
if("scanPackage".equals(key)){
SCAN_PACKAGES.add(value);
}
}
}
/**
* 获取自动描述的包路径
* @Title: getPackages
* @Description: TODO
* @param @return
* @return List<String>
* @throws
*/
public static List<String> getPackages(){
return SCAN_PACKAGES;
}
}
4.5 应用容器
ApplicationContext 通过资源管理器ResourceFactory 获取到所以需要托管类的包路径。然后交由BeanFactory创建所有的类实例,并为其属性赋值。
ApplicationContext 提供了getBean(String name),此方法用户可以通过之前定义的名称为默认的名称来获取类实例。
package xyz.letus.framework.context;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import xyz.letus.framework.ioc.BeanFactory;
/**
* 应用容器
* @ClassName: IocContext
* @Description: TODO
* @author 潘广伟(笨笨)
* @date 2016年2月17日
*
*/
public class ApplicationContext {
private String path;
private ApplicationContext(String path){
this.path = path;
init();
}
public static ApplicationContext getContext(String path){
return new ApplicationContext(path);
}
/**
* 初始化
* @Title: init
* @Description: TODO
* @param
* @return void
* @throws
*/
public void init(){
try {
ResourceFactory.parse(path);
List<String> packages = ResourceFactory.getPackages();
BeanFactory.createInstance(packages);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 根据别名获取一个对象
* @Title: getBean
* @Description: TODO
* @param @param name
* @param @return
* @return T
* @throws
*/
public <T> T getBean(String name) {
return BeanFactory.getBean(name);
}
}
5、框架使用
现在我们使用本文编写的IoC框架进行编程。
- 首先我们需要一个Dao类及一个Service类。
Dao类比较简单,一个say()方法,并加上@Component将由IoC容器管理。
Service和Dao不同的是,它有一个未初始化的dao属性,并由@Inject告诉IoC容器,需要在运行时,为此属性初始化。
package xyz.letus.demo;
import xyz.letus.framework.ioc.annotation.Component;
@Component
public class Dao {
public void say(){
System.out.println("Dao say something.");
}
}
package xyz.letus.demo;
import xyz.letus.framework.ioc.annotation.Component;
import xyz.letus.framework.ioc.annotation.Inject;
@Component
public class Service {
@Inject
private Dao dao;
public void say(){
dao.say();
System.out.println("Service say something.");
}
}
- 资源文件context.properties
scanPackage=xyz.letus.demo
- 启动IoC容器,并获取service实例
ApplicationContext context = ApplicationContext.getContext("context.properties");
Service service = context.getBean("Service");
service.say();
- 输出结果
Dao say something.
Service say something.
当然我们上面使用的注解都是缺省模式的。我们也可以为托管的对象加上别名:
@Component("a")
public class Dao {
@Inject("a")
private Dao dao;
@Component("b")
public class Service {
Service service = context.getBean("b");
得到的结果是一样的。
6、我们还要继续
就这样,我们完成了最简单的IoC及DI框架。当然说是简单,因为它离Spring等框架的IoC功能还很远很远,包括为接口注入实现的实例、单例模式及多例模式的实现等等。
我们还要继续造轮子。
7、源码下载
https://github.com/benben-ren/wheel/tree/d389e62b1f1380b45bb38b098c5ed0ce8123c9dd
注:源码中只有ioc构架源码,不包含使用源码。