Spring学习笔记(一) --- Hello Spring

写了近一天的博客没有了!!!!! 我怎么不剁了我这双手!!!!!!

本系列博客为Spring In Action 这本书的学习笔记


Spring简介

Spring 是一个开源框架,是为了解决企业应用程序开发复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许您选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架。

这是网上给出的Spring框架的概念, 可能初次接触到Spring的小伙伴跟我刚开始一样, 看的是云里雾里, 不过没关系, 我们先来大概分析一下Spring的框架模块吧.

Spring框架由以下六部分组成(也有人将其分为七部分, 由于本文是基于Spring In Action所写, 所以与文中作者所述保持一致)

这里写图片描述

我们来逐一分析Spring的这六个模块.

1. Core Spring container(Spring核心容器)

容器是Spring框架最核心的部分, 它负责Spring应用中Bean的创建/配置和管理. 而在Spring容器中最核心的三个组件为: Beans, Context, Core.

Bean是包装过后的Object, Bean对Spring而言, 就像Object对OOP的意义一样, Spring甚至可以理解为是面向Bean的编程.
如果我们把Bean看作是一场演出里的演员, 那么Context就是这场演出的背景. 我们知道Bean里面包装的是Object, Object里面必然有数据, 如何给这些数据提供生存环境就是Context要解决的问题.
Context就是一个Bean关系的集合, 这个关系集合又叫Ioc容器, 一旦建立起这个Ioc容器, Spring就可以为你工作了.
Core就是发现/建立和维护每个Bean之间的关系所需要的一系列工具, 从这个角度来看, 称之为Util更容易理解.

2. AOP(面向切面编程)

在AOP模块中, Spring对面向切面编程提供了丰富的支持. 这个模块是Spring应用系统中开发切面的基础. AOP的意义就是帮助应用对象解耦, 借助于AOP, 可以将遍布应用中的关注点(比如事务与安全)从他们所应用的对象中解耦出来.

3. Data access & integration(数据访问与集成)

Spring的JDBC和DAO模块封装了大量样板代码, 这样可以使得数据库代码变得简洁, 也可以更专注于我们的业务, 还可以避免数据库资源释放失败而引起的问题.

另外, Spring AOP为数据访问提供了事务管理服务, 同时Spring还与ORM进行了集成, 如Hibernate, MyBatis等.

4. Web and remoting(Web和远程调用)

该模块提供了SpringMVC框架给Web应用, 还提供了多种构建和其它应用交互的远程调用方案.

SpringMVC框架在Web层提升了应用的松耦合水平.

5. Instrumentation

该模块提供了为JVM添加代理(agent)的功能, 具体来讲, 就是为Tomcat提供了一个织入代理, 能够为Tomcat传递类文件, 就像这些类文件是被类加载器加载的一样.

该模块不太常用.

6. Test(测试)

为了使得开发者能够很方便的进行自测, Spring提供了测试模块以致力于Spring应用的测试.

通过该模块, Spring为使用Servlet/JNDI等编写单元测试提供了一系列的mock对象实现.


上面的框架介绍是不是看得很头大, 其实Spring框架说白了就是一句话: 简化Java开发.

那么我们来看一看它是如何简化Java开发的呢?

Spring简化Java开发

为了降低Java开发的复杂性, Spring采取了以下4种关键策略:

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

下面我们来逐一解释这4种策略.

1. 基于POJO的轻量级和最小侵入性编程

这是什么意思呢?

Spring尽量避免因自身的API而弄乱你自身的代码, Spring不会强迫你实现Spring规范的接口或继承Spring规范的类.

可能你的类里面使用了Spring的注解, 但是去掉注解, 它仍然是一个普通的Java类.

2. 依赖注入

这就是Spring的重头戏啦! 为什么Spring如此流行? 使用Spring给我们带来的好处又在哪呢?

通常来讲, 一个应用是由很多个类的对象相互交织相互依赖构成的, 耦合性高的代码将会给开发者的测试和维护带来巨大的麻烦, 因为它们往往牵一发而动全身. Spring就解决了这个关键的问题, 它将对象之间的依赖关系转而用配置文件来管理, 也就是它的依赖注入机制.

依赖注入机制就是面向接口编程,对象通过接口来表明依赖关系. 这就是依赖注入机制带来的最大的收益 — 松耦合.

如果一个对象至通过接口(而不是具体实现或初始化过程)来表明依赖关系, 那么这种依赖就能够在对象本身毫不知情的情况下, 用不同的具体实现进行替换.

接下来我们以经典的骑士例子来说明依赖注入机制.


编写一个类来实现”勇敢的骑士要去拯救被绑架的美丽的公主”.

首先我们先给一个骑士的接口, 以便可以实例化各种各样的骑士.

程序1 : 骑士的接口

//这是一个骑士的接口, 其中的embarkOnQuest()方法为骑士开始执行任务.
public interface Knigths {
    public void embarkOnQuest();
}

然后我们来实现”骑士拯救公主”.
先来实现拯救公主这个动作的类.

程序2 : 拯救公主的动作

public class RescuesPrincessQuest {
    public void embark() {
        System.out.println("骑士去解救公主啦!");
    }
}

再实现拯救公主的骑士.

程序3 : 骑士去拯救公主

//这是一个去拯救公主的骑士
public class KnightOfRescuesPrincess implements Knigths{
    private RescuesPrincessQuest quest;

    //拯救公主的骑士只能拯救公主
    public KnightOfRescuesPrincess(){
        this.quest = new RescuesPrincessQuest(); //(1)
    }

    //调用任务的开始方法, 代表骑士开始解救公主了
    public void embarkOnQuest() {
        quest.embark();
    }
}

通过程序3中的(1)处可以看出, 骑士类的构造函数里实例化了拯救公主类的对象, “骑士”对象与”拯救公主动作”对象是紧密耦合的. 换句话来说, 这个骑士只能拯救公主, 不能再执行其它任务, 比如斩杀恶龙/比如保护国王. 这样是不合理的, 勇敢的骑士是无所不能的. 接着我们对上面的代码进行改动.

首先我们把骑士要执行的任务抽象为一个接口.

程序4 : 骑士要执行的任务接口Quest

//这是任务接口, 表明骑士可以实现的任务. embark()方法代表开始执行任务
public interface Quest {
    public void embark();
}

然后我们让拯救公主这个任务继承Quest接口

程序5 : 可以执行各种任务的骑士

public class BraveKnight implements Knigths {
    private Quest quest;

    //Quest接口被注入进来
    public BraveKnight(Quest quest){
        this.quest = quest;
    }

    public void embarkOnQuest() {
        quest.embark();
    }
}

为了验证程序5中Quest是否成功注入, 我们使用使用mock测试来测试一下. 关于mock测试戳这里.

程序6 : 为了测试BraveKnight, 需要注入一个mock Quest.

//这是关于BraveKnight的mock测试, 注入一个mock Quest判断上述依赖注入有没有成功
public class BraveKnightTest {
    @Test
    public void knightShouldEmbarkOnQuest(){
        //创建mock Quest
        Quest mockQuest = mock(Quest.class);
        //注入mock Quest
        BraveKnight knight = new BraveKnight(mockQuest);
        knight.embarkOnQuest();

        //times(1): 在上述条件下, 验证embark()是否只被调用了一次
        verify(mockQuest,times(1)).embark();
    }
}

既然我们有了一个可以执行各种任务的骑士了, 那么现在有一条恶龙需要斩杀, 就让我们的骑士去斩杀恶龙吧!

程序7 : 斩杀恶龙的任务

public class SlayDragonQuest implements Quest {
    //这里并没有直接指定输出的格式, 输出格式由用户在构造方法中决定
    private PrintStream stream;

    public SlayDragonQuest(PrintStream stream){
        this.stream = stream;
    }

    public void embark() {
        stream.println("骑士开始斩杀恶龙了!!");
    }
}

我们可以看到SlayDragonQuest实现了Quest接口, 这样它就适合注入到BraveKnight中了. 可是, 我们要怎样将SlayDragonQuest交给BraveKnight呢?
这就使用到了依赖注入机制的装配.

创建应用组件之间协作的行为称为装配. Spring有多种装配Bean的方式, 比如使用XML文件进行装配, 或者基于Java进行装配. 接下来我们来装配SlayDragonQuest.

(1) 使用XML文件将SlayDragonQuest注入到BraveKnight中

程序8 : knight.xml 将SlayDragonQuest/BraveKnight/PrintStream装配在一起

<?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">

    <!--创建SlayDragonQuest-->
    <bean id="quest" class="com.Knight.SlayDragonQuest">
        <constructor-arg value="#{T(System).out}" />
    </bean>

    <!--注入Quest bean-->
    <bean id="knight" class="com.Knight.BraveKnight">
        <constructor-arg ref="quest"/>
    </bean>

</beans>

在上述代码中, 关于<constructor-arg value=”#{T(System).out}” />的解释:

  • 在SpEL中, 使用T()运算符会调用类作用域的方法和常量. 例如, 在SpEL中使用Java的Math类, 我们可以像下面的示例这样使用T()运算符:
    T(java.lang.Math)
    T()运算符的结果会返回一个java.lang.Math类对象.

在knight.xml文件中, BraveKnight和SlayDragonQuest被声明为Spring中的Bean. 就BraveKnight来讲, 它在构造时传入了对SlayDragonQuest bean的引用, 将其作为构造器参数.
同时SlayDragonQuest bean的声明使用了SpEL(Spring 表达式语言), 将System.out传入SlayDragonQuest的构造器中.

这只是一种装配方法, 我们还可以使用基于Java的装配方案来代替XML.

(2) 基于Java的装配

程序9 : KnightConfig.java 基于Java进行装配

@Configuration
public class KnightConfig {
    @Bean
    public Quest quest(){
        return new SlayDragonQuest(System.out);
    }

    @Bean
    public Knigths knigths(){
        return new BraveKnight(quest());
    }
}

不管使用XML还是基于Java进行装配, 效果都是一样的. 现在已经声明了BraveKnight和SlayDragonQuest的关系, 现在我们只需要装载XML配置文件, 将应用启动起来.

Spring通过应用上下文装载Bean的定义并把它们组装起来. Spring应用上下文全权负责对象的创建和组装.

Spring提供了多种application context,可列举如下:

  • AnnotationConfigApplicationContext——从Java配置文件中加载应用上下文
  • AnnotationConfigWebApplicationContext——从Java配置文件中加载Spring web应用上下文
  • ClassPathXmlApplicationContext——从classpath(resources目录)下加载XML格式的应用上下文定义文件
  • FileSystemXmlApplicationContext——从指定文件系统目录下加载XML格式的应用上下文定义文件
  • XmlWebApplicationContext——从classpath(resources目录)下加载XML格式的Spring web应用上下文

现在假设我们加载的为XML配置文件, 那么应该使用ClassPathXmlApplicationContext来加载knight.xml.

程序10 : KnightMain.java加载包含Knight的Spring上下文

public class KnightMain {
    public static void main(String[] args){
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext("classpath*:knight.xml");
        //获取Knight Bean
        Knight knight = (Knight) context.getBean("knight");
        //使用knight
        knight.embarkOnQuest();
        context.close();
    }
}

在运行程序10的时候一直抛异常BeanDefinitionStoreException和NoSuchBeanDefinitionException, 不是提示找不到XML文件就是提示找不到Bean, 后来仔细观察才发现是XML配置文件放的地方不对, 一定要放在resources目录下!!!

AOP 面向切面编程

依赖注入能够让相互协作的软件组件保持松散耦合, 而面向切面编程能将遍布应用各处的功能分离出来形成可重用的组件.

我们可以这样理解面向切面编程: 面向对象编程(OOP)的思想主要处理的是对象从上到下的关系, 那么AOP处理的就是一种从左到右的关系.比如说, 我们现在有好几种业务流程从上到下, 但是每种流程都包含了部分的日志处理, 那么我们就可以将日志处理看作是一个切面, 一个横切所有流程的切面, 因为在每个流程的执行过程中, 遇到需要进行日志记录的部分都会转去进行日志处理, 这就是一个切面.

我们可以把切面想象为覆盖在很多组件之上的一个外壳.

这里写图片描述

为了示范在Spring中如何使用切面, 让我们重新回到骑士的例子, 为它添加一个切面.

我们现在知道骑士的事迹, 多亏了中世纪吟游诗人的传唱, 所以现在我们添加一个吟游诗人类.

程序11 : 吟游诗人类

//这是一个吟游诗人类, 来吟唱骑士的事迹, 两个方法分别在骑士执行任务前后吟唱
public class Minstrel {
    private PrintStream stream;

    public Minstrel(PrintStream stream){
        this.stream = stream;
    }

    public void singBeforeQuest(){
        stream.println("快来看啊!多么勇敢的骑士要去执行任务啦!");
    }

    public void singAfterQuest(){
        stream.println("快来看啊!勇敢的骑士执行任务回来啦!");
    }
}

接下来让我们将BraveKnight和Minstrel进行结合, 让诗人传诵骑士的英勇事迹.

程序12 : BarveKnight调用Minstrel方法

public class BraveKnight implements Knight {
    private Quest quest;
    private Minstrel minstrel;

    public BraveKnight(Quest quest, Minstrel minstrel){
        this.quest = quest;
        this.minstrel = minstrel;
    }

    public void embarkOnQuest() {
        minstrel.singBeforeQuest();
        quest.embark();
        minstrel.singAfterQuest();
    }
}

看起来很轻松就完成了, 但是, 仔细想想, 如果诗人不传诵他的事迹那他就不去执行任务了吗? 如果骑士执行了任务而诗人不想去传诵呢? 管理吟游诗人应该是骑士做的事情吗? 似乎不太对阿.

简单的代码因为加入吟游诗人类而变得复杂, 那么切面就派上用场了!

我们将Minstrel声明为一个切面.

程序13 : 在XML配置文件里面添加切面的声明

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--创建SlayDragonQuest-->
    <bean id="quest" class="com.Knights.SlayDragonQuest">
        <constructor-arg value="#{T(System).out}" />
    </bean>

    <!--注入Quest bean-->
    <bean id="knight" class="com.Knights.BraveKnight">
        <constructor-arg ref="quest"/>
    </bean>

    <bean id="minstrel" class="com.Knights.Minstrel">
        <constructor-arg value="#{T(System).out}" />
    </bean>

    <aop:config>
        <aop:aspect ref="minstrel">
            <!--定义切点, 即定义从哪里切入-->
            <aop:pointcut id="embark" expression="execution(* *.embarkOnQuest(..))" />
            <!--声明前置通知, 在切入点之前执行的方法-->
            <aop:before pointcut-ref="embark" method="singBeforeQuest" />
            <!--声明后置通知, 在切入点之后执行的方法-->
            <aop:after pointcut-ref="embark" method="singAfterQuest" />
        </aop:aspect>
    </aop:config>

</beans>

经过XML配置, 在Spring上下文中, Minstel实际上已经变成了一个切面了, 但是Minstel本身仍然是一个普通的Java类, 我们只不过使用了依赖注入的方式将Minstel注入到BraveKnight中了, 而BraveKnight本身并不需要知道Minstrel的存在.

这也验证了前面两种策略, 基于POJO的轻量级和最小侵入性编程和依赖注入机制.

使用模板消除样板式代码

关于使用模版消除样板式代码, 最直观的例子就是调用JDBC代码.

在我们使用JDBC连接数据库进行数据库操作的时候, 有大量重复的样板式代码, Spring的模板就是用来消除这种样板式代码的.

关于这部分内容, 我从网上拉下来了两段代码, 分别为普通JDBC调用代码和Spring模板式JDBC调用代码, 两者对比来说明Spring是如何消除样板式代码的.

程序14 : 原始JDBC调用代码

public Employee getEmployeeById(long id) {
    Connection conn = null;
    PreparedStatement stmt = null;
    ResultSet rs = null;
    try {
        conn = dataSource.getConnection();
        stmt = conn.prepareStatement("select id, name from employee where id=?");
        stmt.setLong(1, id);
        rs = stmt.executeQuery();
        Employee employee = null;
        if (rs.next()) {
            employee = new Employee();
            employee.setId(rs.getLong("id"));
            employee.setName(rs.getString("name"));
        }
        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样板式代码中.

那么如何消除这种样板式代码呢?

程序15 : 使用Spring模板消除样板式代码

public Employee getEmployeeById(long id) {
    //SQL查询
    return jdbcTemplate.queryForObject(
            "select id, name from employee where id=?",
            new RowMapper<Employee>() {
            //将结果匹配为对象
                public Employee mapRow(ResultSet resultSet, int i) throws SQLException {
                    Employee employee = new Employee();
                    employee.setId(resultSet.getLong("id"));
                    employee.setName(resultSet.getString("name"));
                    return employee;
                }
            });
}

这段代码采用回调函数实现, 我们以后再详细说.


千辛万苦终于写完了这篇博客!!!

现在我们了解了Spring的框架构成, 了解了它的优越性, 了解了它的基本思想. 虽然现在只是明白一个大概, 但是没关系, 其中的细节会在后面继续深入学习.

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页