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

2019-12-02 13:29:16 浏览数 (1)

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 依赖注入

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

  1. 上面这个问题隐含的一个前提假设是 Act 是一个 MVC 工具. 实际上这个前提有一点问题, 我启动 Act 项目的动机是希望弄一个符合自己想法的 PlayFramework V1.x 的后继者. Play 本身除了是一个开发框架,也是一个运行时平台, Act 也是. 单单用 "MVC 工具" 来描述 Act 并不符合我自己的想法. 用 "MVC 工具" 来描述 Act 的依赖 osgl-mvc 更能够贴切一点.
  2. 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". 简单地说就是对象的状态不由自己来创建,而是交给另外的对象来注入. 举个例子:

代码语言:javascript复制
public class UserService {
    @Inject
    private Dao<User> userDao;
	
	@GetAction("/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 还允许直接将服务注入请求处理方法, 例如:

代码语言:javascript复制
@GetAction("/users/userId")
public User get(String userId, Dao<User> userDao) {
    return userDao.findById(userId);
}

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

又例如:

代码语言:javascript复制
    @PostAction("login")
    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 中可以很轻易地注入加载资源和配置参数.

代码语言:javascript复制
public static class Dao extends MorphiaDao<Contact> {

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

    @Configuration("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 的内容为:

代码语言:javascript复制
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-ebeanact-hibernate, act-eclipselink 对声明式事务的实现机制是不同的.

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

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

  1. 我认为 AOP 的应用场合并不是非常普遍的, 可能的场景有:
    • 声明式事务 - ACT 有相应的实现机制 (act-db)
    • Auditing (审计) - ACT 有相应的实现机制 (act-aaa)
    • 性能监控 - ACT 有相应的实现机制 (act-core, ASM based MetricEnhancer)
    • 异常处理 - 我不赞同复杂而精巧的异常处理 - 更新的语言包括 .net C# 还有 groovy, kotlin 等都去掉了 CheckedException 这个概念. Web 应用程序的异常处理应该尽量轻量化.
  2. 通用的 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内核合用,这才是一个比较优秀的架构。"

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

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

0 人点赞