扩展技术我爱编程

Spring Ioc (反射) 精华一页纸

2017-04-11  本文已影响391人  轩居晨风

反射是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 来管理对象的生命周期。这种方式,对象间的依赖关系还是硬编码到了代码中。

上一篇下一篇

猜你喜欢

热点阅读