反射是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 配置文件 <Resource name="jdbc/DefaultDS" 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 可以指定属性配置 <bean id="myProductManagerConstructor" class="chapter07.product.constructor.DefaultProductManager" init-method="initialize" destroy-method="shutdown"> 这样,在引入第三方组件时,可以不用依赖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、基本使用 设值和构造子 <bean id="xxx" class="xxx.xxx.XXX"> <constructor-arg value="xx" /> <constructor-arg ref="xxx" /> <property name="xxx" value="XXX"/> <property name="xxx" ref="XXX"/> </bean> 设值是通过 setter 方式注入;构造子按照顺序注入
II、集合装配 <list> <set> 子节点有 <value> <ref bean> <null> <list/set>(可嵌套) <map> 成员有 <entry key/key-ref value/value-ref> <props> 成员比较简单,就是 <prop key > <property name="proper"> <props> <prop key="AAA">value a</prop> <prop key="BBB">value b</prop> </props> </property>
III、工厂装配 <bean name="xxx" class="xxx" factory-method="xxx"></bean> -- 静态工厂 static <bean name="xxx" factory-bean="xxx" factory-method="xxx"></bean> -- 动态工厂 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 开启自动装配 <context:annotation-config/>
VI、注解 a、注入 @Autowired 实现 构造和设置注入 @Qualifier("guitar") 指定Bean注入,甚至可以自定义 注解 @Inject -- 使用JCP的Inject注解
b、bean定义 <context:component-scan base-package="xxx" /> @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 来管理对象的生命周期。这种方式,对象间的依赖关系还是硬编码到了代码中。