框架学习笔记——Spring:(一)Spring核心

01. 简化Java开发

在Java近20年的历史中,它经历过很好的时代,也经历过饱受诟病的时代。尽管有很多粗糙的地方,如applet、企业级JavaBean(Enterprise JavaBean, EJB)、Java数据对象(Java Data Object, JDO)以及无数的日志框架,但是作为一个平台,Java的历史是丰富多彩的,有很多的企业级软件都是基于这个平台构建的。Spring是Java历史中很重要的组成部分。
在诞生之初,创建Spring的主要目的是用来替代更加重量级的企业级Java技术,尤其是EJB。相对于EJB来说,Spring提供了更加轻量级和简单的编程模型。它增强了简单老式Java对象(Plain Old Java object, POJO)的功能,使其具备了之前只有EJB和其他企业级Java规范才具有的功能。
Spring是为了解决企业级应用开发的复杂性而创建的,使用Spring可以让简单JavaBean实现之前只有EJB才能完成的事情。但Spring不仅仅局限于服务器端开发,任何Java应用都能在简单性、可测试性和松耦合等方面从Spring中获益。
为了降低Java开发的复杂性,Spring采取了以下4种关键策略:

  • 基于POJO的轻量级和最小侵入性编程;
  • 通过依赖注入和面向接口实现松耦合;
  • 基于切面和惯例进行声明式编程;
  • 通过切面和模板减少样板式代码。

Spring可以做很多事情,它为企业级开发提供给了丰富的功能,但是这些功能的底层都依赖于它的两个核心特性,也就是依赖注入(dependency injection, DI)和面向切面编程(aspect-oriented programming, AOP)。

  1. 控制反转IOC:
    IOC—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,IOC意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。如何理解好IOC呢?理解好IOC的关键是要明确“谁控制谁,控制什么,为何是反转(有反转就应该有正转了),哪些方面反转了”,那我们来深入分析一下:
    1.1 谁控制谁,控制什么: 传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IOC是有专门一个容器来创建这些对象,即由IOC容器来控制对象的创建;谁控制谁? 当然是IOC 容器控制了对象;控制什么? 那就是主要控制了外部资源获取(不只是对象包括比如文件等);
    1.2 为何是反转,哪些方面反转了: 有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转? 因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了? 依赖对象的获取被反转了;
    1.3 IOC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
  2. 依赖注入DI:
    组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。
    2.1 理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:
    谁依赖于谁: 当然是应用程序依赖于IOC容器;
    为什么需要依赖: 应用程序需要IOC容器来提供对象需要的外部资源;
    谁注入谁: 很明显是IOC容器注入应用程序某个对象,应用程序依赖的对象;
    注入了什么: 就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
    2.2 IOC和DI由什么关系呢? 其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IOC 而言,“依赖注入”明确描述了“被注入对象依赖IOC容器配置依赖对象”

不妨举个例子,请参考下面的HelloWorldBean类:

public class HelloWorldBean {
    public String sayHello() {
        return "Hello World";
    }
}

可以看到,这是一个简单普通的Java类——POJO。没有任何地方表明它是一个Spring组件。Spring的非侵入编程模型意味着这个类在Spring应用和非Spring应用中都可以发挥同样的作用。尽管形式看起来很简单,但POJO一样可以具有魔力。Spring赋予POJO魔力的方式之一就是通过DI来装配它们。让我们看看DI是如何帮助应用对象彼此之间保持松散耦合的:

/**
 *DamselRescuingKnight只能执行RescueDamselQuest探险任务
 */
public class DamselRescuingKnight implements Knight {
    private RescueDamselQuest quest;
    public DamselRescuingKnight() {
        this.quest=new RescueDamselQuest();//与RescueDamselQuest紧耦合
    }
    public void embarkOnQuest() {
        quest.embark();
    }
}

可以看到,DamselRescuingKnight在它的构造函数中自行创建了RescueDamselQuest。这使得DamselRescuingKnight紧密地和RescueDamselQuest耦合到了一起,因此极大地限制了这个骑士执行探险的能力。如果一个少女需要救援,这个骑士能够召之即来。但是如果一条恶龙需要杀掉,或者一个圆桌……额……需要滚起来,那么这个骑士就爱莫能助了。更糟糕的是,为这个DamselRescuingKnight编写单元测试将出奇地困难。在这样的一个测试中,你必须保证当骑士的embarkOnQuest()方法被调用的时候,探险的embark()方法也要被调用。但是没有一个简单明了的方式能够实现这一点。很遗憾,DamselRescuingKnight将无法进行测试。
通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定,对象无需自行创建或管理它们的依赖关系:

public class BraveKnight implements Knight {
    private Quest quest;
    public BraveKnight(Quest quest) {//Quest被注入进来
        this.quest=quest;
    }
    public void embarkOnQuest() {
        quest.embark();
    }
}

我们可以看到,不同于之前的DamselRescuingKnight,BraveKnight没有自行创建探
险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一,即构造器注入(constructor injection)。更重要的是,传入的探险类型是Quest,也就是所有探险任务都必须实现的一个接口。所以,BraveKnight能够响应RescueDamselQuest、 SlayDragonQuest、 MakeRoundTableRounderQuest等任意的Quest实现。
对依赖进行替换的一个最常用方法就是在测试的时候使用mock实现。我们无法充分地测试DamselRescuingKnight,因为它是紧耦合的;但是可以轻松地测试BraveKnight,只
需给它一个Quest的mock实现即可:

public class BraveKnightTest {
    @Test
    public void knightShouldEmbarkOnQuest() {
        Quest mockQuest=mock(Quest.class);//创建mock Quest
        BraveKnight knight=new BraveKnight(mockQuest);//注入mock Quest
        knight.embarkOnQuest();
        verify(mockQuest,times(1)).embark();
    }
}
  1. 面向切面编程AOP:
    DI能够让相互协作的软件组件保持松散耦合,而面向切面编程AOP允许你把遍布应用各处的功能分离出来形成可重用的组件。
    面向切面编程往往被定义为促使软件系统实现关注点的分离一项技术。系统由许多不同的组件组成,每一个组件各负责一块特定功能。除了实现自身核心的功能之外,这些组件还经常承担着额外的职责。诸如日志、事务管理和安全这样的系统服务经常融入到自身具有核心业务逻辑的组件中去,这些系统服务通常被称为横切关注点,因为它们会跨越系统的多个组件。
    如果将这些关注点分散到多个组件中去,你的代码将会带来双重的复杂性。实现系统关注点功能的代码将会重复出现在多个组件中。这意味着如果你要改变这些关注点的逻辑,必须修改各个模块中的相关实现。即使你把这些关注点抽象为一个独立的模块,其他模块只是调用它的方法,但方法的调用还是会重复出现在各个模块中。组件会因为那些与自身核心业务无关的代码而变得混乱。一个向地址簿增加地址条目的方法应该只关注如何添加地址,而不应该关注它是不是安全的或者是否需要支持事务;
  2. 使用模板消除样板式代码:
    你是否写过这样的代码,当编写的时候总会感觉以前曾经这么写过?我的朋友,这不是似曾相识。这是样板式的代码(boilerplate code)。通常为了实现通用的和简单的任务,你不得不一遍遍地重复编写这样的代码。遗憾的是,它们中的很多是因为使用Java API而导致的样板式代码。样板式代码的一个常见范例是使用JDBC访问数据库查询数据。举个例子,如果你曾经用过JDBC,那么你或许会写出类似下面的代码:
public Employee getEmployeeById(long id) {
    Connection conn=null;
    PreparedStatement stmt=null;
    ResultSet rs=null;
    try {
        conn=dataSource.getConnection();
        stmt=conn.prepareStatement("select id, firstname, lastname, salary from"+"employee where id=?");//查找员工
        rs=stmt.executeQuery();
        Employee employee=null;
        if(rs.next()) {
            employee=new Employee();//根据数据创建对象
            employee.setId(rs.getLong("id"));
            employee.setFirstName(rs.getString("firstname"));
            employee.setLastName(rs.getString("lastname"));
            employee.setSalary(rs.getBigDecimal("salary"));
        }
        return employee;
    } catch (SQLException e) {
    } finally {
        if(rs!=null) {//清理
            try {
                rs.close();
            } catch (SQLException e) {}
        }
        if(stmt!=null) {
            try {
                stmt.close();
            } catch (SQLException e) {}
        }
        if(conn!=null) {
            try {
                conn.close();
            } catch (SQLException e) {}
        }
    }
    return null;
}

正如你所看到的,这段JDBC代码查询数据库获得员工姓名和薪水。我打赌你很难把上面的代码逐行看完,这是因为少量查询员工的代码淹没在一堆JDBC的样板式代码中。首先你需要创建一个数据库连接,然后再创建一个语句对象,最后你才能进行查询。为了平息JDBC可能会出现的怒火,你必须捕捉SQLException,这是一个检查型异常,即使它抛出后你也做不了太多事情。
最后,毕竟该说的也说了,该做的也做了,你不得不清理战场,关闭数据库连接、语句和结果集。同样为了平息JDBC可能会出现的怒火,你依然要捕捉SQLException。
Spring旨在通过模板封装来消除样板式代码。Spring的JdbcTemplate使得执行数据库操作时,避免传统的JDBC样板代码成为了可能。

public Employee getEmployeeById(long id) {
    Connection conn=null;
    return jdbcTemplate.queryForObject("select id, firstname, lastname, salary from"+"employee where id=?",new RowMapper<Employee>() {//SQL查询
        public Employee mapRow(ResultSet rs, int rowNum) throws SQLException {//将结果匹配为对象
            Employee employee=new Employee();
            employee.setId(rs.getLong("id"));
            employee.setFirstName(rs.getString("firstname"));
            employee.setLastName(rs.getString("lastname"));
            employee.setSalary(rs.getBigDecimal("salary"));
            return employee;
        }
    },id);//指定参数查询
}

正如你所看到的,新版本的getEmployeeById()简单多了,而且仅仅关注于从数据库中
查询员工。模板的queryForObject()方法需要一个SQL查询语句,一个RowMapper对象(把数据映射为一个域对象),零个或多个查询参数。GetEmployeeById()方法再也看
不到以前的JDBC样板式代码了,它们全部被封装到了模板中。

02. 容纳Bean

在基于Spring的应用中,你的应用对象生存于Spring容器(container) 中。Spring容器负责创建对象装配它们配置它们管理它们的整个生命周期,从生存到死亡(在这里,可能就是new到finalize())。
容器是Spring框架的核心。Spring容器使用DI管理构成应用的组件,它会创建相互协作的组件之间的关联。毫无疑问,这些对象更简单干净,更易于理解,更易于重用并且更易于进行单元测试。
Spring容器并不是只有一个。Spring自带了多个容器实现,可以归为两种不同的类型。bean工厂(由org.springframework. beans. factory.eanFactory接口定义)是最简单的容器,提供基本的DI支持。应用上下文(由org.springframework.context.ApplicationContext接口定义)基于BeanFactory构建,并提供应用框架级别的服务,例如从属性文件解析文本信息以及发布应用事件给感兴趣的事件监听者。
虽然我们可以在bean工厂和应用上下文之间任选一种,但bean工厂对大多数应用来说往往太低级了,因此,应用上下文要比bean工厂更受欢迎。我们会把精力集中在应用上下文的使用上,不再浪费时间讨论bean工厂。

  1. 使用应用上下文:
    Spring自带了多种类型的应用上下文,下面罗列的几个是你最有可能遇到的:
    1.1 AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文;
    1.2 AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中
    加载Spring Web应用上下文;
    1.3 ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加
    载上下文定义,把应用上下文的定义文件作为类资源;
    1.4 FileSystemXmlapplicationcontext:从文件系统下的一个或多个XML配置文件
    中加载上下文定义;
    1.5 XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件中加载上
    下文定义;
  2. bean的生命周期:
    在传统的Java应用中,bean的生命周期很简单。使用Java关键字new进行bean实例化,然后该bean就可以使用了。一旦该bean不再被使用,则由Java自动进行垃圾回收。相比之下,Spring容器中的bean的生命周期就显得相对复杂多了:
    2.1 Spring对bean进行实例化;
    2.2 Spring将值和bean的引用注入到bean对应的属性中;
    2.3 如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBeanName()方法;
    2.4 如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;
    2.5 如果bean实现了ApplicationContextAware接口,Spring将调
    用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来;
    2.6 如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessBeforeInitialization()方法;
    2.7 如果bean实现了InitializingBean接口,Spring将调用它们的afterPropertiesSet()方法。类似地,如果bean使用init-method声明了初始化方法,该方法也会被调用;
    2.8 如果bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessAfterInitialization()方法;
    2.9 此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
    2.10 如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值