谈谈 Act 的依赖注入 和 模板输出 - 回答 drinkjava 同学提问

2023-06-02,,

1. 背景

依赖注入工具 jBeanBox 的作者 drinkjava 同学最近在 Actframework gitee 项目 的提出了如下评论:

你这个DI工具的出发点可能有问题,一个MVC工具为什么要引入DI依赖注入? 这个和Spring或Guice的功能重叠了。直接引入Spring或Guice的不好吗? 按我的观点,DI唯一比较经典的用法只是用来进行声明式事务才需要,从这个角度出发,就必须要DI支持AOP切面功能,而你却没有加入这个功能,这就很尴尬了,当需要声明式事务的时候,不得不引入一个支持AOP的DI工具,例如Spring/Guice/jFinal,这就造成了使用ACT的项目随时都具备了2套DI工具,也就是说你自带的DI工具实际上是多余的,尤其在流行的Boot环境下,各种配置都是建立在Spring-Core这个IOC/AOP工具基础上,别人不大可能移到Genie内核上。

另外考虑一下支持多种模板输出,如包括PDF输出,这才是MVC的V层要做的事,可以参见SpringMVC和Jfinal,必要时可抄它们的源码。jFinal的问题是DAO、IOC、MVC混成一团,是优点,更是一个大缺点,希望你将主要精力集中在MVC,将它做成一个精致、干净的后台表现层,不要介入任何DI、AOP、DAO、事务的工作,这方面优秀的、流行的工具太多,没必要重造轮子。你现在这个HTTP内核也是自已做的,为了一点点效率或钻牛角尖的小特性,放弃通用性,也是冒风险的,在项目MVC架构选型的第一步就可能被Pass,如果能将HTTP内核和MVC分开,比如说ACT的MVC即可以和自已的内核合用,也可以和Jetty或内嵌Tomcat内核合用,这才是一个比较优秀的架构。

首先感谢 drinkjava 同学的意见,看得出来是问题是认真思考之后提出来的. 下面就问题中的两段意见分别作答.

2. 问题一: 一个 MVC 工具为什么要引入 DI 依赖注入

这个问题有两个地方值得商榷:

    上面这个问题隐含的一个前提假设是 Act 是一个 MVC 工具. 实际上这个前提有一点问题, 我启动 Act 项目的动机是希望弄一个符合自己想法的 PlayFramework V1.x 的后继者. Play 本身除了是一个开发框架,也是一个运行时平台, Act 也是. 单单用 "MVC 工具" 来描述 Act 并不符合我自己的想法. 用 "MVC 工具" 来描述 Act 的依赖 osgl-mvc 更能够贴切一点.

    MVC 工具为什么不能引入 DI 依赖注入. 后面 drinkjava 同学也提到 "直接引入 Spring 或 Guice 的不好吗?", 说明 drinkjava 并不是认为 MVC 工具不能引入 DI 依赖注入, 而是认为 Act 引入的 DI 依赖注入 Genie 没有提供 AOP 功能, 而 AOP 功能在他看来是实现声明式事务必须的,所以才认为不适合.

2.1 Act 的依赖注入机制与应用

下面我们就 javadrink 同学上面的关切来谈谈 Act 的依赖注入机制与应用.

这里是来自 Wikipedia 对依赖注入的定义 "In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object". 简单地说就是对象的状态不由自己来创建,而是交给另外的对象来注入. 举个例子:

public class UserService {
    @Inject
    private Dao<User> userDao;

	@GetAction(www.leyouzaixan.cn "/users/{userId}")
	public User get(String userId) {
	    return userDao.findById(userId);
	}
}

上面是一个简单的 UserService 端口. 其中需要使用对应与 User 实体类的 Dao. 在上面的代码中我们没有看到 userDao 是如何初始化的, 因为 userDao 是 Act 框架在实例化 UserService 的时候注入的. 这就是一个典型的 Act 应用依赖注入的方式. 当然 Act 对于依赖注入的使用还有其他的扩展. 我们稍后会提到.

2.1.1 为什么不用 Guice 或者 Spring

现在我来回答 drinkjava 的这个问题: "直接引入 Spring 或 Guice 的不好吗?". 实际上在开发 Genie 之前, Act 尝试过另外两种依赖注入:

act-guice
act-feather

在 Act 正式发布之前, 这上面两种注入都曾经在 act 0.x 版本中进入过实际项目 (当然是老码农所在公司的 - 开坑自己先踩是老码农做开源的基本原则).最终我选择了自己开发 Genie 来提供 Act 的依赖注入, 主要原因有一下几点:

Feather 的实现足够简单轻量; 但并不是 JSR 330 的完整实现, 比如不支持方法注入, 对 Scope 的支持不完善, 扩展性不够好
Guice 提供了完整的 JSR 330 实现; 但同时也引入了一些额外的特性, 比如 Servlet 集成等. Guice 的代码实现也相对比较晦涩.
Spring 的依赖注入至始至终都不是我的一个选项, 首先 Spring 的依赖注入不是 JSR 330 标准的实现, 另外 Spring 的依赖注入运行时效率太低 (参见依赖注入性能测试项目).

Feather 简洁的代码实现最终激励了我启动了 Genie 项目, 这个依赖注入库完整实现了 JSR 330, 同时提供了一些有趣且实用的扩展, 比如注入集合类型数据以及注入值数据 等, 这些扩展对实现 Act 里面的 @LoadResource@LoadConfig 还有 @Configuration 至关重要. 另外因为代码实现比较紧凑, 运行时效率也很不错, 在多项测试中都领先 Guice; 具体数据可以参考这个项目

2.1.2 依赖注入扩展 I - 请求处理方法参数的注入

接下来说说 Act 对传统依赖注入的第一个扩展扩展: 注入请求处理方法参数.

上面那个 UserService 的例子是经典的依赖注入使用方式, 除了将依赖注入字段, Act 还允许直接将服务注入请求处理方法, 例如:

@GetAction("/users/userId")
public User get(String userId, Dao<User>www.jiuyueguojizc.cn userDao) {
    return userDao.findById(userId);
}

上面代码中, get 方法有两个参数, userId 和 userDao, 其中 userId 绑定到 URL 路径参数, 假如请求是 /users/abc123, 那 userId 的值就是 abc123; 而第二个参数 userDao 就是依赖注入了, 这个和前面注入到 userDao 字段是一样的. 但

又例如:

    @PostAction("login"www.wuji5pingtai.cn)
    public void login(String email, char[] password, ActionContext context) {
        User user = userDao.findByEmail(email);
        badRequestIf(null == user, MSG_BAD_EMAIL_PASSWORD);
        badRequestIfNot(user.verifyPassword(password), MSG_BAD_EMAIL_PASSWORD);
        context.login(user);
    }

上面代码中的 ActionContext 也是注入的对象.

2.1.3 依赖注入的扩展 II - 资源和配置参数注入

得益于 Genie 的扩展机制, Act 中可以很轻易地注入加载资源和配置参数.

public static class Dao extends MorphiaDao<Contact> {

    @LoadResource("industry_type.mapping")
    private Map<String, String> industryTypeMapping;

    @Configuration(www.kunlunyulegw.com"sql.url")
    private String jdbcUrl;

	...
}

上面的代码来自实际项目, 其中使用了两种扩展注入:

@LoadResource("industry_type.mapping) 将资源文件 industry_type.mapping 的内容加载进 Map<String, String> industryTypeMapping 字段
@Configuration("sql.url") 将配置项 sql.url 的值加载进 String jdbcUrl 字段

资源文件 industry_type.mapping 的内容为:

AG1 - Agriculture=Agriculture
AG2 - Agribusiness=Agribusiness
AG3 - Hospitality=Tourism and Hospitality
AG4 - Food Manufacturing/Processing=Food Manufacturing/ Processing
...

可以看出依赖注入在这种场景的使用减少了 boilerplate 代码的使用, 让应用代码变得更加简洁易懂.

2.1.4 依赖注入机制总结

通过上面关于依赖注入机制的介绍, 可以看出依赖注入在 Act 应用中是基本的机制, 而 drinkjava 同学在问题中表达的观点 "DI唯一比较经典的用法只是用来进行声明式事务才需要" 完全不能阐述依赖注入在 Act 框架的作用.

2.2 关于声明式事务和 AOP

drinkjava 同学提出 "DI唯一比较经典的用法只是用来进行声明式事务才需要,从这个角度出发,就必须要DI支持AOP切面功能,而你却没有加入这个功能". 这个我完全不能理解, DI 和 AOP 是完全不同的概念, 我从来不知道 DI 需要支持 AOP, DI 的 Java 标准 JSR 330 也完全没有提到这个特性. 而 Wikipedia 上 AOP 的页面 也根本没有谈到 Dependency Injection 的概念. 把这两个放在一起 Google 了一下, 发现这篇文章详细分解了一下这两个概念, 有兴趣的同学可以点击进去看看.

Act 目前不支持 AOP, 但 Act 提供的 SQL DB 插件, 包括 act-ebean, act-hibernate 以及 act-eclipselink 都支持声明式事务. 具体应用代码可以参考下面几个示例项目:

transaction-hibernate
transaction-eclipselink
transaction-ebean

act-ebean 和 act-hibernateact-eclipselink 对声明式事务的实现机制是不同的.

act-ebean 将声明式事务的实现交给 ebean 引擎. 而 Ebean 是采用了 java agent 对代码做增强来实现声明式事务
act-hibernate 和 act-eclipselink 对声明式事务的实现机制都在 act-jpa-common 插件中, 是通过 ASM 对代码做增强来实现的.

我并不认为 AOP 对于应用开发来说是一个非常重要的特性, 也一直没有动手做 AOP 的支持. 我的理由如下:

    我认为 AOP 的应用场合并不是非常普遍的, 可能的场景有:

    声明式事务 - ACT 有相应的实现机制 (act-db)
    Auditing (审计) - ACT 有相应的实现机制 (act-aaa)
    性能监控 - ACT 有相应的实现机制 (act-core, ASM based MetricEnhancer)
    异常处理 - 我不赞同复杂而精巧的异常处理 - 更新的语言包括 .net C# 还有 groovy, kotlin 等都去掉了 CheckedException 这个概念. Web 应用程序的异常处理应该尽量轻量化.
    通用的 AOP 的对于应用开发来说太过晦涩, 且容易导致难以调试的功能性以及性能方面的问题.

反过来讲, AOP 的适用场景都能采用专门的机制来实现, 对于应用来讲可以写出更加简洁容易的代码, 而且没有性能上的损耗. 这里我可以断言 drinkjava 同学评论中的说法 "当需要声明式事务的时候,不得不引入一个支持AOP的DI工具,例如Spring/Guice/jFinal,这就造成了使用ACT的项目随时都具备了2套DI工具,也就是说你自带的DI工具实际上是多余的" 是不成立的. 在 Act 中使用声明式事务以及我上面提到的另外两种 AOP 应用场景都不需要 AOP.

3. 问题二: 考虑一下支持多种模板输出,如包括PDF输出

这其实不是问题, 是一条建议. 看到这个建议我感觉 drinkjava 同学可能还不太熟悉 Act 的模板输出机制. views 示例项目展示了 Act 中同时使用多种不同的模板引擎的特性, 包括:

beetl
freemarker
mustache
rythm
thymeleaf
velocity

而 response-type 示例项目中展示了 Excel 模板的输出 (采用 JXLS 引擎). 可以说 Act 的模板输出框架是足够满足 (同时) 使用多种模板的. 当然到目前位置我还没有开发 PDF 的模板插件, 这个可以作为今后的一个工作.

4. 问题三: 集中精力在 MVC

这里回答 drinkjava 同学评论的最后一部分:

"jFinal的问题是DAO、IOC、MVC混成一团,是优点,更是一个大缺点,希望你将主要精力集中在MVC,将它做成一个精致、干净的后台表现层,不要介入任何DI、AOP、DAO、事务的工作,这方面优秀的、流行的工具太多,没必要重造轮子。你现在这个HTTP内核也是自已做的,为了一点点效率或钻牛角尖的小特性,放弃通用性,也是冒风险的,在项目MVC架构选型的第一步就可能被Pass,如果能将HTTP内核和MVC分开,比如说ACT的MVC即可以和自已的内核合用,也可以和Jetty或内嵌Tomcat内核合用,这才是一个比较优秀的架构。"

    "希望你将主要精力集中在MVC,将它做成一个精致、干净的后台表现层" - 在博客开头我也有讲, Act 并不是一个 MVC 工具, 而是 Web 应用框架以及运行时平台. 另外 "后台表现层" 是个什么鬼? 原谅我读书少, 理解不了这个术语 ^_^.
    "这方面优秀的、流行的工具太多,没必要重造轮子" 这句话我不太赞同, MVC 本身优秀的,流行的工具也很多,SpringMVC 就是一张大伞, 按照这个说法, Act 干脆就不要做了. 我的策略是, 首先看市面上有没有满足自己要求的, 有就用 (比如 Play, FastJSON, JXLS 等等), 没有就自己做 (比如 Rythm, Act, Genie 等等).
    "你现在这个HTTP内核也是自已做的,为了一点点效率或钻牛角尖的小特性,放弃通用性,也是冒风险的". 这里 drinkjava 同学高看我了, Act 的 HTTP 核心实现是 JBoss 的 Undertow. 我在悄悄打算迁移到 Netty 上去.
    "如果能将HTTP内核和MVC分开,比如说ACT的MVC即可以和自已的内核合用,也可以和Jetty或内嵌Tomcat内核合用,这才是一个比较优秀的架构。" - Jetty 和 Tomcat 是基于 Servlet 架构的 (不是 HTTP 网络层核心, 而是 API 层的架构), 我认为 Servlet 架构背负太多的历史报复, 对于现代 Web 框架来说并不是一个很好的选择. 至于 Act 是否是一个比较优秀的架构, 我并不是特别在意. 能在开发时提供友好的支持, 运行时提供足够的性能就行.

最后再次感谢 drinkjava 的评论, 很认真, 信息量很多, 所以我也很认真地使用一篇来回答你的评论.

谈谈 Act 的依赖注入 和 模板输出 - 回答 drinkjava 同学提问的相关教程结束。

《谈谈 Act 的依赖注入 和 模板输出 - 回答 drinkjava 同学提问.doc》

下载本文的Word格式文档,以方便收藏与打印。