Latke 源码解析(二)IOC 部分

本贴最后更新于 2791 天前,其中的信息可能已经渤澥桑田

上篇 Latke 源码解析(一)Servlet 部分讲解了 latke 有关 web 请求的 servlet 部分,这次深入了解一下它的 Ioc 部分内容。

前言

本文分析基于 latke2.3.6 版本,这部分内容主要涉及到了以下 2 个类库的使用,请留意。

  1. javax.enterprise 依赖注入相关(Contexts and Dependency Injection for Java)
  2. javassist,操作 java 字节码的类库

备注: 本人水平有限,若发现文章有误,请积极留言

一、从监听器开始

同 Spring 一样,latke 通过配置监听器初始化 bean。在 latke-demoweb.xml 监听器部分如下

<listener>
        <listener-class>org.b3log.latke.demo.hello.HelloServletListener</listener-class>
</listener>

从这里可以看出,该 demo 的监听器为 HelloServletListener。下面看看 HelloServletListener

HelloServletListener.java

public class HelloServletListener extends AbstractServletListener {
    @Override
    public void contextInitialized(final ServletContextEvent servletContextEvent) {
        Latkes.setScanPath("org.b3log.latke.demo.hello");  //设置项目扫描的包
        super.contextInitialized(servletContextEvent);  //调用父类的contextInitialized()方法
        
        //省略其他操作  
    }
    //省略其他方法   
}

HelloServletListener 继承了 AbstractServletListener 。而 AbstractServletListener 是 latke 提供的默认监听器抽象类,在此类中已实现 bean 的查找注册等许多功能。此类也是我们稍后分析的重点,让我们先看一些它的定义。(为简洁,已去掉与 ioc 无关内容)

AbstractServletListener.java

public abstract class AbstractServletListener implements ServletContextListener, ServletRequestListener, HttpSessionListener {
    /**
     * Servlet context.
     */
    private static ServletContext servletContext;
    
    @Override
    public void contextInitialized(final ServletContextEvent servletContextEvent) {
       //省略其他配置项
        try {
            final Collection<Class<?>> beanClasses = Discoverer.discover(Latkes.getScanPath()); //按需要加载项目中的类文件

            Lifecycle.startApplication(beanClasses); // 创建注册bean
        } catch (final Exception e) {
           //异常处理
        }
        CronService.start();  //定时任务
    }

//其他方法

可以知道,HelloServletListener 通过继承 AbstractServletListener 实现了 ServletContextListener 这个监听器,可以监听 servlet 容器创建销毁事件。当项目启动时,contextInitialized()方法执行,bean 就可以被加载及初始化。从而为 servlet 部分的请求服务。

须知:HelloServletListener 还实现了 ServletRequestListener,ServletRequestListener 两个监听器,用于监听 request、session 变化,不过这里不是重点。

二、AbstractServletListener

我们已经知道,此类的 contextInitialized 方法在项目启动时执行,完成 bean 的创建工作。

具体的执行逻辑分为两步,对应方法内的两行代码

  1. Discoverer.discover(Latkes.getScanPath())

    扫描这些指定的类,选出其中包含特定注解的类(@RequestProcessor、@Service 等),并加载这些特定类。

  2. Lifecycle.startApplication(beanClasses);

beanClasses 为上步得到的类,解析这些类依赖并创建为 bean。

1.discover 方法

discover 接受字符串形式的类路径,一般是项目的根目录包,也好理解,扫描就要扫描整个项目,把能注册的 bean 的类先找到再说。如 demo 项目中,HelloServletListener 传的就是 org.b3log.latke.demo.hello

在 discover 方法中,要知道:

  1. discover 的主要任务是找到特定类并加载它们。
  2. 除传入的类路径外,discover 还要扫描并处理一些内置的包,如 org.b3log.latke.remote 这个包。
  3. discover 使用了 javassist 类库中的 ClassFile,javassist 是一个用来处理 Java 字节码的类库。详细请参考 javassist 使用指南

具体的解析过程较为复杂繁琐。这里只看看大致流程的代码

    public static Collection<Class<?>> discover(final String scanPath) throws Exception {
          //将传入类包与内置包组合
          final String[] paths = ArrayUtils.concatenate(splitPaths, BUILT_IN_COMPONENT_PKGS);
          
         //遍历路径,将其转为磁盘绝对路径,便于javassist解析
         for (String path : paths) {
            if (!AntPathMatcher.isPattern(path)) {
                path = path.replaceAll("\\.", "/") + "/**/*.class";
            }
            urls.addAll(ClassPathResolver.getResources(path));
         }
        for (URL url : urls) {
            //javassist读取class文件,并将文件中类注解信息解析出来
             //遍历一个类下所有注解
            for (final Annotation annotation : annotations){
                //包含`@RequestProcessor,Service,Repository,Named`注解将maybeBeanClass设置true    
            }
            if (maybeBeanClass) {  
              clz = Thread.currentThread().getContextClassLoader().loadClass(className);  //符合条件,加载此类
              ret.add(clz);
            }
        } 
        return ret; //返回Class<?>类型集合信息
    }

2. startApplication 方法

上步中,已经加载了这些 bean 类,下面就该解析这些 bean 的依赖关系并放在所谓的 bean 容器中以便于管理了。

startApplication 的代码不多,我们来看看

 public static void startApplication(final Collection<Class<?>> classes, final BeanModule... beanModule) {
       
        beanManager = LatkeBeanManagerImpl.getInstance();  //获得beanManager实例

        applicationContext.setActive(true);

        beanManager.addContext(applicationContext);   //添加上下文
        final Configurator configurator = beanManager.getConfigurator();

        if (null != classes && !classes.isEmpty()) {
            configurator.createBeans(classes);  //classes为discover()的解析值,据此创建bean
        }
        //设置beanModule
    }

其中最为关键的是 configurator.createBeans(classes); 这行代码,查看 ConfiguratorImpl createBeans() 方法,为遍历调用 createBean() 方法创建。

 public <T> LatkeBean<T> createBean(final Class<T> beanClass) {
        try {
            return (LatkeBean<T>) beanManager.getBean(beanClass);  //若存在直接返回bean
        } catch (final Exception e) {
            LOGGER.log(Level.TRACE, "Not found bean [beanClass={0}], so to create it", beanClass);
        }
        if (!Beans.checkClass(beanClass)) {  //检查是否是符合条件的bean,不能是接口或抽象类
           //抛出异常
        }
        final String name = Beans.getBeanName(beanClass);

        if (null == name) {
           //打印日志,返回
        }
        //获取此bean@Qualifier注解信息
        final Set<Annotation> qualifiers = Beans.getQualifiers(beanClass, name); 
        //获取此bean@Scope注解信息 
        final Class<? extends Annotation> scope = Beans.getScope(beanClass);
        //获取此bean父类及实现的接口信息
        final Set<Type> beanTypes = Beans.getBeanTypes(beanClass);
        
        final Set<Class<? extends Annotation>> stereotypes = Beans.getStereotypes(beanClass);
         //创建bean实例,此为真正创建的方法
        final LatkeBean<T> ret = new BeanImpl<T>(beanManager, name, scope, qualifiers, beanClass, beanTypes, stereotypes);

        beanManager.addBean(ret);  //将bean添加到容器中

        for (final Type beanType : beanTypes) {
            addTypeClassBinding(beanType, beanClass);
        }

        for (final Annotation qualifier : qualifiers) {
            addClassQualifierBinding(beanClass, qualifier);
            addQualifierClassBinding(qualifier, beanClass);
        }

        return ret;
    }

三、深入 bean 创建过程

BeanImpl 的构造方法是创建 bean 的关键,来看看


 public BeanImpl(final LatkeBeanManager beanManager, final String name, final Class<? extends Annotation> scope,
        final Set<Annotation> qualifiers, final Class<T> beanClass, final Set<Type> types,
        final Set<Class<? extends Annotation>> stereotypes) {
        this.beanManager = beanManager;
        this.name = name;
        this.scope = scope;
        this.qualifiers = qualifiers;
        this.beanClass = beanClass;
        this.types = types;
        this.stereotypes = stereotypes;

        this.configurator = beanManager.getConfigurator();

        javassistMethodHandler = new JavassistMethodHandler(beanManager);
        final ProxyFactory proxyFactory = new ProxyFactory();

        proxyFactory.setSuperclass(beanClass);
        proxyFactory.setFilter(javassistMethodHandler.getMethodFilter());
        proxyClass = proxyFactory.createClass();  //得到该bean的Class对象

        // ① 查看这个bean在构造器、方法、字段上的有无@Inject注解,有则存起来
        annotatedType = new AnnotatedTypeImpl<T>(beanClass);  

        constructorParameterInjectionPoints = new HashMap<AnnotatedConstructor<T>, List<ParameterInjectionPoint>>();
        constructorParameterProviders = new ArrayList<ParameterProvider<?>>();
        methodParameterInjectionPoints = new HashMap<AnnotatedMethod<?>, List<ParameterInjectionPoint>>();
        methodParameterProviders = new HashMap<AnnotatedMethod<?>, List<ParameterProvider<?>>>();
        fieldInjectionPoints = new HashSet<FieldInjectionPoint>();
        fieldProviders = new HashSet<FieldProvider<?>>();
        
        //根据①中结果,处理依赖
        initFieldInjectionPoints();  
        initConstructorInjectionPoints(); 
        initMethodInjectionPoints();  
    }

主要有两个过程,一是通过 javassist 得到这个 bean 的 Class 对象,以便后续操作。再者初始化该类依赖。上面注释了流程。我们来看看怎样初始化字段上的 @Inject 注解

BeanImpl.java

  private void initFieldInjectionPoints() {
       //此bean中含有@Inject注解的set集合
        final Set<AnnotatedField<? super T>> annotatedFields = annotatedType.getFields();

        for (final AnnotatedField<? super T> annotatedField : annotatedFields) {
            final Field field = annotatedField.getJavaMember();

            if (field.getType().equals(Provider.class)) { // by provider
                final FieldProvider<T> provider = new FieldProvider<T>(beanManager, annotatedField);

                fieldProviders.add(provider);

                final FieldInjectionPoint fieldInjectionPoint = new FieldInjectionPoint(this, annotatedField);

                fieldInjectionPoints.add(fieldInjectionPoint);
            } else { // 字段类型不是Provider走这个流程
                //构造一个FieldInjectionPoint对象,加入此bean的fieldInjectionPoints中
                final FieldInjectionPoint fieldInjectionPoint = new FieldInjectionPoint(this, annotatedField);

                fieldInjectionPoints.add(fieldInjectionPoint);
            }
        }
    }

至此,bean 的属性基本已构造完毕,以 HelloProcessor 这个类对应的 bean,我添加了一个 Service 依赖,如下

@RequestProcessor
public class HelloProcessor {
    @Inject
    private UserService userService;

    @Before(adviceClass = HelloAdvice.class)
    @RequestProcessing(value = {"/", "/index", "/index.*", "/**/ant/*/path"}, method = HTTPRequestMethod.GET)
    public void index(final HTTPRequestContext context)     {
       //省略
    }   
}

则此 bean 到目前分析这,其属性构造如下图

亮的部分 fieldInjectionPoints 属性表示该 bean 字段上的依赖集合,可以看到 userService 这个依赖。至此 bean 就算基本创建完毕了。其他部分诸如分析依赖的工作,等下次有时间再分析吧。

参考文章:

  1. Javassist 使用指南(一)
  2. Javassist 使用指南(三)
  3. 3.11.3 JSR-330 标准注解的限制
  4. Java 依赖注入标准(JSR-330)简介
  5. CDI 是什么?
  • Latke

    Latke 是一款以 JSON 为主的 Java Web 框架。

    71 引用 • 535 回帖 • 789 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注
  • IoC
    17 引用 • 29 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • 88250

    感谢分享,btw,对 JSR299 和 330 的实现在最新的 Latke 中移除了,以避免和容器的实现产生冲突。

    1 回复
  • wthfeng
    作者

    哦,还真没注意这个,只看到引了 javax.inject 的内容,谢提醒。

  • pangwen

    这个源码解析真的挺不错,我自己也看了好久,完全没你分析的这么清楚,只是大概知道要干什么。。

    然后我一直比较疑惑的地方就是 JavassistMethodHandler 这个是对所有方法进行拦截还是可以配置的啊,求指教~

    2 回复
  • wthfeng
    作者

    我又细看了看,感觉应该是这样:

    创建 bean 的时候,把 javassistMethodHandler 与 bean 的一个属性 proxyClass 关联起来设置到 bean 里了。这样当请求到来找到要执行的方法后,委托给 javassistMethodHandlerinvoke 执行。实际方法也在里面执行了。具体可看看 invoke 方法。

    
            handleInterceptor(invokingMehtodName, params, BeforeMethod.class);
          
                //实际方法
    		ret = proceed.invoke(proxy, params);
    
            handleInterceptor(invokingMehtodName, params, AfterMethod.class);
    
    

    也就是只要是请求方法都会被此类的 invoke() 拦截。应该可使用 @BeforeMethod,@AfterMethod 在方法前后做一些操作。
    这部分具体细节我还没研究,。欢迎随时交流。

  • 88250

    目前是所有方法都会拦截,不过感觉对性能没多大影响,所以就没有细化了。

  • tmedivh

    都是大神呀

  • xzuse

    这个语音播放是错的吧,怎么说的是 http 连接?

  • nobt

    请问您在此之前是否看过 spring 的源码,这个源码跟 spring 源码相比如何?

    1 回复
  • 88250

    比 Spring 简化很多,没有可比性。如果是学习的话可以看看 Latke,代码不多。

  • 88250

    @participants Lakte v2.4.18 重写了 IoC 容器,去除了很多用不到的特性,对实现进行了大量简化。

请输入回帖内容 ...