@yong9981 在 actframework-1.8.32 发布新闻 中提出了一下问题:
我的回应是:
然后 yong 同学非常热心的贴了下面这段评论:
这里面我用红色标注出两段有趣的论点:
你提供的功能和SpringBoot/JBoot有很多重叠的部分
这个有问题吗? 还是不应该重复发明轮子?
SpringBoot的基本原理是用一个通用的IOC(AOP)工具配置一堆第三方工具,这些第三方工具原本和SpringBoot没有一毛钱关系,只是因为它们的优秀被Spring看中整合进来,你的竟争对手是这么个集成体系,而且它随时可以添加新的模块进来,因为本质上通用IOC工具的作用就是用来初始化Bean的。它们有个共同的特点:没有源码,所以比较适合用YML/XML或Java方式配置,而不是用JSR330注入。
这段话里面包含下面几段陈述:
至于 yong 同学在上面说的建议, 我的建议是:
接下来 yong 同学讲到:
类Guice的配置不如Spring配置通用和方便
类 Guice 配置是 Java 的标准, 不如 Spring 配置通用是现状, 不如 Spring 配置方便我不认同.
需要开发插件往往是要用AOP功能的,如果不需要AOP,直接new就行了。比方说spring-data-mongodb也实现了AOP联盟标准,所以Guice也可以拿来用的,但你看Genie能不能拿来用?这是个架构问题,导致所有spring插件你都必须重新开发一个,偷懒都偷不了
@yong9981 这是我最后一次就 IOC 和 AOP 的问题回答你, 我们以前已经就此问题讨论多次: 2019-11-30 的讨论, 2019-12-25 的讨论, 2019-12-26 的讨论
最后来看看你的项目:
那个 Guice/jBeanBox 实现 Spring 声明式自动回滚事务的就不多说了, Genie 没有实现 AOP, 所以我做不了. 但放在一个更大的 Context 下, Act-Db 是可以做自动事务回滚的, 这是不同的生态. 再次强调, 别让我去支持 Spring 机制, 我不会容忍在 Act 代码里面引入一大堆 Spring jar 文件这样的事情 更别让我因为要支持 Spring 机制, 所以在 Genie 中实现 AOP!
你项目这个 Question 倒可以谈谈:
我第一眼看这个代码就想问: 干嘛要先搞个
public static class CarConfig extends BeanBox {
{
this.injectConstruct(car, String.class, color);
}
}
然后
Car car = JBEANBOX.getBean(CarConfig.class);
直接
Car car = new Car1(color);
它不香吗?
yong9981 大概要说, 没看见我的 color
和 car
都标注有 "// read from file" 吗?
那我的回答是, 有多么蠢的项目才会这样来配置?
直接在配置文件里面给出实现类的情况有没有? 当然有, 比如你的数据库插件可能是 Hibernate 的, 也可能是 MongoDB 的, 所以会在配置文件中指定数据库插件实现类. 另一个例子, 假设你使用 osgl-storage 来存放上传文件, 你会在配置文件中指定存储系统是本地文件系统, 还是七牛云, 或者 Azure Blog, 这种情况, 也需要直接在配置中给出存储服务的实现类. 然而这些情况的共同特点是都是 Heavy load, 需要的配置和初始化, 绝不仅仅用一个构造函数就搞定的. 为应用完成重型对象配置和初始化工作正是插件的价值.
那 DI 注入本身有没有价值呢? 当然有, 假设你有两个数据库服务配置:
db.instances=sales,marketing
db.sales.implementation=org...HibernateService
db.sales.url=<jdbc-url to sales db>
db.marketing.implemenation=org...MorphiaService
db.marketing.url=<jdbc-url to marketing db>
DI 的威力就在于代码可以清晰地指定需求而无需考虑如何准备这个需求, 例如:
public class XyzService {
@Named("sales")
@Inject
private DataSource salesDs;
@Named("marketing")
@Inject
private DataSource marketingDs;
}
当然我注意到 yong9981 在代码中演示的特性是 "使用外部工具时,比如说A中要注入B属性,B的构造器要注入C对象这种, 而且A,B,C全是第三方工具,拿不到源码,所以不能使用注解方式去配置。". 我觉得这里面需要考虑的一个问题是 这种没有考虑依赖注入的代码是否值得 DI 工具去适配, 或者需要适配到何种地步
jBeanbox 的 API 提供了一种比较粗糙的 API 包装来强行适配这种场景 (估计实际案例中很少会出现吧):
其中全然没有考虑一点类型安全, 在其演示代码中, 做一点改变, 让 Car1
不再继承 Car
, 即下面代码中的 car
不再是 Car
的实例:
在编译时没有办法检测到这样的问题. 知道运行时才会在这里抛出错误:
这样的代码貌似简洁 (其实在所有已知条件下, 反而不如直接用 new 操作符), 但把 Java 这种类型安全语言当做动态语言来使用, 实际项目上怕是有点隐隐担忧吧.
从我看来, DI 工具为所谓三方库提供这样的适配是得不偿失的. 那 Genie 能否处理 Contructor binding 呢, 当然是可以的. 下面是 Genie 的实现代码:
这里我们看到了几个地方的不同, 首先将 Car.class
绑定到 Car1.class
的过程是类型安全的. 我们把 Car1 改写, 让其不要继承 Car, 我们发现 IDE 会有错误提示:
其次, 我们并没有直接向构造函数绑定中去写某个具体的值 e.g "red", 而是通过 @Named
注解来告诉 DI 引擎, 当你遇到名字为 color
的字串的时候, 提供 red
这个值. 这样的做法看起来有这样的问题, 如果你的构造函数参数上面没有 @Named
注解, 那就没法绑定到需要的值了. 在此我想强调的是依赖注入处理的应用程序逻辑拓扑, 并不是数据. 每个注入的对象都应该是一个特定概念, 构造函数绑定也不应该脱离这个观念. 因此你注入的对象要不应当是一个特定类型, 要不是普通数据类型假设某个 Qualifier (比如 @Named
) 来限定这个概念范围.
设想你有下面的构造函数:
public class MyAwsS3ServiceAgent {
private String awsKey;
private String awsSecret;
public MyService(@Named("aws.key") awsKey, @Named("aws.Secret") awsSecret) {
this.awsKey = awsKey;
this.awsSecret = awsSecret;
}
}
在构造函数中清楚地地指定了参数的概念, 因此我们可以让 DI 引擎发现其中的逻辑关系并提供需要的值绑定. 看官可能要问, 如果我用的是很老的库, 的确没有 @Named
这样的机制怎么办. 我的回答是应用提供一层 Wrapper 来封装这个库, 适配到 DI 引擎. 这种做法的优势在于所有的概念都很清晰, 所有的值来源也很透明. 代码的可维护性和采用 jBeanBox.injectConstruct
这样晦涩不清的 API 相比不可同日而语.
另一方面, yong9981 的代码除了有两句注释说明 car 和 color 的值来自配置文件, 但其实根本没有演示如何把配置从文件加载进 JVM 的. 下面的代码演示 Genie 是如何处理这个过程的.
为了更好地展示 Genie 这方面的能力, 我们在 Car
中添加一个整型字段: seats
(座位数):
我们在 test/resources/test.properties
文件中准备好配置数据:
然后我们需要适配 Genie 提供的 ConfigurationLoader 机制到这个配置文件中:
注意上面的适配机制每个应用只需要完成一次即可. 下面是绑定和测试代码:
注意到 Genie 的配置机制很聪明地将配置文件中的 "6" 变成需要的整型变量 6 了吗? ActFramework 中大量使用了这样的机制. 大家可以参考一下这个演示项目
总结一下: 提供工具库, 比如 Genie 这样的 DI 引擎, 我们应该仔细思索提供这个工具的目的是什么, DI 的目的到底是什么, 在什么层面上可以帮助应用程序, 使用这个工具是否有利于应用程序的代码组织, 维护, 是否鼓励更好的编码方式. 毫无选择地提供粗糙的封装 API 只能让工具沦为没有价值的玩具
最后, 文中的代码都在 https://gitee.com/greenlaw110/GuiceSpringTx 项目中, 有兴趣的朋友可以 Checkout 出来看看