[原]Spring实战(1):初步了解Spring

贺含悦 18/04/12 23:28:40

Spring是什么啊 O_O
春天?emm…我现在所说的Spring是指一个开源框架

PS!!!从现在开始,一定要很熟悉的知道下面这些英文单词首字母组成的简称,后面不再做解释
EJB:Enterprise JavaBean ——企业级JavaBean
JDO:Java Data Object ——Java数据对象
POJO:Plain Old java Object ——简单老式Java对象
DI:Dependency Injection ——依赖注入
AOP:Aspect-Oriented Programming ——面向切面编程

Spring到底是什么?它用来干嘛呢?

Spring是为了解决企业级应用开发的复杂性而创建 的,使用Spring可以让简单的JavaBean来实现之前只有EJB和其他企业级Java规范才能完成的事情。相对于EJB来说,Spring提供了更加轻量级和简单的编程模型,它增强了POJO的功能。

但Spring不仅仅局限于服务器端开发,任何Java应用都能在简单性、可测试性和松耦合等方面从Spring中获益。

Spring的目标是全方位简化Java开发,它采取了以下4种关键策略:

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

Spring如何达到它的目的(简化Java开发)?

激发POJO的潜能

在基于Spring构建的应用中,它的类通常没有任何痕迹表明我们使用Spring。也就是说,Spring会竭力避免因自身的API而弄乱我们的应用代码,不会强迫我们实现Spring规范的接口或继承Spring规范的类。

尽管我们可能只是写了一个形式非常简单的POJO,Spring也会发挥它的作用让POJO一样可以具有魔力——通过DI来装配它们。也就是说,Spring可以激发POJO的潜能。

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

依赖注入(DI)

依赖注入已经演变成一项复杂的编程技巧或设计模式理念,可以帮助应用对象彼此之间保持松散耦合。应用DI可以让我们的代码变得异常简单并且更容易理解和测试。

耦合涉及到什么?
耦合具有两面性,一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且bug也是一个接一个;另一方面,一定程度的耦合又是必须的,完全没有耦合的代码什么也做不了。所以耦合性高的代码将会给开发者的测试和维护带来巨大的麻烦,因为它们往往牵一发而动全身。但为了完成有实际意义的功能,不同的类必须以适当的方式进行交互。

Spring就解决了这个关键的问题,它将对象之间的依赖关系转而用配置文件来管理。也就是它的依赖注入机制。

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

如果一个对象至通过接口(而不是具体实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
接下来我们以经典的骑士例子来说明依赖注入机制.

接下来我们来看一个骑士的例子:
编写一个类来实现“勇敢的骑士要去拯救被绑架的少女”。

1.1 一个骑士的接口

public interface Knight {
    public void embarkOnQuest();
    //embarkOnQuest()方法为骑士开始执行任务。
}

这个骑士现在要去拯救少女啦~
所以我们要实现拯救少女这个动作的类

1.2 拯救少女的动作

public class RescueDamselQuest {
    public void embark() {
        System.out.println("骑士去拯救少女啦!");
    }
}

再实现拯救少女的骑士

1.3 : 骑士去拯救少女

public class DamselRescuingKnight implements Knight {
    private RescueDamselQuest quest;
    public DamselRescuingKnight() {
        this.quest = new RescueDamselQuest();
    }
    public void embarkOnQuest() {
        quest.embark();
    }
}

我们可以从代码中看到,DamselRescuingKnight在它的构造函数里自行创建了RescueDamselQuest。这使得DamselRescuingKnight和RescueDamselQuest紧密耦合到了一起,也就是说如果一个少女需要救援,这个骑士能够召之而来,但是如果去做其他的活动,这个骑士就无能为力了,极大地限制了骑士执行探险的能力。

骑士应该是无所不能的,接着我们对上面的代码进行改动。
首先我们把骑士要执行的任务抽象为一个接口。

1.4 : 骑士要执行的任务接口Quest

public interface Quest {
    public void embark();
    //embark()方法代表开始执行任务
}

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

1.5:可以执行各种任务的勇敢骑士

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

这个勇敢的骑士不像上一个骑士,没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是DI的方式之一,即构造器注入。

为了验证程序1.5中Quest是否成功注入,我们使用使用mock测试来测试一下。

mock:在测试过程中,对于某些不容易获取的对象,用一个虚拟的对象来创建以便测试的方法。

1.6 为了测试BraveKnight,需要注入一个mock Quest

import static org.mockito.Mockito.*;
import org.junit.Test;

public class BraveKnightTest{

    @Test
    public void knightShouldEmbarkOnQuest() {
        Quest mockQuest = mock(Quest.class); 
        //创建mock Quest
        BraveKnight knight = new BraveKnight(mockQuest); 
        //注入mock Quest

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

这个勇敢的骑士现在不止可以拯救少女,还可以斩杀恶龙。

1.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呢?
这就使用到了依赖注入机制的装配(wiring)。

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

1.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.springinaction.knights.SlayDragonQuest">
        <constructor-arg value="#{T(System).out}" />
    </bean>

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

</beans>

PS!!!XML配置文件一定要放在resources目录下

BraveKnight和SlayDragonQuest被声明为Spring中的bean。就BraveKnight bean来讲,它在构造时传入了对SlayDragonQuest bean的引用,将其作为构造器参数。

SlayDragonQuest bean的声明中使用了Spring表达式语言(SpEL),将System.out传入到了SlayDragonQuest的构造器中。

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

Spring还支持使用Java来描述配置。

1.9 Spring提供了基于Java的配置,可作为XML的替代方案

@Configuration
public class KnightConfig {

    @Bean
    public Knight knight() {
        return new BraveKnight(quest());
    }

    @Bean
    public Queue quest() {
        return new SlayDragonQuest(System.out);
    }
}

不管使用XML还是Java配置,DI所带来的收益都是相同的。这样我们就可以在不改变所依赖的类的情况下,修改依赖关系。

现在已经声明了BraveKnight和SlayDragonQuest的关系,接下来只需要装载XML配置文件,并把应用启动起来。

Spring通过应用上下文(Application Context)装载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。

1.10 : KnightMain.java加载包含Knight的Spring上下文

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

这里的main()方法基于knights.xml文件创建了Spring应用上下文。随后它调用该应用上下文获取一个ID为knight的bean。得到Knight对象的引用后,只需简单调用embarkOnQuest()方法就可以执行所赋予的探险任务了。

通过这个例子,我们大概了解了什么是DI,以及它的作用。现在我们再关注Spring简化Java开发的下一个理念:基于切面进行声明式编程。

应用切面(AOP)

DI能让相互协作的软件组件保持松散耦合,而AOP允许我们把遍布应用各处的功能分离出来形成可重用的组件。

系统由许多不同的组件组成,每一个组件各负责一块特定功能。而AOP往往被定义为促使软件系统实现关注点分离的一项技术。

可以说AOP是OOP的补充和完善。
OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。


AOP技术恰恰相反,它利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为”Aspect”,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。


使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

这里写图片描述
我们可以把切面想象为覆盖在很多组件之上的一个外壳。应用是由那些实现各自业务功能的模块组成的。借助AOP,可以使用各种功能层去包裹核心业务层。这些层以声明的方式灵活地应用到系统中,核心应用甚至根本不知道它们的存在。

为了示范Spring中如何应用切面,让我们重新回到骑士的例子,并为它添加一个切面。
我们现在能够知道骑士所做的事情,是因为吟游诗人用诗歌记载了骑士的事迹并将其进行传唱。所以我们需要使用吟游诗人这个服务类来记载骑士的所有事迹。

1.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进行结合,让诗人传唱骑士的事迹。

1.12 修改BarveKnight,让它调用Minstrel方法

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

    public BraveKnight(Quest quest, Minstrel minstrel) {  //Quest被注入进来
        this.quest = quest;
        this.minstrell = minstrel;
    }
    public void embarkOnQuest() {
        minstrell.singBeforeQuest();
        quest.embark();
        minstrell.singAfterQuest();
    }
}

好像完成了?
但是再仔细想想,管理吟游诗人是骑士应该做的事情吗?并不是,传唱事迹是吟游诗人的职责。简单的代码开始变得复杂…
但利用AOP,将Minstrel抽象为一个切面,在Spring配置文件中声明它,就可以解决这个问题。

1.13 修改knight.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.springinaction.knights.SlayDragonQuest">
        <constructor-arg value="#{T(System).out}" />
    </bean>

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

    <bean id="minstrel" class="com.springinaction.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配置,就把Minstrel声明为一个Spring切面了。但Minstrel仍然是一个POJO,没有任何代码表明它要被作为一个切面使用。

使用模版消除样板式代码

我们通常为了实现通用的和简单的任务,会写一些样板式的代码。样板式代码的一个常见范例就是使用JDBC访问数据库查询数据。

1.14 查询数据库获得员工姓名和薪水

public Employee getEmployeeById(long id) {
    Connection conn = null;
    PreparedStatement stmt = null;
    ResultSet rs = null;
    try {
        conn = dataSource.getConnection();
        stmt = conn.preparedStatement(
                "select id, firstname, lastname, salary 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.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的样板代码。
Spring旨在通过模版封装来消除样板式代码,Spring的JdbcTemplate使得执行数据库操作时,可以避免传统的JDBC样板式代码。

1.15 : 使用Spring模板消除样板式代码

public Employee getEmployeeById(long id) {
    //SQL查询
    return jdbcTemplate.queryForObject(
            "select id, firstname, lastname, salary from employee where id=?",
            new RowMapper<Employee>() {
            //将结果匹配为对象
                public Employee mapRow(ResultSet rs, int 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"));
                    employee.setName(resultSet.getString("name"));
                    return employee;
                }
            }, id); //指定查询参数
}

这样使用起来就简单多了。

以上就是Spring通过面向POJO编程、DI、AOP和模板技术来简化Java开发的简单介绍。

作者:hxllhhy 发表于 2018/04/12 23:28:40 原文链接 https://blog.csdn.net/hxllhhy/article/details/79890280
阅读:65