一.前言
大家好,最近学校让写一个控制台的饿了么点餐项目,但是要求用mvc模式,没学spring框架之前,我们一般在要注入的类中主动new对象,像这样: UserService userservice = new UserService();
这样其实不好,而且在一些类中每次都新建对象,当该属性需要依赖其他对象时,其他对象还没创建,就会引起失败,于是我模仿spring简易的实现了一个依赖注入的小型框架,我给它起名为:
Winter
二.@ComponentScan,@Component,@Autowired注解定义
我们主要模拟实现该三个注解的基本功能,我们先把这三个注解定义出来:
/**
* @author JJH
*/
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
/**
* @author JJH
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
}
/**
* 组件扫描注解
* @author JJH
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ComponentScan {
String[] packagePath();
}
三.WinterUtils工具类
该工具类主要是用来扫描包下的.class文件,返回,以及获取类名等功能
/**
* winter框架提供的工具类
* @author JJH
*/
public class WinterUtils {
private static final String WINTER_BANNER = "src/main/resources/banner.txt";
/**
* 返回当前时间
* @return
*/
public static String getNowTime(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
/**
* 打印winterLOGO
*/
public static void printWinterLogo() {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(WINTER_BANNER)));
String msg = null;
while ((msg = reader.readLine()) != null) {
System.out.println(msg);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 从文件路径截取出类名
*/
public static String getClassName(String path){
String replace = path.replace("\\", ".");
int index = replace.lastIndexOf("classes");
String substring = replace.substring(index+8);
String classname = substring.substring(0, substring.lastIndexOf("class")-1);
return classname;
}
/**
* 递归遍历所有文件
*/
public static void listFile(File file, List<File> fileList){
if (!file.isDirectory()) {
fileList.add(file);
}else {
File[] files = file.listFiles();
for (File subFile : files) {
listFile(subFile,fileList);
}
}
}
/**
* 根据包名数组获取.class文件
*
* @param packagePaths 包名路径数组
*/
public static List<File> getClassFileByPackage(String[] packagePaths){
List<File> fileList = new ArrayList<>();
ArrayList<File> classFileList = new ArrayList<>();
for (String packge : packagePaths) {
//把对应包名下的文件中含有@Component注解的类解析了,然后创建对象
String replace = packge.replace(".", "/");
URL url = ClassLoader.getSystemResource(replace);
String path = url.getPath();
//找该目录下的所有.class文件
File file = new File(path);
listFile(file,fileList);
for (File fiLe : fileList) {
String name = fiLe.getName();
if(name.endsWith(".class")){
classFileList.add(fiLe);
}
}
}
return classFileList;
}
/**
* 返回类路径下的所有.class文件
*/
public static List<File> getClassFiles(){
URL url = Thread.currentThread().getContextClassLoader().getResource("");
String path = url.getPath();
File file = new File(path);
List<File> fileList = new ArrayList<>();
listFile(file,fileList);
ArrayList<File> classFileList = new ArrayList<>();
for (File fiLe : fileList) {
String name = fiLe.getName();
if(name.endsWith(".class")){
classFileList.add(fiLe);
}
}
return classFileList;
}
/**
* 获取类上有@Component该注解的类文件集合
* @return 类文件集合
*/
public static List<File> getClassFileByAnnatation(Class<Component> componentClass) {
List<File> list = new ArrayList<>();
List<File> classFiles = getClassFiles();
for (File classFile : classFiles) {
String path = classFile.getPath();
String className = getClassName(path);
Class<?> aClass = null;
try {
aClass = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
Annotation annotation = aClass.getAnnotation(componentClass);
if(annotation!=null){
//表示该类上有该注解,可以把该文件装入集合中
list.add(classFile);
}
}
return list;
}
}
三.模仿spring容器ApplicationContext
ApplicationContext中应该存bean的map集合,而且还是单例的,我们可以这样写:
/**
* 模拟spring制作一个winter框架
* spring是春天的意思,
* 那么winter就是冬天的意思
* @author JJH
*
* winterframework
*
*/
public class ApplicationContext {
/**
* 最核心的应该是bean的map集合了
*/
private static Map<String,Object> beanMap = new ConcurrentHashMap<>();
public ApplicationContext(){
}
/**
* 模拟run方法
*/
public static ApplicationContext run(){
WinterUtils.printWinterLogo();
System.out.println("LOG: "+WinterUtils.getNowTime()+": winterframework start success!");
ApplicationContext context = new ApplicationContext();
//先把注解解析类注册为bean
context.registerBean("annotationProcess",new CommonAnnotationProcess());
context.registerBean("autowiredProcess",new AutoWiredProcess());
return context;
}
/**
* 向map中添加bean
* @param beanName bean名称
* @param bean bean对象
* @return 是否添加成功
*/
public boolean registerBean(String beanName,Object bean){
beanMap.put(beanName,bean);
return true;
}
/**
* 根据类型获取bean对象
* @param clazz
* @return
*/
public Object getBean(Class<?> clazz){
String clazzName = clazz.getName();
Object[] objects = new Object[1];
beanMap.forEach((name,bean)->{
if(bean.getClass().getName().equals(clazzName)){
objects[0] = bean;
}
});
return objects[0];
}
/**
* 根据bean的名称获取bean对象
*/
public Object getBean(String name){
Object[] objects = new Object[1];
beanMap.forEach((id,bean)->{
if(id.equals(name)){
objects[0] = bean;
}
});
return objects[0];
}
/**
* 通过静态方法获取bean对象
* @return
*/
public static Map<String,Object> getBeans(){
return beanMap;
}
/**
* 通过this获取
* @return
*/
public Map<String,Object> getAllBean(){
return beanMap;
}
/**
* 静态方法获取bean
* @param clazz
* @return
*/
public static Object GetBean(Class<?> clazz){
Object[] objects = new Object[1];
Map<String, Object> beans = getBeans();
beans.forEach((k,v)->{
if(v.getClass().equals(clazz)){
objects[0] = v;
}
});
return objects[0];
}
}
我们在ApplicationContext中声明了registerBean(),getBean(),getBean(String name)等基本方法
来注册bean,根据类型,名称获取bean
注意:
我们在该容器的构造方法里,已经提前加入了对@ComponentScan,@Component,@Autowired注解解析的处理类CommonAnnotationProcess ,AutoWiredProcess ,下面我们来解析这俩处理类
四.模拟@ComponentScan,@Component实现
该注解就是组件扫描,把@Component注解的类注册为bean,下面我们来模拟实现一下:
我们创建CommonAnnotaionProcess类,用来解析@ComponentScan和@Component注解
原理:
主要就是先获取.class文件,然后遍历每个文件,通过反射获取该类的注解信息,判断是否在类上有@ComponentScan注解,如果有,我们获取@ComponentScan注解中package的路径信息,
然后在去找该路径下的类文件上是否含有@Component注解,如果有,那就创建该类的对象,把它存入容器中的beanMap集合中
/**
* 注解解析器,暂时先让它解析@Component @ComponentScan
* @author JJH
*/
public class CommonAnnotationProcess {
public CommonAnnotationProcess(){
//初始化时,就调用
try {
componetScanAnalysis();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* 找到标注了@ComponentScan注解的类
*/
public void componetScanAnalysis() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
List<File> classFiles = WinterUtils.getClassFiles();
for (File classFile : classFiles) {
String path = classFile.getPath();
String className = WinterUtils.getClassName(path);
//类加载获取类对象
Class<?> aClass = Class.forName(className);
ComponentScan componentScan = aClass.getAnnotation(ComponentScan.class);
if(componentScan!=null){
//该类上面有这个注解,要获取该注解中的包名信息
String[] paths = componentScan.packagePath();
List<File> clsFiles = WinterUtils.getClassFileByPackage(paths);
componentAnalySis(clsFiles);
}
}
}
/**
* 解析带有@Component注解的类
* @param classFiles
*/
public void componentAnalySis(List<File> classFiles) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
for (File file : classFiles) {
String className = WinterUtils.getClassName(file.getPath());
Class<?> aClass = Class.forName(className);
Component component = aClass.getAnnotation(Component.class);
if(component!=null){
//可以给该类创建对象拉!哈哈哈
Object obj = aClass.newInstance();
//默认根据类名首字母小写作为id
String name = aClass.getName();
String id = name.substring(0, 1).toLowerCase() + name.substring(1);
//添加到beanmap中去啦!
ApplicationContext.getBeans().put(id,obj);
}
}
}
}
五.模拟@Autowired注解实现
该注解在spring中主要是用来做依赖注入的,可以加在属性上,set方法,构造方法上,我们这里为了简单,我们只做加在属性上的实现:
原理:
先找到被@Component注解的类,然后通过反射获取该类的属性集合,然后遍历属性集合,依次判断每个属性上是否有@Autowired注解,如果有,那么我们根据该属性的类型,去beanMap中找是否有该类型的bean,如果有,就通过getBean()拿到该bean,将他赋值给该属性
**
* Autowired 注解解析器
* 目的是为了自动注入
* 原理:
* 先拿到注解了@Componet的类文件,
* 然后判断该类中的属性上是否被@Autowired注解标记,
* 如果标记了,那么就根据这个属性的类型从beanMap中
* 查找,找到了该类型的bean对象,就把该对象赋值给该
* 属性
* @author JJH
*/
public class AutoWiredProcess {
public AutoWiredProcess(){
autowiredProcess();
}
/**
* 解析属性上的autowired注解,依赖注入
*
*/
public void autowiredProcess() {
//先把@componet注解的类找到再说
List<File> comFiles = WinterUtils.getClassFileByAnnatation(Component.class);
Map<String, Object> beans = ApplicationContext.getBeans();
for (File comFile : comFiles) {
//获取属性
String className = WinterUtils.getClassName(comFile.getPath());
Class<?> aClass = null;
try {
aClass = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {
if(field.getAnnotation(Autowired.class)!=null){
//属性上有@autowired注解,
//我们从map集合中查找它所需要的bean
field.setAccessible(true);
beans.forEach((id,bean)->{
if(className.endsWith(bean.getClass().getName())){
//类名对住的时候,就该来给他依赖注入了
Field[] beanfields = bean.getClass().getDeclaredFields();
for (Field beanfield : beanfields) {
Class<?> type = beanfield.getType();
beans.forEach((k,v)->{
if(type.equals(v.getClass())){
try {
beanfield.setAccessible(true);
beanfield.set(bean,v);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
});
}
}
});
}
}
}
}
}
六.为框架添加Logo和简易日志
为了使框架美观,在启动时显示Logo,我们创建banner.txt文件,在该文件中写入我们想要的文字图
文字图我们可以在该网站:Text to ASCII Art Generator (TAAG) 生成我们想要的图片,我这里选取的图片为:
$$\ $$\ $$\ $$$$$$\
\__| $$ | $$$$ | $$$ __$$\
$$\ $$\ $$\ $$\ $$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\ \_$$ | $$$$\ $$ |
$$ | $$ | $$ |$$ |$$ __$$\\_$$ _| $$ __$$\ $$ __$$\ $$ | $$\$$\$$ |
$$ | $$ | $$ |$$ |$$ | $$ | $$ | $$$$$$$$ |$$ | \__| $$ | $$ \$$$$ |
$$ | $$ | $$ |$$ |$$ | $$ | $$ |$$\ $$ ____|$$ | $$ | $$ |\$$$ |
\$$$$$\$$$$ |$$ |$$ | $$ | \$$$$ |\$$$$$$$\ $$ | $$$$$$\ $$\ \$$$$$$ /
\_____\____/ \__|\__| \__| \____/ \_______|\__| \______|\__| \______/
我们想让框架启动时打印一下日志,我们可以在ApplicationContext的run()方法中写:
System.out.println("LOG: "+WinterUtils.getNowTime()+": winterframework start success!");
工具类中写:
/**
* 返回当前时间
* @return
*/
public static String getNowTime(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
}
七.测试框架启动
我们可以看到在ApplicationContext中有一个静态run()方法,我们先写一些pojo类,来测试我们的依赖注入:
我们创建两个类BookService,UserService,
@Component
public class BookService {
public void getBook(){
System.out.println("获取图书信息...");
}
}
@Component
public class UserService {
@Autowired
private BookService bookService;
public void userinfo(){
System.out.println("获取用户信息...");
bookService.getBook();
}
}
可以看到,在UserService中注入了BookService,调用了bookservice的getBook()方法,两个类都加了@Component注解,下面我们创建启动类:
@ComponentScan(packagePath = {"com.jjh"})
public class TestWinter {
public static void main(String[] args) {
ApplicationContext context = ApplicationContext.run();
Map<String, Object> beans = context.getAllBean();
UserService userservice = (UserService) context.getBean(UserService.class);
userservice.userinfo();
}
}
我们运行一下:
$$\ $$\ $$\ $$$$$$\
\__| $$ | $$$$ | $$$ __$$\
$$\ $$\ $$\ $$\ $$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\ \_$$ | $$$$\ $$ |
$$ | $$ | $$ |$$ |$$ __$$\\_$$ _| $$ __$$\ $$ __$$\ $$ | $$\$$\$$ |
$$ | $$ | $$ |$$ |$$ | $$ | $$ | $$$$$$$$ |$$ | \__| $$ | $$ \$$$$ |
$$ | $$ | $$ |$$ |$$ | $$ | $$ |$$\ $$ ____|$$ | $$ | $$ |\$$$ |
\$$$$$\$$$$ |$$ |$$ | $$ | \$$$$ |\$$$$$$$\ $$ | $$$$$$\ $$\ \$$$$$$ /
\_____\____/ \__|\__| \__| \____/ \_______|\__| \______|\__| \______/
LOG: 2023-10-07 19:38:48: winterframework start success!
获取用户信息...
获取图书信息...
可以看到,成功的对@Component注解解析,并对@Autowird实现了依赖注入功能!
下面我们获取容器中的所有bean:
beans.forEach((name,bean)->{
System.out.println(name+"="+bean);
});
com.jjh.winterframework.test.UserService=com.jjh.winterframework.test.UserService@5702b3b1
com.jjh.winterframework.test.BookService=com.jjh.winterframework.test.BookService@69ea3742
autowiredProcess=com.jjh.winterframework.core.AutoWiredProcess@4b952a2d
annotationProcess=com.jjh.winterframework.core.CommonAnnotationProcess@3159c4b8
可以看到,我们的注解处理类,和我们通过@Component注解指定的两个类都成功被注册到了beanMap集合中,次上,我们的winter框架简易的实现成功了!
八.总结
我们在学习时,要善于反思,并能举一反三,善于模仿。
对于其他注解作者以后有时间会逐步实现,我们下期再见!