造一个方形的轮子3--控制反转

造一个方形轮子文章目录:造一个方形的轮子

01、先把车正过来

在上一篇《造一个方形的轮子2--添加配置》的最后又翻车了,现在先把发现的这个问题解决,将square项目的包引用其它项目无法启动的主要原因是没有正确的指定tomcat运行的关键目录,以及读取资源的目录。

也就是说把设置的目录结构调整一下就可以了。

添加一个配置目录的util类,ClassesPathUtil.java:

/**
 * 处理项目路径问题
 * @author ixx
 * @date 2019-06-15
 */
public class ClassesPathUtil {
    private static final Logger log = LoggerFactory.getLogger(ClassesPathUtil.class);
    /**
     * 项目目录(.../classes)
     */
    private String projectPath;
    /**
     * 静态资源目录(.../classes/public)
     */
    private String publicPath;

    public ClassesPathUtil(Class clzz){
        String basePath = clzz.getResource("").getPath();
        //  ..../classes
        projectPath = basePath.substring(0, basePath.indexOf("classes")+7);
        publicPath = setPublic(projectPath, "/public");
    }

    private String setPublic(String basePath, String path){
        File publicFile = new File(basePath+path);
        if(!publicFile.exists()){
            publicFile.mkdirs();
        }
        return basePath+path;
    }

    public String getProjectPath() {
        return projectPath;
    }

    public String getPublicPath() {
        return publicPath;
    }

}

如果classes目录下没有public 目录的话就创建一个,设置tomcat.addWebapp(context_path, publicPath)时会用到

修改启动类(这块改动有点大,放一个全量代码吧),SquareApplication.java:

/**
 * 项目启动类
 * @author ixx
 * @date 2019-6-13
 */
public class SquareApplication {
    private static final Logger log = LoggerFactory.getLogger(SquareApplication.class);
    private static Map<String, Object> CONF_MAP = new HashMap<>();
    private static Tomcat tomcat = null;
    private static String CONTEXT_PATH = "/";
    private static String ENCODING = "UTF-8";
    private static int TOMCAT_PORT = 8080;
    private static ClassesPathUtil classesPathUtil;

    public static void run(Class clzz, String[] args) {
        try {
            long startTime = System.currentTimeMillis();
            classesPathUtil = new ClassesPathUtil(clzz);
            // 加载配置
            loadYaml(classesPathUtil.getProjectPath());
            // 初始化参数
            setArgs(args);
            // 输出banner
            printBanner(classesPathUtil.getProjectPath());
            tomcat = new Tomcat();
            // 设置Tomcat工作目录
            tomcat.setBaseDir(classesPathUtil.getProjectPath() + "/Tomcat");
            tomcat.setPort(TOMCAT_PORT);
            tomcat.addWebapp(CONTEXT_PATH, classesPathUtil.getPublicPath());
            // 执行这句才能支持JDNI查找
            tomcat.enableNaming();
            tomcat.getConnector().setURIEncoding(ENCODING);
            tomcat.start();
            log.info("Tomcat started on port(s): {} with context path '{}'", TOMCAT_PORT, CONTEXT_PATH);
            log.info("Started Application in {} ms." , (System.currentTimeMillis() - startTime));
            // 保持服务器进程
            tomcat.getServer().await();
        } catch (Exception e) {
            log.error("Application startup failed...", e);
        }
    }

    /**
     * 初始化参数
     * @param args
     */
    private static void setArgs(String[] args){
        Map<String, String> map = ArgsToKVUtil.convert(args);
        if(map.get("--server.port") != null){
            TOMCAT_PORT = Integer.parseInt(map.get("--server.port"));
        }
    }

    /**
     * 加载配置文件
     * @param projectPath
     */
    private static void loadYaml(String projectPath){
        CONF_MAP =  LoadApplicationYmlUtil.load(projectPath);
        if(CONF_MAP.get("server.port") != null){
            TOMCAT_PORT = (Integer)CONF_MAP.get("server.port");
        }
        if(CONF_MAP.get("server.servlet.context-path") != null){
            CONTEXT_PATH = (String)CONF_MAP.get("server.servlet.context-path");
        }
    }

    /**
     * 输出Banner图
     * @param projectPath
     */
    private static void printBanner(String projectPath){
        BufferedReader br = null;
        try{
            File f = new File(projectPath+"/default-banner.txt");
            if(f.exists()){
                br = new BufferedReader(new FileReader(f));
            } else {
                InputStream is = SquareApplication.class.getClassLoader().getResourceAsStream("default-banner.txt");
                br = new BufferedReader(new InputStreamReader(is));
            }
            StringBuilder stringBuilder = new StringBuilder("\n");
            String line;
            while ((line = br.readLine()) != null){
                stringBuilder.append(line).append("\n");
            }
            log.info(stringBuilder.toString());
        } catch (Exception e){
            log.info("load banner file error!!", e);
            if(br != null){
                try {
                    br.close();
                } catch (IOException e1) {
                }
            }
        }
    }

}

重新在square项目根目录下执行mvn clean install 然后到car项目里去启动项目就可以了。

02、添加注解

实现控制反转的思路,

1、要有自己定义的注解,标识类需要初始化到容器中

2、程序启动时调用初始化方法(默认从启动类目录向下扫描所有包里的类文件)

3、根据接口名称、类名称及添加注解时定义的bean名称初始化到容器中

4、处理Bean互相之间的依赖关系(属于依赖注入)

这里我先只实现一个@Service@Component 两个注解,对外提供服务的@Controller 后边实现对外Rest接口的时候再实现。

Service.java

package com.jisuye.annotations;
// import ...;
/**
 * Service注解
 * @author ixx
 * @date 2019-06-20
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
     String value() default "";
}

Component.java

package com.jisuye.annotations;
// import ...;
/**
 * Component注解
 * @author ixx
 * @date 2019-06-20
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
     String value() default "";
}

这两个注解都只有一个value的参数可配置,接收设置的Bean名称。

03、定义BeanObject对象

定义BeanObject对象作为Bean反射初始化的封装,除了反射生成的bean对象外还保存一些类的其它信息,方便后边做依赖的时候使用,BeanObject.java:

/**
 * 封装Bean对象
 * @author ixx
 * @date 2019-06-20
 */
public class BeanObject {
    /**
     * 类全名(带包路径)
     */
    private String className;
    /**
     * 类名
     */
    private String simpleName;
    /**
     * 实际对象
     */
    private Object object;
    /**
     * 包路径(com.jisuye)
     */
    private String packages;
    /**
     * 注解类型集合
     */
    private Annotation[] annotaions;

    /**
     * 接口名
     */
    private Class[] interfacs;
    // ... getter and setter
}

04、Bean初始化工具类

初始化的Bean对象会放在一个HashMap的容器里,方便其它地方使用。

程序启动时初始化Bean的工具类 BeansInitUtil.java(返回这个容器Map):

/**
 * Bean初始化类
 * @author ixx
 * @date 2019-06-20
 */
public class BeansInitUtil {
    private static final Logger log = LoggerFactory.getLogger(BeansInitUtil.class);

    public static Map<String, BeanObject> init(Class clazz){
        Map<String, BeanObject> beansMap = new HashMap<>();
        String path = clazz.getResource("").getPath();
        log.info("===bean init path:{}", path);
        File root = new File(path);
        initFile(root, beansMap);
        return beansMap;
    }

    private static void initFile(File file, Map<String, BeanObject> map){
        File[] fs = file.listFiles();
        for (File f : fs) {
            if(f.isDirectory()){
                // 递归目录
                initFile(f, map);
            } else {
                // 处理class
                loadClass(f, map);
            }
        }
    }
    private static void loadClass(File file, Map<String, BeanObject> map){
        if(file == null){
            return;
        }
        try {
            BeanObject beanObject = new BeanObject();
            log.info("load bean path:{}", file.getPath());
            String path = file.getPath();
            path = path.substring(path.indexOf("classes")+8).replace(".class", "");
            path = path.replace("\\", ".");
            Class clzz = Class.forName(path);
            Annotation[] annotations = clzz.getAnnotations();
            if(annotations.length >0 && filterAnnotation(annotations)){
                beanObject.setAnnotaions(annotations);
                beanObject.setSimpleName(clzz.getSimpleName());
                beanObject.setClassName(clzz.getName());
                beanObject.setInterfacs(clzz.getInterfaces());
                beanObject.setPackages(clzz.getPackage().toString());
                Object obj = clzz.newInstance();
                beanObject.setObject(obj);
                // 按接口设置bean
                for (Class aClass : beanObject.getInterfacs()) {
                    map.put(aClass.getName(), beanObject);
                }
                // 按类设置bean
                map.put(beanObject.getClassName(), beanObject);
                // 按注解输入value设置bean
                for (Annotation annotation : annotations) {
                    String tmp_name = "";
                    if(annotation instanceof Service){
                        ((Service)annotation).value();
                    } else if(annotation instanceof Component) {
                        ((Component) annotation).value();
                    }
                    if(tmp_name != null && !tmp_name.equals("")) {
                        map.put(tmp_name, beanObject);
                    }
                }
            }
        } catch (Exception e) {
            log.error("init bean error:{}", file.getPath(), e);
        }
    }
    private static boolean filterAnnotation(Annotation[] annotations){
        boolean b = false;
        for (Annotation annotation : annotations) {
            b = annotation instanceof Service || annotation instanceof Component;
        }
        return b;
    }
}

05、启动测试

上边的BeansInitUtil 工具类只实现了基本的,反射生成bean,没有考虑依赖的问题,先启动测试一下,添加一个com.jisuye.service.Abc 以及com.jisuye.service.impl.AbcImpl 模拟一个service bean的初始化,AbcImpl.java:

package com.jisuye.service.impl;

import com.jisuye.annotations.Service;
import com.jisuye.service.Abc;

@Service
public class AbcImpl implements Abc {

    @Override
    public int test(String name) {
        return 0;
    }
}

在启动方法 SquareApplication.run()方法里添加如下片段:

            //....
            // 输出banner
            printBanner(classesPathUtil.getProjectPath());
            Map<String, BeanObject> map = BeansInitUtil.init(clzz);
            log.info("beans size is:{}", map.size());
            tomcat = new Tomcat();
            //....

运行T.main()后查看项目输出:

......
19:04:29.850 [main] INFO com.jisuye.core.SquareApplication - beans size is:2
......

说明新添加的AbcImpl类已经初始化成功。并放到了Bean容器里。

ps: Abc和AbcImpl这两个类我会暂时先提到git上,从本文开始我后边的代码将以单独的分支提交,就不打tag了。

06、翻车时间

上边把初始化Bean的基本功能实现了一下,但按照目前的实现有一个问题,如果接口有两个实现类的话,那么初始化第二个实现类的时候会覆盖掉第一个Bean在map里设置的值(key 是相同的接口名),要怎么处理呢?

本篇代码地址: https://github.com/iuv/square/tree/square3

本文作者: ixx
本文链接: http://jianpage.com/2019/06/26/square3/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!

推荐阅读更多精彩内容