自从Spring问世以后,在中国大部分项目是基于Spring来搭建的,导致大部分程序员只会Spring Ioc来实现业务代码,却鲜有人了解为什么要使用Ioc。今天我们需要重新认识一下Ioc,深入了解为什么需要Ioc。
控制反转(inversion of control,简称ioc)是一种设计原则,顾名思义,它就是把原来代码里需要实现的对象创建,依赖,反转给容器来帮忙实现。
传统编程模型
MVC是经典的三层架构,大致代码如下:
public class UserServlet extends HttpServlet{
// 创建userService
private final UserService userService = new UserServiceImpl();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
userService.doSth();
}
}
public class UserServiceImpl implements UserService{
// 创建UserDao
private final UserDao userDao = new UserDaoImpl();
// doSth()
}
public class UserDaoImpl implements UserDao{
private DataSource datasource;
public UserDaoImpl(){
// 创建Jdbc Connection
}
// doSth()
}
三层架构的主要目的是为了确保单一职责,视图层主要为了页面展示,服务层则为了处理复杂的业务逻辑,持久层则为了与数据库进行交互。层次越低复用程度越高,比如一个DAO对象往往会被多个 Service 对象使用,一个 Service 对象往往也会被多个Servlet对象使用:
这种结构本身没有太大的问题,逻辑复用率得到了提高,但资源复用率则没有提高。实际上多个Servlet可以复用相同的Service实例,多个Service也可以复用相同的dao实例。针对这个问题,我们自然而然想到的是单例模式,虽说我们可以用单例模式来解决问题,这样每个类都需要编写单例,会存在大量重复的代码,其弊端不言而明。
即使我们使用了单例模式,也会有另外的问题,比如Dao层,UserDao这个接口,我可以实现一个UserDaoJdbcImpl类,通过访问数据库的方式来存储user对象,我也可以实现一个UserDaoRedisImpl类,可以通过访问redis的方式存储User对象。假设我有2个Service需要用到UserDaoJdbcImpl,有1个Service需要用UserDaoRedisImpl,我需要根据需求将具体的实现类显示得在Service里创建出来,未来换个需求可能就要修改代码。当然这个问题的本质就是面向抽象实现而非具体实现编程。
即使我们使用了面向接口编程,还会有另外的问题。还是以Dao层为例,UserDaoJdbcImpl实现类,通过访问数据库的方式来存储user对象,需要创建数据库连接。如果环境比较多,有开发环境,测试环境,uat环境,生产环境,每个环境的数据库连接是不一样的,如果需要根据环境切换数据库连接,通过硬代码的方式,即使通过配置文件能够写出来,也不是非常优雅的。这个问题的本质就是创建和管理配置。
我们稍微整理一下问题:
- 如何提高资源利用率,又能避免单例模式存在大量重复代码的问题。
- 面向接口实现而非具体实现。
- 创建和管理相关配置。
其实就是需要一个管理者,能够帮助我们管理对象。
实际上在Spring出来之前,也是有一些解决方案的,我们也一起来学习一下,看看Spring Ioc与其他方案的优缺点。
Ioc容器需要具备的功能或者特性
- 能够管理应用程序代码
- 启动速度快
- 无需任务特殊依赖,就可以创建对象
- 可以提高资源利用率,依赖低,基本对业务代码无侵入性,能够运行在多种环境中
- 性能开销低
- 可测试性高
- 更好的面向对象
Ioc实现方法
Ioc主要有两种实现方法:
- 依赖注入
- 依赖查找
| 类型 | 依赖处理 | 实现便利性 | 代码侵入性 | API依赖性 | 可读性 |
|---|---|---|---|---|---|
| 依赖查找 | 主动获取 | 相对繁琐 | 侵入业务逻辑 | 依赖容器API | 良好 |
| 依赖注入 | 被动获取 | 相对便利 | 低侵入性 | 不依赖容器API | 一般 |
依赖注入方式:
- 构造器注入:不可变对象,保证必须依赖项不为空
- setter方法注入:主要用于可选依赖项,且这些非必需依赖项在使用之前都必须进行非空检查,主要是因为不能想构造器注入那样,保证必须依赖项不为空。可以部分避免循环依赖的问题。
- 字段注入
传统Ioc实现
Java Beans
JavaBeans是Java中一种特殊的类,可以将多个对象封装到一个对象(bean)中。特点是可序列化,提供无参构造器(英语:Nullary constructor),提供getter方法和setter方法访问对象的属性。名称中的“Bean”是用于Java的可重用软件组件的惯用叫法。
优点
- Bean可以控制它的属性、事件和方法是否暴露给其他程序。
- Bean可以接收来自其他对象的事件,也可以产生事件给其他对象。
- 有软件可用来配置Bean。
- Bean的属性可以被序列化,以供日后重用。
规范如下:
- 有一个public的无参数构造函数。
- 属性可以透过get、set、is(可替代get,用在布尔型属性上)方法或遵循特定命名规则的其他方法访问。
- 可序列化。
代码示例:
public class StudentsBean implements java.io.Serializable{
private String firstName = null;
private String lastName = null;
private int age = 0;
public StudentsBean() {
}
public String getFirstName(){
return firstName;
}
public String getLastName(){
return lastName;
}
public int getAge(){
return age;
}
public void setFirstName(String firstName){
this.firstName = firstName;
}
public void setLastName(String lastName){
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}
UI JavaBean访问方式:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<title>get 和 set 属性实例</title>
</head>
<body>
<jsp:useBean id="students"
class="com.runoob.StudentsBean">
<jsp:setProperty name="students" property="firstName"
value="小强"/>
<jsp:setProperty name="students" property="lastName"
value="王"/>
<jsp:setProperty name="students" property="age"
value="10"/>
</jsp:useBean>
<p>学生名字:
<jsp:getProperty name="students" property="firstName"/>
</p>
<p>学生姓氏:
<jsp:getProperty name="students" property="lastName"/>
</p>
<p>学生年龄:
<jsp:getProperty name="students" property="age"/>
</p>
</body>
</html>
通过内省Introspector方式访问
public class User implements Serializable {
private int id;
private String username;
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public static void main(String[] args) throws Exception{
BeanInfo beanInfo = Introspector.getBeanInfo(User.class);
BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor();
MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
for (MethodDescriptor methodDescriptor: methodDescriptors){
}
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
User user = new User();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
if("class".equals(propertyDescriptor.getName())){
continue;
}
Method readMethod = propertyDescriptor.getReadMethod();
Method writeMethod = propertyDescriptor.getWriteMethod();
String name = writeMethod.getName();
if(name.equals("setUsername")){
writeMethod.invoke(user, "shawn");
}
}
System.out.println(user.getUsername());
}
}
上述代码可以参考javax.el.BeanELResolver。
Java Spi
在JavaSE中有一个服务提供发现的机制,Service Provider Interface。例如java.sql.Driver接口,不同的数据库厂商有不同的实现方式,Java会通过SPI机制,找到对应的实现类。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。
我们已java.sql.Driver接口,MySQL驱动实现为例,看看SPI是怎么实现的。
com.mysql.cj.jdbc.Driver实现了Driver接口,然后在classpath目录下,建立了目录META-INF/services。
然后在该目录下,创建一个文件名为java.sql.Driver的文件,其文件名就是接口的全限定名,文件内容为com.mysql.cj.jdbc.Driver,为MySQL的驱动全限定名。
最后应该如何发现MySQL Driver呢?查看DriverManager类型,我本地JDK的版本为11,其源码如下:
private static void ensureDriversInitialized() {
if (driversInitialized) {
return;
}
synchronized (lockForInitDrivers) {
// ...省略部分代码
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
});
// 。。。省略部分代码
}
}
JDK中查找服务的实现的工具类是:java.util.ServiceLoader。我们发现ServiceLoader实现了Iterable接口,也就是说ServiceLoader实际上也是一个迭代器,作用可以根据接口类型依赖查找该接口所有的实现类。
JNDI
Java 命名和目录接口 (Java Naming and Directory Interface, JNDI) 是一种应用编程接口 (application programming interface, API),用于访问不同类型的命名和目录服务。Java EE 组件通过调用 JNDI 查找方法,根据名称来进行依赖查找。
这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),公共对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。
每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索指定的对象,而该对象可能存储在RMI、LDAP、CORBA等等。
下表列出了用于 Application Server 所使用的 J2EE 资源的 JNDI 查找及其关联的引用。
| JNDI 查找名称 | 关联的引用 |
|---|---|
| java:comp/env | 应用程序环境项 |
| java:comp/env/jdbc | JDBC 数据源资源管理器连接工厂 |
| java:comp/env/ejb | EJB 引用 |
| java:comp/UserTransaction | UserTransaction 引用 |
| java:comp/env/mail | JavaMail 会话连接工厂 |
| java:comp/env/url | URL 连接工厂 |
| java:comp/env/jms | JMS 连接工厂和目的地 |
| java:comp/ORB | 应用程序组件之间共享的 ORB 实例 |
传统的Jdbc编程如下:
class.forName("") //加载驱动
Connection conn = DriverManager.getConnection(url, username, password);
// conn相关操作
conn.close();
即使有了Java SPI,省去了class.forName()这一行代码,剩下来的代码还是需要的。
目前代码存在的问题如下:
- 如果连接的数据库发生了变化或者username, password发生了变化,就需要重新修改代码然后打包,比较繁琐
- 如果mysql数据库切换成了oralce数据库,也需要重新打包。
- 目前还只是一个连接,怎么使用连接池,连接池参数怎么调整。
实际上这些都可以通过配置文件来解决,程序员只需要关注配置,然后让程序引用配置即可。所以我们也可以把JNDI看成是一个配置文件,主要配置资源。
我们以tomcat为例,看看tomcat如何实现jndi的。
- 在src/main/webapp/META-INF目录下创建一个context.xml文件,如果没有META-INF目录,则先创建该目录
- context.xml内容如下:
<Context>
<!-- maxTotal: Maximum number of database connections in pool. Make sure you
configure your mysqld max_connections large enough to handle
all of your db connections. Set to -1 for no limit.
-->
<!-- maxIdle: Maximum number of idle database connections to retain in pool.
Set to -1 for no limit. See also the DBCP 2 documentation on this
and the minEvictableIdleTimeMillis configuration parameter.
-->
<!-- maxWaitMillis: Maximum time to wait for a database connection to become available
in ms, in this example 10 seconds. An Exception is thrown if
this timeout is exceeded. Set to -1 to wait indefinitely.
-->
<!-- username and password: MySQL username and password for database connections -->
<!-- driverClassName: Class name for the old mm.mysql JDBC driver is
org.gjt.mm.mysql.Driver - we recommend using Connector/J though.
Class name for the official MySQL Connector/J driver is com.mysql.jdbc.Driver.
-->
<!-- url: The JDBC connection url for connecting to your MySQL database.
-->
<Resource name="jdbc/TestDB" auth="Container"
type="javax.sql.DataSource"
maxTotal="100"
maxIdle="30"
maxWaitMillis="10000"
username="root"
password="123456"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/demo"/>
</Context>
- 在WEB-INF/web.xml文件内添加
<resource-ref>
<description>DB Connection</description>
<res-ref-name>jdbc/TestDB</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
- 然后可以通过InitialContext类进行访问
InitialContext context = new InitialContext();
Context envContext = (Context)context.lookup("java:comp/env");
DataSource ds = (DataSource)envContext.lookup("jdbc/TestDB");
Connection conn = ds.getConnection();
//
参考文档:Apache Tomcat 9 (9.0.98) - JNDI Datasource How-To
Servlet
实际上Servlet设计的主要思想也是控制反转,但是容器只会控制Servlet对象。
Servlet容器会实例化和调用Servlet,那Servlet是怎么注册到Servlet容器中的呢?一般来说,我们是以Web应用程序的方式来部署Servlet的,而根据Servlet规范,Web应用程序有一定的目录结构,在这个目录下分别放置了Servlet的类文件、配置文件以及静态资源,Servlet容器通过读取配置文件,就能找到并加载Servlet。Web应用的目录结构大概是下面这样的:
| - MyWebApp
| - WEB-INF/web.xml -- 配置文件,用来配置Servlet等
| - WEB-INF/lib/ -- 存放Web应用所需各种JAR包
| - WEB-INF/classes/ -- 存放你的应用类,比如Servlet类
| - META-INF/ -- 目录存放工程的一些信息
Servlet规范里定义了ServletContext这个接口来对应一个Web应用。Web应用部署好后,Servlet容器在启动时会加载Web应用,并为每个Web应用创建唯一的ServletContext对象。你可以把ServletContext看成是一个全局对象,一个Web应用可能有多个Servlet,这些Servlet可以通过全局的ServletContext来共享数据,这些数据包括Web应用的初始化参数、Web应用目录下的文件资源等。由于ServletContext持有所有Servlet实例,你还可以通过它来实现Servlet请求的转发。
Ioc框架
除了Spring,还有Google Guice(github.com/google/guic…