JavaEE 框架预备知识

JavaEE 框架预备知识:JavaBean 与 POJO、Model1 与 Model2、SSH 与 SSM 框架、配置文件与注解、类库与框架。

JavaBean 与 POJO

JavaBean 是什么?
JavaBean 是符合一定规范的 Java 类,所谓规范也就是约定,只要符合这 3 条约定的 Java 类就可以称为 JavaBean:

  • 所有属性都是私有的(提供 getter/setter 方法)
  • 拥有一个 public 的默认无参构造函数
  • 实现了 java.io.Serializable 接口

对于非布尔类型的属性 xxx,使用 getXxx()setXxx() 方法;
而对于布尔类型的属性 xxx,可使用 isXxx()setXxx() 方法。

比如,下面这个 StudentBean 类就是一个 JavaBean:

那么 JavaBean 有什么用呢?想详细了解的可以参考这两篇文章:Java 帝国之 Java bean (上)Java 帝国之 Java bean(下)

说说我自己的理解:JavaBean 最开始是 GUI 开发中提出的一个概念,Bean 的含义是“组件”,每个 JavaBean 都是一个独立的可重用的 Java 类(组件)。当我们添加某个组件时,IDE 就可以利用 JavaBean 的默认无参构造函数来创建它的实例;当我们想设置这个组件的相关属性时,IDE 可以通过反射 API 来获取这个 Bean 的所有方法的名称,然后找出符合 setXxx()getXxx() 命名格式的方法,这样 IDE 就能提供一个属性清单给我们选择(只有对应的 getXxx() 方法 IDE 会认为这是只读属性,只有对应的 setXxx() 方法 IDE 会认为这是只写属性,如果二者都有,那么它就是可读写的属性);而序列化接口的作用,是为了避免在关闭 IDE 之后丢失了之前设置的属性,也就是说,实现序列化接口的目的是为了在关闭之后仍然能够保持 Bean 对象的状态。

但是因为 Java 开发的桌面程序需要 JRE 运行时环境才能正常使用,而 Windows 系统并没有内置 JRE,这意味着用户如果需要使用你的程序,必须先安装好 JRE、配置好相关的环境变量才行,这显然是不太实际的事情。所以 Java 在桌面程序开发领域并不起色,那么有没有办法将 JavaBean 运用到 Java EE 开发中呢?毕竟 Java 差不多统治了服务器后端开发的半壁江山。聪明的开发者首先想到了 JSP,于是乎我们经常在 JSP 中看到如下代码:

稍微思考一下就知道,JSP 中使用的 JavaBean 其实不需要实现序列化接口,我们可以简单的将 JSP 中的 JavaBean 看作为一个纯粹的数据对象(这其实与后面的 POJO 有点类似)。

POJO 是什么?
POJO 是 Plain Old Java Object 的缩写,即 普通的 Java 对象。来看一下 wikipedia 对 POJO 的描述:

我们疑惑为什么人们不喜欢在他们的系统中使用普通的对象,我们得到的结论是:普通的对象缺少一个响亮的名字,因此我们给它们起了一个,并且取得了很好的效果。——Martin Fowler

理想情况下,POJO 是一个除了受到 Java 语言规范的限制外,不受其他任何约束的 Java 对象,即:

  1. POJO 不应该继承预先指定的类;
  2. POJO 不应该实现预先指定的接口;
  3. POJO 不应该包含预先指定的注解。

我喜欢将 POJO 理解为没有实现 java.io.Serializable 接口的 JavaBean,也就是说,POJO 是一个纯粹的“数据”对象(C 语言中的结构体)。而 JSP 中使用的 JavaBean 其实就可以看作为一个 POJO 对象,因为我们根本没有必要去实现 java.io.Serializable 接口。对于上面的 StudentBean 类,我们可以这样写一个对应的 StudentPojo 类:

Model1 与 Model2

Model1 和 Model2 是 Java Web 开发领域的两种架构模式,Model1 是 Web 早期开发中常用的一种模式,而 Model2 则是 Model1 的改良版,因为 Model2 完整的实现了 MVC 架构模式,所以我们也经常将 Model2 称为 Model2/MVC。

Model1 是什么?
Model1 的中心是 JSP 页面,每个 JSP 页面都是互相独立的,当用户请求一个 JSP 时,JSP 会收集好对应的请求参数,然后调用处理业务逻辑的 JavaBean,JavaBean 处理完之后,JSP 从中提取响应数据,然后渲染结果页面,最后返回给用户。

Java Web 之 Model1

  • Model1 的优点:实现简单,可以快速开发,适合小规模项目开发。
  • Model1 的缺点:JSP 中混杂着很多 Java 代码,不利于分工和维护。

Model2 是什么?
Model2 是基于 MVC 模式的一种架构模式,MVC 是指 Model/View/Controller,即模型、视图、控制器。Model 是应用的业务逻辑(通过 JavaBean、EJB 等组件实现),View 是应用的表示页面(一般是 JSP、HTML 页面),Controller 则用于控制应用的处理过程(一般是 Servlet、Filter),通过这种设计模型把 应用逻辑处理过程显示逻辑 分成不同的组件实现,使得这些组件可以进行交互和重用,从而弥补了 Model1 的不足。从结构上将,Model2 其实是在 Model1 的基础上又抽了一个控制层(Controller)。

Java Web 之 Model2

  • Model2 的优点:因为采用了 MVC 分层思想,更容易实现对大规模系统的开发和管理,职责划分清晰,可以做到分工明确。
  • Model2 的缺点:对于小型项目,使用 Model2 有点杀鸡用牛刀的感觉,大材小用,所以 Model2 不太适合用来做快速开发。

总结:Model1 和 Model2 最根本的区别是,Model1 是 JSP + JavaBean 开发模式,Model2 是 JSP + Servlet + JavaBean 开发模式,两者都是 MVC 模式的应用,但是应用程度不同。Model1 因为简单,所以适合小项目的快速开发,而 Model2 因为是完整的 MVC 思想的实现,所以更适合做中大型项目,人员的分工很明确,各干各的,互不影响,而且后期维护也容易。

Model1 的例子
以最常见的用户登录为例子,首先我们要创建一个登录页面,这个用 HTML 写个表单就行;然后是一个处理表单请求的 JSP 页面,它的逻辑很简单,如果用户名和密码没问题,则显示登录成功,否则显示登录失败;为了表示一个用户,我们需要创建一个 User 类(JavaBean)。结构如下:

login.html

login.jsp

UserBean.java

Model2 的例子
还是上面那个用户登录的例子,我们用 Model2/MVC 模式来重新实现它。首先,我们还是需要一个 login.html 登录表单页面;然后,我们需要写一个处理表单请求的 Servlet,这个 Servlet 内部会调用 UserBean 来验证用户是否合法,如果是合法用户,则将请求 forward 到登录成功的 jsp 页面,如果不是合法用户,则将请求 forward 到登录失败的 jsp 页面。结构如下:

login.html

LoginServlet.java

UserBean.java

login-success.jsp

login-failure.jsp

SSH 与 SSM 框架

  • SSH 三大框架(旧):Spring + Struts2 + Hibernate
  • SSM 三大框架(新):Spring + SpringMVC + MyBatis

MVC 框架
Struts2SpringMVC 都是 MVC 框架,MVC 的概念在 Model2 架构模式中提过了,它是一个分层思想,将 Web 开发分为 3 个不同的部分:业务逻辑(M)、显示逻辑(V)、流程控制(C)。这么做的目的是为了将 Web 开发中的不同关注点进行分离,M 只需要关心如何处理业务逻辑(Java 程序员、DBA 数据库管理员等),V 只需关心如何渲染页面(前端开发人员),C 只需关心如何处理用户的请求,然后根据需要调用不同的 M,最后将结果传递给对应的 V 即可(Java 后端程序员)。这样项目的每个部分都可以由最擅长该领域的开发人员来做,而不是一个开发人员同时充当前端、后端、DBA,这样太累,而且效率也低。同时,MVC 也使得项目更加容易维护,代码的可重用性也提高了,还降低了不同组件之间的耦合度。

Struts2 曾经是最流行的 MVC 框架,但是由于安全漏洞太多(万年漏洞王),现在越来越多的开发团队选择了 SpringMVC,因为 SpringMVC 是 Spring Framework 家族中的一个,相比较 Struts2,SpringMVC 更容易与 Spring 框架相契合,上手难度也低于 Struts2,最关键的是没有重大的安全漏洞。但由于各种历史原因,现在还是有很多老项目在使用 Struts2,所以学习一下(稍微了解就行)Struts2 框架还是有必要的。

ORM 框架
ORM 是 Object Relational Mapping 的缩写,即 对象关系映射HibernateMyBatis 都是所谓的 ORM 框架。那么 ORM 到底是什么呢?最简单的理解:ORM 就是用面向对象的方式来访问关系型数据库。没有 ORM 之前,在 Java 中如果需要访问数据库,必须通过 JDBC API 来完成,JDBC 复杂倒是不复杂,就是挺繁琐的,稍不注意就可能导致资源泄漏(比如忘记关闭数据库连接),而且 JDBC 并没有提供数据库连接池的 API,基本上所有事情都要自己做。但是有了 ORM 后,我们完全可以用 OO 的方式来访问数据库(ORM 框架会将数据表映射为一个 POJO 对象,这样我们就只需关心这个 POJO 对象,不用操心底层的数据库),而其他的什么数据库连接池、缓存功能,通通帮我们实现了,我们只管用就行,非常方便。所以我们也可以说,ORM 其实是对 JDBC API 的一种封装。

虽然 Hibernate 和 MyBatis 都是 ORM 框架,但是它们对 JDBC API 的封装程度其实是不一样的,一句话总结就是:Hibernate 是全自动 ORM 映射工具,而 MyBatis 则是半自动 ORM 映射工具。Hibernate 之所以称为“全自动”,是因为 Hibernate 完全可以通过对象关系模型来实现对数据库的操作(屏蔽了底层的 SQL 语句);而 MyBatis 仅有基本的字段映射,对象数据以及对象实际关系仍然需要通过手写 SQL 来实现和管理。也因为这个原因,在 SQL 语句优化上,MyBatis 要比 Hibernate 方便和简单许多。不过 Hibernate 的优点是完全屏蔽了底层数据库的细节差异,所以可移植性比 MyBatis 好很多,毕竟 MyBatis 需要手写 SQL,而不同数据库的 SQL 方言基本是不同的。

简单一句话总结:

  • MyBatis:小巧、方便、高效、简单、直接、半自动
  • Hibernate:强大、方便、高效、复杂、绕弯子、全自动

Spring 是什么
从官网文档看:

The Spring Framework is a lightweight solution and a potential one-stop-shop for building your enterprise-ready applications. However, Spring is modular, allowing you to use only those parts that you need, without having to bring in the rest. You can use the IoC container, with any web framework on top, but you can also use only theHibernate integration code or the JDBC abstraction layer. The Spring Framework supports declarative transaction management, remote access to your logic through RMI or web services, and various options for persisting your data. It offers a full-featured MVC framework, and enables you to integrate AOP transparently into your software.

Spring is designed to be non-intrusive, meaning that your domain logic code generally has no dependencies on the framework itself. In your integration layer (such as the data access layer), some dependencies on the data access technology and the Spring libraries will exist. However, it should be easy to isolate these dependencies from the rest of your code base.

一句话,Spring 是一个开发应用框架,什么样的框架呢,有这么几个标签:轻量级非侵入式一站式模块化,其目的是用于简化企业级应用程序的开发。我们知道应用程序是由一组相互协作的对象组成,所以开发一个应用除了要开发业务逻辑之外,最多的是关注如何使这些对象协作来完成所需功能,而且要 低耦合、高内聚。业务逻辑开发是不可避免的,那如果有个框架出来帮我们来创建对象及管理这些对象之间的依赖关系,我们只需要专心业务逻辑,是不是省心很多,同时这个叫 Spring 的干的又专业又稳定,何乐而不为呢。

从这里我们可以认为 Spring 是一个超级粘合平台,除了自己提供功能外,还提供粘合其他技术和框架的能力,从而使我们可以更自由的选择到底使用什么技术进行开发。

Spring 带来了什么
Spring 虽然不能帮我们写业务逻辑,却能帮助我们简化开发,有以下几点:

  • Spring IoC 能帮我们根据配置文件创建及组装对象之间的依赖关系。
  • Spring AOP 能帮助我们无耦合的实现日志记录,性能统计,权限控制。
  • Spring 提供与第三方数据访问框架(如 Hibernate、JPA)的无缝集成,而且自己也提供了一套 JDBC 访问模板,来方便数据库访问。
  • Spring 提供与第三方 Web 开发框架(如 Struts2、JSF)的无缝集成,而且自己也提供了一套 Spring MVC 框架,来方便 Web 层开发。
  • Spring 能方便的与 Java EE(如 Java Mail、任务调度)进行整合,与更多技术整合(比如缓存框架)。

Spring 有什么好处
在看 Spring 的好处之前,先来理解以下几个专业名词:

  • 应用程序:是能完成我们所需要功能的成品,比如购物网站、OA 系统、ERP 系统。
  • 框架:是能完成一定功能的半成品,比如我们可以使用框架进行购物网站开发;框架做一部分功能,我们自己做一部分功能,这样应用程序就创建出来了。而且框架规定了你在开发应用程序时的整体架构,提供了一些基础功能,还规定了类和对象的如何创建、如何协作等,从而简化我们开发,让我们专注于业务逻辑开发。
  • 非侵入式设计:从框架角度可以这样理解,无需继承框架提供的类,这种设计就可以看作是非侵入式设计,如果继承了这些框架类,就是侵入设计,如果以后想更换框架之前写过的代码几乎无法重用,如果非侵入式设计则之前写过的代码仍然可以继续使用。
  • 轻量级&重量级:轻量级是相对于重量级而言的,轻量级一般就是非入侵性的、所依赖的东西非常少、资源占用非常少、部署简单等等,其实就是比较容易使用,而重量级正好相反。
  • POJO:POJO(Plain Old Java Objects)是简单的 Java 对象,它可以包含业务逻辑或持久化逻辑,但不担当任何特殊角色且不继承或不实现任何其它 Java 框架的类或接口。
  • 容器:在日常生活中容器就是一种盛放东西的器具,从程序设计角度看就是装对象的的对象,因为存在放入、拿出等操作,所以容器还要管理对象的生命周期。
  • 控制反转:即 Inversion of Control,缩写为 IoC,就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。
  • Bean:一般指容器管理对象,在 Spring 中指 Spring IoC 容器管理对象。

那么用 Spring 框架到底有什么好处呢?好处如下:

  • 非常轻量级的容器:以集中的、自动化的方式进行应用程序对象创建和装配,负责对象创建和装配,管理对象生命周期,能组合成复杂的应用程序。Spring 容器是非侵入式的(不需要依赖任何 Spring 特定类),而且完全采用 POJOs 进行开发,使应用程序更容易测试、更容易管理。而且核心 JAR 包非常小,Spring 3.0.5 不到 1M,而且不需要依赖任何应用服务器,可以部署在任何环境(Java SE 或 Java EE)。
  • AOP:AOP 是 Aspect Oriented Programming 的缩写,意思是面向切面编程,提供从另一个角度来考虑程序结构以完善面向对象编程(相对于 OOP),即可以通过在编译期间、装载期间或运行期间实现在不修改源代码的情况下给程序动态添加功能的一种技术。通俗点说就是把可重用的功能提取出来,然后将这些通用功能在合适的时候织入到应用程序中;比如权限控制,日志记录,这些都是通用的功能,我们可以把它们提取出来,然后在程序执行的合适地方织入这些代码并执行它们,从而完成需要的功能并复用了这些功能。
  • 简单的数据库事务管理:在使用数据库的应用程序当中,自己管理数据库事务是一项很让人头疼的事,而且很容易出现错误,Spring 支持可插入的事务管理支持,而且无需 JavaEE 环境支持,通过 Spring 管理事务可以把我们从事务管理中解放出来来专注业务逻辑。
  • JDBC 抽象及 ORM 框架支持:Spring 使 JDBC 更加容易使用;提供 DAO(数据访问对象)支持,非常方便集成第三方 ORM 框架,比如 Hibernate 等;并且完全支持 Spring 事务和使用 Spring 提供的一致的异常体系。
  • 灵活的 Web 层支持:Spring 本身提供一套非常强大的 MVC 框架,而且可以非常容易的与第三方 MVC 框架集成,比如 Struts2 等。
  • 简化各种技术集成:提供对 Java Mail、任务调度、JMX、JMS、JNDI、EJB、动态语言、远程访问、Web Service 等的集成。

以上关于 Spring 的介绍均摘自 「Java学习」Spring 框架简介

总结:Spring 是一个 Java Web 一站式的集成(粘合)框架,Spring 的核心概念是 IoC(控制反转)和 AOP(面向切面编程)。

IoC 控制反转
IoC 全称:Inversion of Control,即控制反转。先来看看它的字面解释:当一个对象创建时,它所依赖的对象由外部传递给它,而非自己去创建所依赖的对象。也就是说:一个对象在如何获取它所依赖的对象这件事情上,控制权被反转了。这也就不难理解“控制反转”这个名字的由来了。

那么 IoC 有什么好处呢?我们先来看一个简单的例子:孩子吃东西。

首先我们定义一个 Eatable 接口,表示一个可以吃的东西:

然后,我们创建两个 Eatable 接口的实现类,如苹果和橘子:

创建一个 Child 类,表示孩子,这里我们假设它现在想吃苹果:

最后,我们创建一个 Main 类,运行我们的程序:

运行结果如下:

OK,没问题。那么,如果现在 Child 想吃 Orange 怎么办?必须修改 Child 类的代码:

运行结果如下:

也没问题。但是如果 Child 现在又想吃 Apple 了,你还得改回去(你是不是想打死它?!)

为了应付这个熊孩子,我们必须修改 Child 的代码,怎么改呢?使用上面提到的“控制反转”。
修改之前,Child 想吃什么就吃什么;修改之后那就由不得他了,只能吃粑粑麻麻给的 eatable。

然后,我们还需要改动一下 Main 类,如下:

运行结果如下:

现在,如果 Child 想换过一个口味,那么我们只需要修改 Main 类就行了。你又要问了,你这不是还要修改代码吗?是的,没错,我现在还是需要修改 Main.java 这个类的代码。但是,我们可以将需要吃什么东西的代码提取出来,放到配置文件中(或者使用 Java 注解,好的框架一般都是注解和配置文件搭配着用),然后在 Main 类中读取配置文件或者注解中的值(全限定类名),接着使用 Java 反射 API,new 一个可以吃的东西出来,最后传递给 Child 类的构造函数。这样如果我们要改变 Eatable 的实现类,就只需要修改配置文件或者注解了,一行代码都不用改。这就是 IoC 的好处。

上面的例子中,Child 吃的 eatable 并不是它自己创建的,而是由 Main(粑粑麻麻)创建并传递给它的,所以我们把这个 Main 称之为 IoC 容器。容器的意思很简单,它就是一个生产指定对象所依赖的对象的地方(装对象的地方,也就叫容器)。

而 Main 传递 eatable 给 Child 的方式是构造函数传参,传参也就是赋值,但是在这里我们有一个新的术语来描述这个赋值的过程:依赖注入(Dependency Injection,简称 DI)。也就是说,Child 所依赖的对象(Eatable)是由 IoC 容器(Main 类)注入的。注入的方式有很多种,常见的有两个:一是通过构造函数(如上);二是通过 setter 方法(如下)。

IoC(控制反转)和 DI(依赖注入)其实是看待同一件事情的两种不同角度,也可以说 DI 是 IoC 的一种实现方式。IoC 的关注点是反转对象获取其所依赖的对象的方式;而 DI 的关注点是如何将对象所依赖的对象注入进去。换句话说:IoC 是目的,DI 是手段,我们可以通过 DI 这个手段来达到 IoC 这个目的。但实现 IoC 除了 DI(依赖注入)之外,还有一种方式:DL(依赖查找,Dependency Lookup),依赖查找比依赖注入更加主动,依赖查找会在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key 等信息来确定获取对象的状态(其实就是调用工厂方法,获取指定对象的实例)。也就是说,DI 是被动的(IoC 容器将依赖亲自送到它手中),DL 是主动的(自己向 IoC 容器索要它依赖的对象)。

那么 Spring 是如何实现 IoC 的呢?其实也差不多:

  1. 读取注解或配置文件,拿到对象所依赖的对象的类名
  2. 使用 Java 反射 API,基于类名实例化对应的对象实例
  3. 通过构造函数、setter 等方法将依赖对象传递给指定对象

最后总结一下 IoC 的好处:资源不再由使用资源的双方管理,而是由不使用资源的第三方管理(IoC 容器),这样可以带来很多好处。第一,资源集中管理,实现资源的可配置和易管理。第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度。我们可以将 IoC 看作是工厂模式的一种升级,IoC 很好的体现了面向对象设计法则之一:好莱坞法则:“别找我们,我们找你”,总之,IoC 使得软件更加 高内聚,低耦合(严于律己,宽以待人)。

AOP 面向切面编程
AOP(Aspect Oriented Programming),即面向切面编程,是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP 引入了封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。但 OOP 只允许开发者定义纵向的关系,并不能定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它所在对象的核心功能毫无关系。对于其它类型的代码,如权限控制、异常处理和性能统计也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP 技术恰恰相反,它利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为 Aspect,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用”横切”技术,AOP 把软件系统分为两个部分:核心关注点横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,它们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

OOP 中的基本单元是 Class(类),而 AOP 中的基本单元是 Aspect(切面)。OOP 是从纵向的角度看待问题(继承、多态),而 AOP 则是从横向的角度看待问题(关注点分离),它们是分析抽象软件结构的两个不同的视角。AOP 并不是 OOP 的替代技术,它只是 OOP 的一种延续,OOP 和 AOP 是相辅相成的。AOP 是 SOC(Separation of Concerns,关注点分离)的一种体现,即把核心逻辑和其它逻辑的代码分离开来,这样我们就能够专心写某一个逻辑,无需关心其他的东西。既然有了分离,那么就有合并(专业术语叫做“织入”),织入的时机主要有 3 个:一是在编译期间进行静态织入(需要特殊的 Java 编译器);二是在类装载期间进行动态织入(需要特殊的 Java 类加载器);三是在实际使用之前使用 动态代理 来进行动态织入(Spring AOP 就是这种实现方式)。

提到动态代理,就不得不提代理模式,因为动态代理是代理模式的一种(与之相对的就是静态代理)。代理的概念:为某个对象提供一个代理,以控制对这个对象的访问。代理类和委托类有共同的父类或父接口,这样在任何使用委托类对象的地方都可以用代理对象替代。代理类负责请求的预处理、将请求分派给委托类处理、以及委托类执行完请求后的后续处理(是不是感觉和 Java Web 中的 Filter 很相似?预处理、后处理)。根据代理类生成时机的不同,可以将代理分为 静态代理(运行之前确定代理类与委托类的关系,代码写死)和 动态代理(运行期间确定代理类与委托类的关系,相对灵活)两种。

模拟需求:委托类需要处理一项耗时较长的任务,而客户需要知道委托类执行这个任务的具体耗时。
解决方法:在调用委托类方法之前以及之后,分别记录对应的时间,它们的时间差就是任务的耗时。

静态代理
所谓静态代理就是,代理类的字节码是在运行之前就生成好了的(比如我们先写好代理类的代码)。

定义委托类和代理类的共同接口,Task.java

然后定义委托类,用于处理具体的业务,RealTask.java

接着定义代理类,用来代理委托类的业务方法,ProxyTask.java

创建一个静态工厂类,因为客户无需知道它用的是代理类对象还是委托类对象,TaskFactory.java

最后,我们创建测试用的客户类,这里我就使用 Main 类来代替了,Main.java

运行结果如下:

静态代理的优点:

  • 业务类只需要关注业务逻辑本身,保证了业务类的重用性。这是所有代理模式的共有优点。

静态代理的缺点:

  • 代理类的一个接口只服务于一种类型的委托类,如果要代理的方法很多,势必要为每一种方法都进行代理,程序规模稍大时将无法胜任。
  • 如果后期要为相关接口增加一个方法,除了所有委托类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。

动态代理(JDK 动态代理)
动态代理与静态代理正好相反,其代理类的字节码是在运行期间动态生成的(通过反射等机制)。
与动态代理紧密相关的 API:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler

1、java.lang.reflect.Proxy,这是 JDK 动态代理机制生成的所有代理类的父类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。

方法 4 其实是一个便携方法,该方法内部会先调用方法 2 获取对应的代理类的 Class 对象,然后再通过 java.lang.Class 类的 getConstructor() 方法获取包含 InvocationHandler 调用处理器参数的构造函数(调用处理器作为一个参数传递给代理类的构造函数),最后通过 java.lang.Class 类的 newInstance() 方法创建该代理类的对象实例,并返回给调用者。

2、java.lang.reflect.InvocationHandler,调用处理器接口,它自定义了一个 invoke() 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。每次生成动态代理类对象时都要指定一个对应的调用处理器对象。

为了实现动态代理,我们需要先实现一个 InvocationHandler 调用处理器,TaskInvocationHandler.java

然后在刚才的静态工厂类中添加一个静态工厂方法,getInstanceByDynamicProxy()

然后,修改 Main 类,进行测试:

执行结果如下:

动态代理类的继承关系
JDK 动态代理类的继承关系

动态代理类实例的特点
每个代理类实例都会关联一个调用处理器对象,可以通过 Proxy 提供的静态方法 getInvocationHandler() 获得代理类实例的调用处理器对象。在代理类实例上调用其代理的接口中所声明的方法时,这些方法最终都会由调用处理器的 invoke() 方法执行,此外,值得注意的是,代理类的根类 java.lang.Object 中有 3 个方法也同样会被分派到调用处理器的 invoke() 方法执行,它们是 hashCode()equals()toString(),可能的原因有:一是因为这些方法为 public 且非 final 类型,能够被代理类重写;二是因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行。当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。

动态代理类关联的接口的特点
首先,要注意不能有重复的接口,以避免动态代理类代码生成时的编译错误。其次,这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败。再次,需被代理的所有非 public 的接口必须在同一个包中,否则代理类生成也会失败。最后,接口的数目不能超过 65535(估计也没这么吊的委托类),这是 JVM 设定的限制。

动态代理类异常处理方面的特点
从调用处理器接口声明的方法中可以看到理论上它能够抛出任何类型的异常,因为所有的异常都继承于 Throwable 接口,但事实是否如此呢?答案是否定的,原因是我们必须遵守一个继承原则:即子类重写父类或实现父接口的方法时,抛出的异常必须在原方法支持的异常列表之内(当然可以不抛出任何异常)。所以虽然调用处理器理论上讲能够,但实际上往往受限制,除非父接口中的方法支持抛 Throwable 异常。那么如果在 invoke() 方法中的确产生了接口方法声明中不支持的异常,那将如何呢?放心,Java 动态代理类已经为我们设计好了解决方法:它将会抛出 UndeclaredThrowableException 异常。这个异常是一个 RuntimeException 类型,所以不会引起编译错误。通过该异常的 getCause() 方法,还可以获得原来那个不受支持的异常对象,以便于错误诊断。

动态代理的优点和美中不足

优点
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke())。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。在本示例中看不出来,因为 invoke() 方法体内嵌入了具体的外围业务(记录任务处理前后时间并计算时间差),实际中可以类似 Spring AOP 那样配置外围业务。

美中不足
诚然,Proxy 已经设计得非常优美,但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持 interface 代理的桎梏,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫 Proxy。Java 的继承机制注定了这些动态代理类们无法实现对 class 的动态代理,原因是多继承在 Java 中本质上就行不通。

有很多条理由,人们可以否定对 class 代理的必要性,但是同样有一些理由,相信支持 class 动态代理会更美好。接口和类的划分,本就不是很明显,只是到了 Java 中才变得如此的细化。如果只从方法的声明及是否被定义来考量,有一种两者的混合体,它的名字叫抽象类。实现对抽象类的动态代理,相信也有其内在的价值。此外,还有一些历史遗留的类,它们将因为没有实现任何接口而从此与动态代理永世无缘。如此种种,不得不说是一个小小的遗憾。

以上关于静态代理和动态代理的大部分内容,均取自 Java 静态代理和动态代理

CGLIB 动态代理
因为 JDK 动态代理是基于接口的,但如果目标对象没有实现接口我们该如何处理呢?这时候就该 CGLIB(Code Generation Library)登场了。CGLIB 是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 是通过继承方式来实现动态代理的,弥补了 JDK 动态代理无法为没有实现接口的类提供代理的不足。

CGLIB 实现原理

  • CGLIB 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是 final 的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用 Java 反射的 JDK 动态代理要快。
  • CGLIB 底层:使用字节码处理框架 ASM,来转换字节码并生成新的类。不鼓励直接使用 ASM,因为它要求你必须对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉。
  • CGLIB 缺点:对于 final 方法,无法进行代理。

AOP 的好处,总结
OK,说了这么多关于动态代理的东西,现在回到 AOP 面向切面编程。Spring AOP 的实现原理我们上面已经进行了详细探讨,那么使用 AOP 究竟能给我们带来什么好处呢?具体的好处其实前文也提到了,一句话概括就是 AOP 能够让我们同一时间专心干一件事,每个人(在 Java 中就是一个类)都各司其职,灵活组合,达到一种可配置的、可插拔的程序结构。最后再引用一下某位大佬的话:AOP 是 SOC(关注点分离)的一种实现,关注点分离是人类的解决问题的一种思维方法,先将复杂的问题做合理的分解,然后再分别仔细研究问题的不同侧面(关注点,Aspect 切面)。带来的好处是使复杂的问题变的清晰,分而治之又为维护和扩展带了很大的便利。而且有点儿像四人帮的 SRP(单一职责原则),但它不仅仅在责任上分离,还在视角上分离。从最早的 C 语言 .c .h 文件的分离,到后来的 MVC 三层架构,乃至现在 IOC、AOP,无不体现了这种分离。

配置文件与注解

配置文件

  • 优点:容易编辑,配置比较集中,方便修改,在大业务量的系统里面,通过配置文件可以方便后人理解整个系统的架构。
  • 缺点:比较繁琐,类型不安全(对于 XML 配置文件来说,都是字符串),配置形态丑陋,配置文件过多的时候难以管理。

注解

  • 优点:方便、简洁,配置信息和 Java 代码放在一起,有助于增强程序的内聚性和可读性。
  • 缺点:注解往往分散在各个 class 文件中,不利于后期维护,而且注解本身也是一种耦合。

总结:需要经常改的东西建议使用 XML 配置文件,不需要经常改的东西建议使用 Java 注解。

类库和框架

库是工具箱
比如宜家买个便宜的架子,自己装,螺丝刀拧的手痛,就买了个电动螺丝刀,生产力立马跃迁好几倍。螺丝刀,电动螺丝刀,就是不同的库,虽然用法不同,API 不同,但想换就换了,改动不大。

框架是一套通用的解决方案
比如刚才买的架子,可以放书,可以放花,可以放 DVD。也可以个性化,有的地方装上门,装抽屉,但是,它还是个架子,不能在上面睡觉。
对应软件框架来说,通常针对某类典型问题,预先规定好在什么地方做什么事情,不好僭越。更换框架,也是麻烦,就像架子一样,之前装的门,抽屉都要丢掉,架子里东西也要迁移,重新安置。

架构是高度抽象的需求,是系统中的不变量
比如架子装好了,要有个房子放。买房子,房型图就是架构,这屋书房,那屋睡觉,二人世界,70 平搞定!装修,北欧风,中国风,都可以;打通墙,换马桶,也行,但承重墙不能动,70 平不会多,这就是架构。

开心的住了一年,怀孕了,三胞胎,噗噗噗,生了,婆婆岳母都来了,乌压压的一屋人,晚上客厅里睡倒一片,书房改胶囊旅店也塞不下了,怎么办?没办法!架构在那里,需求的发展超出了架构师的想象,只能认怂,有钱就买新房,换架构,没钱就受着,谁让你当初低估生殖力呢?

平台是所有可能做的事的集合
比如带着三胞胎搬进了新小区。想溜娃,去小区的滑梯;想运动,去小区的健身角;想游泳,物业说,不好意思,咱是中档小区,大哥你出门,打车,奥体。

在家里,小区就是平台,你能做的受限于小区的设施。同样,网页上,JS 操作 DOM,风生水起,但想要随意动硬盘里的文件,就困难了,因为它跑在浏览器里,浏览器是平台,有所限制。如果 JS 跑另一个平台上,操作文件也不是大问题。微信之于小程序,浏览器之于前端,JVM 之于 Java,皆是平台,既提供了服务,也限制了可能。

具体到实际操作,平台不需太费脑筋,没多少可选;架构比较考验人,设计的不好以后就被虐;框架看团队偏好,一般选熟悉的;库自己选顺手的即可。

以上描述均摘自 库,框架,架构,平台,有什么明确的区别? 中的第一个回答。

我用自己的语言来总结一下类库和框架的区别:
类库:我调用类库,主动权在我们自己;通常只有类库我们还不能直接完成我们的程序,因为它只是一个工具,程序的主体部分还是需要由我们自己写。
框架:框架调用我,主动权在框架本身;而框架则是将大部分代码都写好了,我们只要按照框架的约定,实现我们自己的业务逻辑部分,程序就能运行。