IOC容器 - mrcao20/McQuickBoot GitHub Wiki
所有的功能其实都能在Examples目录下的子项目中找到,关于相关样例的解析请参考博客,下面更多的是对这个库的大致介绍,注意事项及原因,以及关于各种小功能点和相关函数以及宏的解析。
- IOC容器为此仓库中的McIoc项目,目前支持配置文件注入和声明式注入两种方式。虽然配置文件注入提供了YAML和XML两种方式,但建议使用XML的方式。XML格式才能更加清晰的表述对象与对象之间的关系、对象中的属性等内容。YAML在此库中的实现具有迷惑性,虽然在McQuickBoot中使用了这种方式,但是它进行了额外的优化才使其用起来更加人性化。
- 为了方便堆内存的管理,容器中的所有对象均使用QSharedPointer包裹,并且将其重定义为ClassPtr(Class为你定义的类名),并且建议你使用这种形式:如声明一个变量时使用ClassPtr var;在其他功能中会介绍自定义对象析构的功能。
- 如果某个类实现至某些接口,在使用时又是使用的这些接口,那么该类在定义时需要使用MC_TYPELIST宏(最新版提供了一个功能相同的宏MC_INTERFACES,并且建议用这个)来声明这些接口:
下面给个不用接口的示例
// 以下为头文件定义 #include <McIoc/McGlobal.h> // 通常来说只需要引入这个头文件即可 class IA { public: // C++中基类析构函数必须为虚函数 virtual ~IA() = default; }; MC_DECL_METATYPE(IA) // 此接口需要在IOC中使用,则必须使用该宏 class IB : public IA { MC_INTERFACES(IA) // 由于此接口继承至IA,所以需要特别声明IA public: virtual ~IB() = default; }; MC_DECL_METATYPE(IB) class C : public QObject, public IB { Q_OBJECT MC_INTERFACES(QObject, IB) // 此类继承至QObject,并且实现至IB,所以需要特别声明。QObject可以不用声明,容器会自动指定,这里只是更加具体的说明而已 }; MC_DECL_METATYPE(C) // 以下为cpp定义 MC_STATIC() MC_REGISTER_BEAN_FACTORY(C); // 注册IOC MC_STATIC_END // 以下为正式使用 IAPtr a = appCtx->getBean<IA>("c"); // 这里真正获取的类型是IA,返回的类型为IAPtr
这里只是说明MC_INTERFACES宏,getBean的参数并没有具体说明,更详细的代码可以参考博客或者Examples中的IocTest项目。代码中的MC_STATIC宏在其他功能中可以找到// 以下为头文件定义 class C : public QObject { Q_OBJECT // 因为这里只继承了QObject,所以不需要特别定义MC_TYPELIST }; MC_DECL_METATYPE(C) // 以下为cpp定义 MC_STATIC() MC_REGISTER_BEAN_FACTORY(C); MC_STATIC_END // 以下为正式使用 CPtr a = appCtx->getBean<C>("c"); // 这里真正获取的类型是C
- IOC容器提供了四个宏来提供额外的功能
- MC_STARTED:
如果一个对象从IOC容器中读取,并且该对象中存在上面这种被MC_STARTED标记的函数,那么该函数将会在该对象的构造函数调用完成之后被立即调用。注意:该函数的调用线程为调用getBean/refresh的线程,和对象本身的生存线程无关。
Q_INVOKABLE // 该函数必须优先被该宏标记为元对象可以调用的函数 MC_STARTED // void start() noexcept; // 函数名可以随便起
- MC_FINISHED:
同理,该函数将会在该对象的所有属性全部注入完成之后调用。注意:该函数的调用线程为调用getBean/refresh的线程,和对象本身的生存线程无关。
Q_INVOKABLE MC_FINISHED void end() noexcept;
- MC_THREAD_MOVED:
注意:该函数不一定会调用,appCtx->getBean方法的第二个参数可以传入一个对象的生存线程,只有当传入的线程和IOC容器中的对象的线程不一致时,该函数才会被调用。注意:该函数的调用线程为调用getBean/refresh的线程,和传入的的生存线程无关。
Q_INVOKABLE MC_THREAD_MOVED void threadEnd() noexcept;
- MC_COMPLETE:
上面三个宏你都可以不使用,仅仅只使用此宏即可满足所有需求。该函数将会在对象的所有构造操作完成之后调用,并且调用线程为对象的生存线程。即:如果调用getBean时没有传入生存线程,那么将使用Qt::DirectConnection的方式调用;如果传入了生存线程,但是该线程没有调用start函数启动的话,将会使用Qt::QueuedConnection方式调用,并且会给出相应的提示信息;如果传入了生存线程,并且已经调用start函数启动了的话,将会使用Qt::BlockingQueuedConnection的方式调用。
Q_INVOKABLE MC_COMPLETE void allFinished() noexcept;
- 这里有一个注意点,由于QT元对象的限制,如果标记的函数是一个虚函数,那么virtual必须放在Q_INVOKABLE之前,也就是说Q_INVOKABLE或者MC_STARTED等tag必须在函数的返回值前才能被识别:
上面的函数无法被识别,virtual需要调换位置放在最前面
Q_INVOKABLE MC_FINISHED virtual void end() noexcept;
由于Q_GADGET没有生存线程的概念,所以它只能使用第一二种类型的函数,Q_OBJECT可以使用所有4中类型。virtual Q_INVOKABLE MC_FINISHED void end() noexcept;
- MC_STARTED:
- 不能存在一样的bean name,如果存在,则只会保留最后一个。在XML文件中最后面的为最后一个,但是用声明式方法时无法确保先后
- 不要声明一个beanName为this的bean
- 如果bean中没有将要注入的属性,那么容器会调用QObject::setProperty函数注入动态属性
- 受QT自身插件系统性质影响,同一个插件在同一个程序中只能创建一次,即不能将同一个插件声明两次bean:
<bean plugin="p1"></bean> <bean plugin="p1"></bean>
即上面的两种用法是错误的,因为QT的插件在同一个程序中是单例的,这会造成多次析构同一个插件。但可以将同一个插件bean多处使用。<bean plugin="p1"></bean> <list plugins="包含文件p1的路径"></list>
- getBean时生成的bean的生存线程问题。由于QT对线程的强控制,所以在getBean之后可能会存在线程冲突问题,所以容器内使用自动和手动指定线程的两种形式:
- 如果指定的目标线程为空,那么获取到的bean及其QObject属性都将生存在调用getBean时的线程中。注意:
- 这里的getBean指的是IMcBeanFactory的getBean,而不是IMcApplicationContext。因为IMcApplicationContext存在refresh函数自动加载bean,此时bean的生存线程将是调用refresh时的线程,如果不调用refresh,那么bean的生存线程才是getBean时的线程
- 有如下形式:
- 如果在次线程中创建appContext,如果在次线程中调用getBean,那么获取到的bean的线程为次线程。如果在主线程调用getBean,那么获取到的bean的线程为主线程。
- 如果在次线程中创建appContext,如果在次线程中调用了refresh函数,那么getBean获取到的bean的线程永远在次线程,反之,如果在主线程中调用refresh,那么bean的线程在主线程。
- 上面两种情况只适用于将isSingleton设置为true时的情况,如果isSingleton为false,那么每次调用getBean时都会创建一个新对象,此时获取到的bean就永远为调用getBean时的线程。
- 可以在调用getBean或refresh函数时指定一个目标线程,那么目标bean的生存线程将为指定的线程。同样的,只适用于isSingleton为true的情况。同时,如果isSingleton指定为true,但是在次线程中调用了refresh函数,就算在主线程中调用getBean时指定了目标线程也不会起作用,因为只能在对象创建时才能指定线程。
- 提供上面形式的线程指定方式的主要目的在于,如果XML中声明的对象结构过于复杂,对象的创建可能耗时太长,那么可以在子线程中完成创建,并且能指定目标线程。
- 如果指定的目标线程为空,那么获取到的bean及其QObject属性都将生存在调用getBean时的线程中。注意:
容器中的对象由QSharedPointer来管理其内存,但是在某些情况下可能并不希望QSharedPointer析构包裹的对象,我们想自己在适当的时候析构该对象。所以容器提供了IMcDestroyer接口,当QSharedPointer的引用计数为零,该对象并不会直接delete,而是调用IMcDestroyer接口的destroy方法。所以你只需要实现至该接口(注意要用MC_INTERFACES声明),重写destroy方法,即可在该方法中实现自己的析构方法和析构时间。比如调用QObject::deleteLater、delete this、或者直接将该方法置空,在其他地方析构。
在使用QT元对象的时候难免会遇到调用qRegisterMetaType的情况,通常的处理方法是在一个公共的地方,或者main函数中调用,这样做未免有点麻烦,所以我提供了类似于JAVA静态代码块的功能。
我提供了两种方式,各有优缺点。
- 只需要在cpp中使用MC_STATIC宏就能完成:
这段代码将在QCoreApplication系列的对象构造时调用。并且MC_STATIC()的括号里面可以加参数用来指示执行的优先级(默认不加时为0):
MC_STATIC() qRegisterMetaType<Class>("Class"); MC_STATIC_END
上面三个代码块中的执行顺序为C->A->B。A.cpp MC_STATIC() qRegisterMetaType<Class>("Class"); MC_STATIC_END B.cpp MC_STATIC(-1) qRegisterMetaType<Class>("Class"); MC_STATIC_END C.cpp MC_STATIC(1) qRegisterMetaType<Class>("Class"); MC_STATIC_END
使用这种方式的时候要注意的是同一个cpp文件中只能存在一个由MC_STATIC()和MC_STATIC_END包裹的代码块,而且不能使用类Class中的私有类型、函数和变量。 - 在h中的类声明中使用MC_DECL_INIT,在cpp中使用MC_INIT
效果和MC_STATIC一样,并且MC_INIT也可以传入第二个参数来指示执行优先级。
.h class Class { MC_DECL_INIT(Class) // 尽量在private中声明 }; .cpp MC_INIT(Class) qRegisterMetaType<Class>("Class"); MC_INIT_END
这种方法两种好处:- 同一个cpp文件中可以存在多个MC_INIT()和MC_INIT_END包裹的代码块,只要相应头文件中有声明。
- 该代码块中可以使用Class类中的私有类型
.h class Class { MC_DECL_INIT(Class) // 尽量在private中声明 using Integer = int; // 私有类型 }; .cpp MC_INIT(Class) qRegisterMetaType<Class>("Class"); Class::Integer a = 1; // 可以使用 MC_INIT_END
- 我同时提供了一个额外的功能,在MC_STATIC和MC_STATIC_END之间插入MC_DESTROY宏,那么在MC_STATIC和MC_DESTROY之间的代码将在构造时调用,MC_DESTROY和MC_STATIC_END之间的代码将在析构时调用:
同样的,MC_DESTROY()的括号中也可以指定优先级。
MC_STATIC() qRegisterMetaType<Class>("Class"); // 这行代码将在QCoreApplication系列的对象构造时调用 MC_DESTROY() qDebug() << "destroy............."; // 这行代码将在QCoreApplication系列的对象析构时调用 MC_STATIC_END
MC_INIT同理。