Spring Ioc (反射) 精华一页纸
反射是Java实现模块化的一个非常基础的功能,通过加载类的字节码,然后动态的在内存中生成对象。也是深入Java 研究的第一个高级主题。关于加载器和字节码部分的内容,可以参见本博的 《java Class和加载机制精华一页纸》
Spring 框架基础的Ioc就是采用了反射的功能,实现了框架。
1、反射
I、反射操作经典步骤
一、获取 Class对象
a、最常用的就是 Class.forName(className)
b、如果知道类名字,直接通过类获取 String.class
c、如果已有一个对象 object.getClass
二、获取 Method对象
a、通过Class对象的getdeclaredMethods 获取所有方法
b、通过名字和参数类型列表,获取具体的方法getdeclaredMethod
三、实例化该Class的对象
Class.newInstance
四、调用方法
Method.invoke(newobject,new Object[]{parmalist}
II、反射的作用
反射是实现抽象的一个基础设施。单个应用内的模块化和解耦, 大家都比较熟悉, 比如 面向接口编程, 工厂模式等等。
iterface a = Factory.create;
在Factory 里面,我们是知道这个具体的实现类的。
但如果是应用模块之间呢, 不同人或者团队开发的, 商量好名字? 如果 名字改变后呢?
这样耦合性太强, 每次修改都会要带来代码重新修改和编译。
反射正是可以解决这个问题的工具。静态编译时, 并不需要知道具体的名字;在加载时, 通过传入名称参数, 获取到这个类
比如, 配置文件中配置了 具体实现类的名字, 只要在一个ClassPath下,就可以加载到具体的实现类。
Class c = Class.forName( param ); // 此处param可以是加载文件\其他应用传入的参数等等
iterface a = c.newInstance();
这个解耦套路,就是 传统的框架 套路
2、传统模块间解耦框架 - 依赖查找(DL)
依赖查找, 有个最经典的例子就是 JNDI , JavaEE 就是通过这个实现模块间对象的访问, 比如EJB, 下面是 tomcat下一个依赖查找的例子
I、context or server 配置文件
type="javax.sql.DataSource" auth="Container"
driverClassName="com.mysql.jdbc.Driver"
maxActive="4000"
maxIdle="60"
maxWait="15000"
url="jdbc:mysql://localhost:3306/mysql?useUnicode=true&characterEncoding=UTF-8"
username="root"
password="root"/>
II、代码中依赖查找
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/DefaultDS");
III、依赖查找的问题
依赖查找的关键问题是 对代码侵入性强, 带来的结果就是 模块集成、单元测试等等工作很难操作, 比如测试一个EJB调用的代码, 必须要有完整的 Web框架, 要配置好基础设施;而 这段代码只是要测试自己的逻辑和接口。
3、轻量级模块间解耦框架 - 依赖注入(DI)/控制反转(Ioc)
这两个概念自从Spring横空出世以后, 一直抄的非常火热。先解释一下两个名词
依赖注入:是从应用角度出发, 需要的对象是从 外面注入进来的, 属于被动接受对象;而不像传统的 依赖查找, 主动的去查找对象。
控制反转: 是从框架和容器的角度出发, 创建对象的工作, 由应用 让渡给 容器来完成, 对象间的依赖, 也都由容器完成。
依赖注入/控制反转,看起来很神奇, 其实,如果遵循 开发的几大原则, 面向接口、职责单一、接口隔离、开放封闭等(可以参照本博《设计模式 精华一页纸》),就会发现, 这是一种比较自然和优雅的架构设计。
传统的依赖查找,虽然解开了模块间的耦合,但他违背了职责单一的要求,对于 应用而言, 只需要了解和调用 接口中的方法, 而查找这个工作不应该放在应用中。所以,可以对查找这个过程进行封装。
Object o = Lookup.get(xxx);
-- 这里的 Lookup 封装了对象的查找过程
再进一步封装和解耦,查找对象的过程对应用彻底屏蔽隔离、在应用的代码中不再出现 查找的代码。要完成这个工作
a、 首先,查找获取的对象 要设置到 使用该对象的目标对象的应用代码中, 也就是所谓的 注入工作
b、 其次,要完成注入工作,要么 把目标对象的引用传递给框架, 要么目标对象本身就是框架创建的
c、 从解耦、隔离的角度看, 框架创建管理对象更符合要求。
框架管理对象的生命周期、提供对象的注入工作。
......
Spring Ioc 框架就是在这个基础上产生了。
4、Spring Ioc 框架
从上面的讨论, 可以了解, 对象都交由框架管理和构造, 所以、首先要有对象的管理容器;其次要有注入的接口,实现装配工作。
I、Bean 工厂/容器
某种角度上,Spring Ioc就是一个对象容器, 依赖注入这些只是提供的功能而已
public interface BeanFactory{
Object getBean(String name) throws BeansException
Object getBean(String name, Class requiredType)throws BeansException
boolean containsBean(String name)
boolean isSingleton(String name)throws NoSuchBeanDefinitionException
String[] getAliases(String name)throws NoSuchBeanDefinitionException
}
四级接口
BeanFactory作为最基础的接口,只提供了基本功能。
秉着 接口隔离的设计原则, 从BeanFactory开始的继承体系
二级接口 AutowireCapableBeanFactory ListableBeanFactory HierarchicalBeanFactory
分别对应 自动装配 Bean工厂 : 作用是不在Spring(主要是 ApplicationContext)中管理的对象, 如果在应用中用到了,Spring 无法注入,比如如果用到Tomcat已存在的对象,通过这个工厂把 这些对象引入并注入应用对象。
迭代Bean的 Bean工厂 : 提供对容器中的Bean访问功能
访问父接口的 Bean工厂 : 提供对父容器的访问功能
三级接口 ConfigurableBeanFactory :叠加配置功能(是否单例、范围、Bean依赖等等)
四级接口 ConfigurableListBeanFactory : 大合集功能的 接口, 继承之前面的接口
第一个默认的实现类 DefaultListableBeanFactory
一个比较有意思的问题: BeanFactory 和 FactoryBean 的区别?
这其实是两个完全不同层次的内容
BeanFactory 是 Ioc 容器的接口,管理Bean的核心接口
FactoryBean 则是 适配 第三方应用的一个接口, 提供了对第三方Bean的适配, 以便更好的集成到Spring中来
通过工厂Bean,应用不需要自己写适配类去装配其他应用
org.springframework.jndi.JndiObjectFactoryBean -- 提供JNDI查找的对象
org.springframework.orm.hibernate.LocalSessionFactoryBean -- 提供Hibernate SessionFactory
org.springframework.orm.jdo.LocalPersistenceManagerFactoryBean -- 提供JDO PersistenceManagerFactory的
org.springframework.aop.framework.ProxyFactoryBean -- 获取AOP的动态代理,实现AOP切面功能
org.springframework.transaction.interceptor.TransactionProxyFactoryBean -- 创建事务代理
org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean -- EJB业务接口
...
org.springframework.remoting.caucho.HessianProxyFactoryBean -- Hessian 远程协议的代理
org.springframework.remoting.caucho.BurlapProxyFactoryBean -- Burlap远程协议的代理
II、Bean的生命周期
容器托管了 Bean的创建, 所以容器需要负责管理 Bean的生命周期。
a、生命周期
实例化 -> 设值注入 -> 设置Bean ID -> 设置工厂 -> 设置上下文 -> 初始化(开始\初始化\结束)
正常构建Bean的这些过程, 不需要应用介入。如果有特殊需要介入的地方。Spring开放了二次接口。
如果需要在构造对象的时候提供 初始化和 销毁时 额外处理的能力
方法一:Spring提供了回调接口 BeanNameAware| ApplicationContextAware | BeanPostProcessor | InitializingBean | BeanPostProcessor | DisposableBean 等等对应不同的构造阶段二次接口
org.springframework.beans.factory.InitializingBean 该接口提供了对象构造后 afterProperiesSet() throws Exception 方法
org.springframework.beans.fatory.Disposable 该接口提供了一个对象销毁后调用的 destory() throws Exception 方法
@PostConstruct 注解 | @PreDestory 初始化调用和销毁调用
方法二:Spring 可以指定属性配置
这样,在引入第三方组件时,可以不用依赖Spring容器,第三方组件不需要修改代码,或者为Spring写适配器
也可以配置全局的 init-method/destroy-method 方法
方法三:Spring提供的Bean工厂接口,Bean实现该接口,可以获取Bean工厂的引用,可以获取对其他Bean的引用,实现生命周期干预
org.springframework.beans.factory.BeanFactoryAware 该接口提供一个 setBeanFactory(BeanFactory beanFactory) throws BeanException
如何选择?
如果希望解耦Spring 框架, 则可以使用 方法二 指定属性, 这样配置方法干预初始化和销毁;否则建议使用 注解
b、作用域
singlton - 一个Spring容器对应一个 对象
prototype - 每获取一个对象
request | session | gloabl - Web应用的作用域,每作用域一个对象
默认是 singlton 作用域
Web应用 DispatchServlet 会默认管理作用域,默认是request
c、创建和销毁
何时被创建?
默认是随容器启动创建
可以配置为 lazy-init="true" 获取时创建
何时被销毁?
singlton, 在容器关闭时销毁,平时一直驻留
prototype 销毁由应用管理
- 因为只有 singlton的 对象才会进入 Bean容器工厂的ConcurrentHashMap 缓存。这也是为什么 prototype 类型的对象, 无法进行销毁回调, 因为对象的控制权交给了应用
III、 应用上下文(org.springframework.context.ApplicationContext)
工厂接口提供Bean管理的核心功能, 如果要把这个工厂应用到具体项目中, 还需要很多基础设施, ApplicationContext就是这个功能合集。
a、继承了Bean工厂的功能,继承了 ListableBeanFactory | HierarchicalBeanFactory
b、提供资源的管理,主要是加载各种配置文件
c、国际化信息,主要是各种信息的国际化
通过委托给代理类 ResourceBundleMessageSource实现国际化
d、提供事件管理
继承自Java自带的事件分发
事件ApplicationEvent -> 继承 EventObject
监听者 ApplicationListener -> 继承 EventListener
提供了 ApplicationEventPublisher 事件管理器(分发)
具体参见本博 《java 观察者、事件机制 到Spring 事件分发机制》
e、lifycycle 生命周期管理
容器的生命周期管理提供 Lifecycle 接口, 提供给任何实现 该接口的Bean, 通过LifecycleProcessor 执行回调接口, 可以和容器的生命周期管理同步。
提供 start | stop | isRunning | onRefresh 等回调接口
常用的容器实现对象
ClassPathXmlApplicationContext
FileSystemXmlApplicationContext
XmlWebApplicationContext
5、Spring Ioc实例
I、基本使用 设值和构造子
undefinedundefined
undefinedundefined
设值是通过 setter 方式注入;构造子按照顺序注入
II、集合装配
子节点有 (可嵌套)
成员有
成员比较简单,就是
value a
value b
III、工厂装配
-- 静态工厂 static
-- 动态工厂 new
IV、SPEL表达式
#{xxx} 其实是一种占位替换表达式语法, 类似的有很多比如 Freemarker 的${}, angular JS的 {{}}, 支持对内存对象的访问和简单表达式操作, 这些语法也很类似
常量 #{xx} 等同于 xx 常量一般直接用的很少
引用 #{xxx.xxx} -- 属性 #{xxx.getxxx()} -- 方法
静态属性 #{T(ClassXXX).xxx}
各种运算(算术|逻辑|正则) #{1+2} #{a == b && b == c}
V、自动装配
byName -- 根据Bean名称和属性名称进行匹配 缺点是名称要一致,如果多个名称类似,就要避开重复
byType -- 根据Bean类型和属性类型进行装配 缺点是不能存在相同类型的多个bean(解决方法,首选bean,排除其他bean)
constructor -- 把具有相同类型的 type 构造到属性中
autodetect -- 首先尝试 constuctor 装配,失败采用 byType
指定单个Bean autowire="byName"
指定全局 default-autowire
开启自动装配
VI、注解
a、注入
@Autowired 实现 构造和设置注入
@Qualifier("guitar") 指定Bean注入,甚至可以自定义 注解
@Inject -- 使用JCP的Inject注解
b、bean定义
@Component -- 通用构造性注解
@Controller -- Spring MVC Controller
@Repository -- 标记为数据仓库
@Service -- 标记为服务
经过测试发现,XML手工配置的 注入,会覆盖 注解注入的值,应该Spring的顺序最后是手工
c、用Spring配置类来替代注入的工作
// 定义全局文件的 Beans 测试的时候发现,SpringConfig 类,Spring使用了CGlib(asm) 技术重新处理了字节码
// 主要原因是,Spring 并不是直接 调用方法返回对象的,比如如下 duke() 方法,Spring会拦截,针对单例的情况
// Spring 会从自己的上下文返回一个已经存在的对象
@Configuration
public class SpringConfig {
// 定义一个名为 duke 的Bean
@Bean
public Performer duke(){
return new Juggler();
}
@Bean
public Instrument guitar(){
return new Guitar(0);
}
@Bean
public Performer kenny(){
Instrumentalist kenny = new Instrumentalist();
kenny.setSong("aaa");
kenny.setInstrument(guitar());
return kenny;
}
}
使用 Java 配置的问题是,SpringConfig 就相当于facade 门面的实现,使用了 Spring的 Context 来管理对象的生命周期。这种方式,对象间的依赖关系还是硬编码到了代码中。