Spring是一个轻量级Java开发框架,最早由Rod Johnson创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。
是一个分层的Java SE/Java EE full-stack(一站式)轻量级开源框架,为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构,因此Java开发者可以专注于应用程序的开发。Spring最根本的使命是解决企业级应用开发的复杂性,即简化Java开发。Spring可以做很多事情,它为企业级开发提供给了丰富的功能,但是这些功能的底层都依赖于它的两个核心特性,也就是依赖注入(dependency injection,DI)和面向切面编程(aspect-oriented programming,AOP)。
为了降低Java开发的复杂性,Spring采取了以下4种关键策略:
Spring设计理念:在Java EE开发中,支持POJO和Java Bean开发方式,使应用面向接口开发,充分支持OO(面向对象)设计方法;Spring通过IoC容器实现对象耦合关系的管理,并实现依赖反转,将对象之间的依赖关系交给IoC容器,实现解耦;
Spring框架的核心:IoC容器和AOP模块。通过IoC容器管理POJO对象以及他们之间的耦合关系;通过AOP以动态非侵入的方式增强服务。IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。Spring中的IoC的实现原理就是工厂模式加反射机制。
IOC,就是控制反转,如上图,拿公司招聘岗位来举例:
假设一个公司有产品、研发、测试等岗位。如果是公司根据岗位要求,逐个安排人选,如图中向下的箭头,这是正向流程。如果反过来,不用公司来安排候选人,而是由第三方猎头来匹配岗位和候选人,然后进行推荐,如图中向上的箭头,这就是控制反转。
在Spring中,对象的属性是由对象自己创建的,就是正向流程;如果属性不是对象创建,而是由Spring来自动进行装配,就是控制反转。这里的DI也就是依赖注入,就是实现控制反转的方式。正向流程导致了对象于对象之间的高耦合,IOC可以解决对象耦合的问题,有利于功能的复用,能够使程序的结构变得非常灵活。
Spring进行IOC实现时使用的有两个概念:context上下文和bean。
如中间图所示,所有被Spring管理的、由Spring创建的、用于依赖注入的对象,就叫做一个bean。Spring创建并完成依赖注入后,所有bean统一放在一个叫做context的上下文中进行管理。
AOP就是面向切面编程。如右面的图,一般程序执行流程是从controller层调用service层、然后service层调用DAO层访问数据,最后在逐层返回结果。
这个是图中向下箭头所示的按程序执行顺序的纵向处理。但是,一个系统中会有多个不同的服务,例如用户服务、商品信息服务等等,每个服务的controller层都需要验证参数,都需要处理异常,如果按照上图中红色的部分,对不同服务的纵向处理流程进行横切,在每个切面上完成通用的功能,例如身份认证、验证参数、处理异常等等、这样就不用在每个服务中都写相同的逻辑了,这就是AOP思想解决的问题。
AOP以功能进行划分,对服务顺序执行流程中的不同位置进行横切,完成各服务共同需要实现的功能。
实现AOP的技术,主要分为两大类:
Spring AOP的实现原理其实很简单:AOP框架负责动态地生成AOP代理类,这个代理类的方法则由Advice和回调目标对象的方法所组成,并将该对象可作为目标对象使用。AOP代理包含了目标对象的全部方法,但AOP代理中的方法与目标对象的方法存在差异,AOP方法在特定切入点添加了增强处理,并回调了目标对象的方法。
Spring AOP使用动态代理技术在运行期织入增强代码。使用两种代理机制:基于JDK的动态代理(JDK本身只提供接口的代理)和基于CGlib的动态代理。静态代理是在编译时进行织入或类加载时进行织入,比如AspectJ。
IoC(控制反转:Inverse of Control)是Spring容器的核心,实现IOC的手段有依赖查找与依赖注入。依赖查找是主动或手动的依赖查找方式,通常需要依赖容器或标准 API 实现。而依赖注入则是手动或自动依赖绑定的方式,无需依赖特定的容器和 API。
传统程序设计中,我们需要使用某个对象的方法,需要先通过new创建一个该对象,我们这时是主动行为;而IoC是我们将创建对象的控制权交给IoC容器,这时是由容器帮忙创建及注入依赖对象,我们的程序被动的接受IoC容器创建的对象,控制权反转,所以叫控制反转。
由于IoC确实不够开门见山,所以提出了DI(依赖注入:Dependency Injection)的概念,即让第三方来实现注入,以移除我们类与需要使用的类之间的依赖关系。总的来说,IoC是目的,DI是手段,创建对象的过程往往意味着依赖的注入。我们为了实现IoC,让生成对象的方式由传统方式(new)反转过来,需要创建相关对象时由IoC容器帮我们注入(DI)。
简单的说,就是我们类里需要另一个类,只需要让Spring帮我们创建 ,这叫做控制反转;然后Spring帮我们将需要的对象设置到我们的类中,这叫做依赖注入。
创建一个测试用户类
public class User {
public void add(){
System.out.println("add.....");
}
}
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
//配置要创建的类
<bean id="user" class="com.spring.test.User"/>
</beans>
public class Test {
@Test
public void test(){
//加载配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
//获取对象
User user = (User) context.getBean("user");
System.out.println(user);
//调用方法
user.add();
}
}
在容器启动时,Spring会根据配置文件的描述信息,自动实例化Bean并完成依赖关系的装配,从容器中即可获得Bean实例,就可以直接使用。Spring为什么仅凭一个简单的配置文件,就能神奇的实例化并配置好程序使用的Bean呢?答案是通过 Java的反射技术。
从以上一个简单的例子也能看出,一个DI容器的核心功能一般有三个:配置解析、对象创建和对象生命周期管理(单例or原型)。
写业务代码时,我们的manager层总是用到dao层,以前我们总是在manager层new出dao对象,现在我们使用依赖注入的方式向manager层注入dao层。
// UserDao
public class UserDao {
public void add(){
System.out.println("dao.....");
}
}
// UserManager
public class UserManager {
//这里也可以用注解注入依赖
UserDao userdao;
public void setUserdao(UserDao userdao){
this.userdao=userdao;
}
public void add(){
System.out.println("service.......");
userdao.add();
}
}
// 配置文件
<bean id="userDao" class="com.spring.test.UserDao"></bean>
//这样在实例化manager的时候,同时装配了dao对象,实现了依赖注入
<bean id="userManager" class="com.spring.test.UserManager">
//ref为dao的id值
<property name="userdao" ref="userDao"></property>
</bean>
BeanFactory是一个类工厂,和传统的类工厂不同,传统的类工厂仅负责构造一个类或几个类的实例;而BeanFactory可以创建并管理各种类的对象,Spring称这些被创建和管理的Java对象为Bean。
BeanFactory是一个接口,Spring为BeanFactory提供了多种实现,最常用的就是XmlBeanFactory。其中,BeanFactory接口最主要的方法就是getBean(String beanName),该方法从容器中返回指定名称的Bean。此外,BeanFactory接口的功能可以通过实现它的接口进行扩展(比如ApplicationContext)。
看下面的示例:
//我们使用Spring配置文件为User类提供配置信息,然后通过BeanFactory装载配置文件,启动Spring IoC容器。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="user" class="com.spring.test.User"></bean>
</beans>
// 通过XmlBeanFactory启动Spring IoC容器
public class Test {
@Test
public void test(){
//获取配置文件
ResourcePatternResolver resolver=new PathMatchingResourcePatternResolver();
Resource rs=resolver.getResource("classpath:bean.xml");
//加载配置文件并启动IoC容器
BeanFactory beanFactory = new XmlBeanFactory(rs);
//从容器中获取Bean对象
User user=(User) beanFactory.getBean("user");
user.speak();
}
}
XmlBeanFactory装载Spring配置文件并启动IoC容器,通过BeanFactory启动IoC容器时,并不会立刻初始化配置文件中定义的Bean,初始化创建动作是在第一次获取bean时。
ApplicationContext由BeanFactory派生而来,提供了更多面向实际应用的功能。在BeanFactory中,很多功能需要用编程的方式来实现,而ApplicationContext中可以通过配置的方式来实现。ApplicationContext的主要实现类是ClassPathXmlApplicationContext和FileSystemXmlApplicationContext,前者默认从类路径加载配置文件,后者默认从文件系统中加载配置文件,如下所示:
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
ApplicationContext的初始化和BeanFactory初始化有一个重大的区别,BeanFactory初始化容器时并未初始化Bean,只有第一次访问Bean时才创建;而ApplicationContext则在初始化时就实例化所有的单实例的Bean。因此,ApplicationContext的初始化时间会稍长一点,占用内存空间也多一些,程序启动较慢。
BeanFactory是Spring框架的基础,面向Spring本身;ApplicationContext面向使用Spring框架的开发者,几乎所有的应用我们都直接使用ApplicationContext而非底层的BeanFactory。
前面提到的bean都是普通bean,Spring利用反射机制通过bean的class属性来实例化Bean。如果有的Bean属性特别多,我们就需要编写大量的配置信息,极其繁琐。Spring也提供了一个FactoryBean接口,我们可以通过实现该接口来返回特定的Bean,该接口定义了三个方法:
public class UserFactory implements FactoryBean<User> {
//返回对象
public User getObject() throws Exception {
User user=new User();
user.setAge(14);
user.setName("tom");
return user;
}
//返回bean的类型
public Class<User> getObjectType() {
return User.class;
}
public boolean isSingleton() {
// TODO Auto-generated method stub
return false;
}
}
public class Test {
@Test
public void test(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
User user=(User) applicationContext.getBean("userfactory");
user.say();
}
}
当我们配置文件中bean标签的class属性配置的类实现了FactoryBean接口时,通过getBean返回的就不是该类本身,而是getObject()方法所返回的对象,相当于getObject()方法代理了getBean(),这也是Spring使用此接口构造AOP的原因。在容器调用此方法的时候,返回一个代理,完成AOP代理的创建。
自己实现一个FactoryBean, 用来代理对象,对该对象的所有方法做一个拦截,在方法调用前后都输出一行log,示例如下。
public interface UserService {
/**
* 发送用户姓名到控制台
* @param userName 用户姓名
* @return 发送是否成功
*/
boolean sendUserName2Console(String userName);
}
public class UserServiceImpl implements UserService {
@Override
public boolean sendUserName2Console(String userName) {
System.out.println("Hello, " + userName);
return true;
}
}
public class UserFactoryBean implements FactoryBean<Object>, InitializingBean, DisposableBean {
//被代理对象实现的接口名(在使用Proxy时需要用到,用于决定生成的代理对象类型)
private String interfaceName;
//被代理的对象
private Object target;
// 生成的代理对象
private Object proxyObj;
@Override
public void destroy() throws Exception {
System.out.println("destroy...");
}
@Override
public Object getObject() throws Exception {
System.out.println("getObject...");
return proxyObj;
}
@Override
public Class<?> getObjectType() {
return proxyObj == null ? Object.class : proxyObj.getClass();
}
@Override
public boolean isSingleton() {
return false;
}
@Override
public void afterPropertiesSet() throws Exception {
proxyObj = Proxy.newProxyInstance(target.getClass().getClassLoader(),
new Class[] {Class.forName(interfaceName)}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("method Before");
Object result = method.invoke(target, args);
System.out.println("method After");
return result;
}
});
System.out.println("afterPropertiesSet");
}
public String getInterfaceName() {
return interfaceName;
}
public void setInterfaceName(String interfaceName) {
this.interfaceName = interfaceName;
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
}
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="com.spring.test.impl.UserServiceImpl"/>
<bean id="userFactoryBean" class="com.spring.test.UserFactoryBean">
<property name="interfaceName" value="com.spring.test.UserService"/>
<property name="target" ref="userService"/>
</bean>
</beans>
那么在代码中根据userFactoryBean来获取的Bean实际上是UserService类型的。
public class FactoryBeanTest {
@Test
public void test() throws Exception {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("test-factory-bean.xml");
UserService userService = applicationContext.getBean("userFactoryBean", UserService.class);
userService.sendUserName2Console("Jerry");
applicationContext.close();
}
}
JDK提供的访问资源的类并不能很好很方便的满足各种底层资源的访问需求。Spring设计了一个Resource接口,为应用提供了更强的访问底层资源的能力,该接口拥有对应不同资源类型的实现类。Resource接口是Spring资源访问策略的抽象,它本身并不提供任何资源访问实现,具体的资源访问由该接口的实现类完成——每个实现类代表一种资源访问策略。
这些Resource实现类,针对不同的的底层资源,提供了相应的资源访问逻辑,并提供便捷的包装,以利于客户端程序的资源访问。
为了访问不同类型的资源,必须使用相应的Resource实现类,这是比较麻烦的。Spring提供了一个强大的加载资源的机制,仅通过资源地址的特殊标识就可以加载相应的资源。首先,了解一下Spring支持哪些资源类型的地址前缀。
Spring定义了一套资源加载的接口。ResourceLoader接口仅有一个getResource(String location)的方法,可以根据资源地址加载文件资源。资源地址仅支持带资源类型前缀的地址,不支持Ant风格的资源路径表达式。ResourcePatternResolver扩展ResourceLoader接口,定义新的接口方法getResources(String locationPattern),该方法支持带资源类型前缀以及Ant风格的资源路径的表达式。PathMatchingResourcePatternResolver是Spring提供的标准实现类。
public class Test {
@Test
public void testResourceLoader() throws IOException {
ResourceLoader resourceLoader = new DefaultResourceLoader();
Resource res = resourceLoader.getResource("https://www.baidu.com/");
System.out.println(res instanceof UrlResource); // true
BufferedReader bf = new BufferedReader(new InputStreamReader(res.getInputStream()));
StringBuilder sb = new StringBuilder();
String temp;
while ((temp = bf.readLine())!= null) {
sb.append(temp);
}
System.out.println(sb.toString());
System.out.println("\n-----------------------------\n");
res = resourceLoader.getResource("classpath:antx.properties");
bf = new BufferedReader(new InputStreamReader(res.getInputStream()));
sb = new StringBuilder();
while ((temp = bf.readLine())!= null) {
sb.append(temp);
}
System.out.println(sb.toString());
System.out.println("\n-----------------------------\n");
res = resourceLoader.getResource("file:/Users/XXXX/Downloads/908653986.jpg");
bf = new BufferedReader(new InputStreamReader(res.getInputStream()));
sb = new StringBuilder();
while ((temp = bf.readLine())!= null) {
sb.append(temp);
}
System.out.println(sb.toString());
}
}
在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全的状态采用ThreadLocal进行处理,解决线程安全问题。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal,ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。要想了解Spring事务管理的底层技术,ThreadLocal是必须攻克的山头堡垒。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。
下面的实例能够体现Spring对有状态Bean的改造思路:
public class TopicDao {
//①一个非线程安全的变量,类的属性
private Connection conn;
public void addTopic(){
//②引用非线程安全变量
Statement stat = conn.createStatement();
…
}
}
由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造。
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
//①使用ThreadLocal保存Connection变量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){
//②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
//并将其保存到线程本地变量中。
if (connThreadLocal.get() == null) {
Connection conn = ConnectionManager.getConnection();
connThreadLocal.set(conn);
return conn;
}
//③直接返回线程本地变量
return connThreadLocal.get();
}
public void addTopic() {
//④从ThreadLocal中获取线程对应的
Statement stat = getConnection().createStatement();
}
}
不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否为null,如果为null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其他线程的Connection。因此,这个TopicDao就可以做到线程内共享。
当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在Dao只能做到本Dao的多个方法共享Connection时不发生线程安全问题,但无法和其他Dao共用同一个Connection,要做到同一事务多个Dao共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。但这个示例基本上说明了Spring对有状态类线程安全化的解决思路。