学习spring时,曾经试过因为没有在xml中配<context:component-scan base-package="xxx" />,导致标注了@Component、Controller、Service也找不到bean,于是对这个功能起了兴趣,试着自己尝试在xml中配置标签后通过一个注解去扫包,扫完后通过反射创建对象放到自己写的ioc容器中,也算是做了一个简易版ioc容器。(底层只是一个map)
先写一个朴实无华的xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<componentScan package="com.joker"/>
</beans>
写一个类解析此xml:
public class MyXmlParser {
public File parseConfig() throws ParserConfigurationException, IOException, SAXException {
return parseConfig("applicationContext.xml");
}
public File parseConfig(String resourcePath) throws ParserConfigurationException, IOException, SAXException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
String scanPackage = null;
//创建DocumentBuilder对象
DocumentBuilder db = dbf.newDocumentBuilder();
//通过DocumentBuilder对象的parser方法解析applicationContext.xml文件
InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath);
Document document = db.parse(is);
//获得<componentScan></componentScan>标签内容
NodeList node = document.getElementsByTagName("componentScan");
Node item = node.item(0);
if (item.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) item;
//将<componentScan>标签中的package=xxx拿到
scanPackage = element.getAttribute("package");
}
List<String> fileNames = new ArrayList<>();
//com.joker ===> com/joker,变成文件路径形式
String packagePath = scanPackage == null ? "" : scanPackage.replace(".", "/");
Enumeration<URL> urls = getClass().getClassLoader().getResources(packagePath);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
String path = url.toString();
if (url == null)
continue;
//String type = url.getProtocol();
String realPath = path.substring(6);
return new File(realPath);
}
return null;
}
}
debug时获取到的信息如下,至此,MyXmlParser中的方法已经可以返回一个路径为xml中写的包路径的File对象了,至于为什么要返回File对象,因为后面要用到
现在知道了包的所在路径,但如何找到它下面的所有类名呢,所以要再写一个类,写一个核心方法,也是一个递归方法,找到他的子子孙孙包下的所有.class文件:
public class MyComponentScan {
private List<String> classNames = new ArrayList<>();
public List<String> getClassNames() {
return classNames;
}
public MyComponentScan(){
}
public MyComponentScan(File file){
getClassesRecursion(file);
}
public void getClassesRecursion(File file){
File[] files = file.listFiles();
for (File childFile : files) {
if (childFile.isDirectory()) {
//如果该包下还有包,递归调用自己
getClassesRecursion(childFile);
} else {
String childFilePath = childFile.getPath();
//C:\Users\joker\Desktop\course\spring\Aop\target\classes\com\joker\myspring\MyComponentScan.class
if (childFilePath.endsWith(".class")) {
//windows系统路径为反斜杠,类Unix系统要写"/"
childFilePath = childFilePath.substring(childFilePath.lastIndexOf("classes")+8, childFilePath.lastIndexOf("."));//com\joker\myspring\MyComponentScan.class
childFilePath = childFilePath.replace("\\",".");
classNames.add(childFilePath);
}
}
}
}
}
我的包结构如下:
写个测试类看看拿到了没:
拿到此包下所有类的名字后,就剩最后一步了:模仿spring的ioc容器,自己写一个简易版ioc容器,首先是四个注解,为了区分加上了My前缀,这里只列出一个,毕竟四个的功能是一样的,剩下的MyController、Myrepository、MyService只是名字不同,内容都一样(这里被Retention这个元注解坑过,不写runtime无法通过反射获取此注解):
package com.joker.stereotype;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyComponent {
String[] beanId() default "";
}
毕竟是自己写的简易版,我这个注解必须写bean的id,否则默认没有,后面取放bean时就很麻烦。
然后是简易版IOC容器,底层是一个map,通过反射获取类的注解后new对象放入map中,id作为键,对象作为值,这里跟spring同理,默认调用类的空参构造器,没有的话会报错:
public class MyIocContainer {
Map<String,Object> bean = new HashMap();
public MyIocContainer(List<String> classNames) throws Exception {
for (String className : classNames) {
Class<?> clazz = Class.forName(className);
Annotation[] declaredAnnotations = clazz.getDeclaredAnnotations();
for (Annotation anno : declaredAnnotations) {
Class<? extends Annotation> annotation = anno.annotationType();
// MyComponent myComponent = UserDaoImpl.class.getAnnotation(MyComponent.class);
if( annotation.getName().equals("com.joker.stereotype.MyComponent") ||
annotation.getName().equals("com.joker.stereotype.MyRepository") ||
annotation.getName().equals("com.joker.stereotype.MyService") ||
annotation.getName().equals("com.joker.stereotype.MyController")
){
//确认到该类注解为@MyComponent之后,将其强转,为的是能调用其方法,获取其成员变量的值(调用beanId()方法)
MyComponent myComponent = (MyComponent)anno;
String[] beanIds = myComponent.beanId();
for (int i = 0; i < beanIds.length; i++) {
if (i != 0){
//当有多个id时,防止反复创建对象,取出ioc容器中已有的对象即可
Object bean = getBean(beanIds[i - 1]);
setBean(beanIds[i],bean);
continue;
}
//将beanId做为键,new出来的对象作为值,放到我自己的"ioc容器"中
setBean(beanIds[i],clazz.getDeclaredConstructor().newInstance());
}
}
}
}
}
public Object getBean(String beanId){
return bean.get(beanId);
}
public void setBean(String beanId,Object bean){
this.bean.put(beanId,bean);
}
}
最后在最开始写的那个xml中的包(com.joker)中的随意一个类上标上注解,试试看。没加注解或者不在该包下的类自然拿不到:
乞丐版springIOC容器和扫包功能完成