Spring 学习笔记(一) 初识 Spring

2018-12-10  本文已影响14人  Theodore的技术站

第一章 Spring 之旅

简化 Java 开发

Spring是为了解决企业级应用开发的复杂性而创建的,使用 Spring 可以让简单的 JavaBean 实现之前只有 EJB 才能完成的事情。但 Spring 不仅仅局限于服务器端开发,任何 Java 应用都能在简单性、可测试性和松耦合等方面从 Spring 中获益。

为了降低Java开发的复杂性,Spring采取了以下4种关键策略:
-基于POJO的轻量级和最小侵入性编程;
-通过依赖注入和面向接口实现松耦合;
-基于切面和惯例进行声明式编程;
-通过切面和模板减少样板式代码。

Spring竭力避免因自身的API而弄乱你的应用代码。Spring不会强迫你实现Spring规范的接口或继承Spring规范的类,相反,在基于Spring构建的应用中,它的类通常没有任何痕迹表明你使用了Spring。最坏的场景是,一个类或许会使用Spring注解,但它依旧是POJO。

package com.habuma.spring;
public class HelloWorldBean{
    public String sayHello(){
        return "Hello World";
    }   
}

程序清单1.1 Spring不会在HelloWorldBean上有任何不合理的要求

依赖注入
直接看代码吧:

package com.springination.knights;
public class DamselRescuingKnight implements Knight{
    private RescueDamselQuest quest;
    public DamselRescuingKnight(){
        this.quest = new RescueDamselQuest();//紧耦合
    }
    
    public void embarkOnQuest(){
        quest.embark();
    }
}

可以看到这个骑士自行创建了拯救公主的对象,因此极大地限制了他的能力,如果他要去杀一条龙,或者去干掉坏人,就不行了。

通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系,如图1.1所示,依赖关系将被自动注入到需要它们的对象当中去。


image.png

看下面这个例子:

package com.springination.knights;
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实现。
程序清单1.4 为了测试BraveKnight,需要注入一个mock Quest

package com.springination.knights;
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.embarkOnQuest();
        verify(mockQuest,times(1)).embark();
    }
}

你可以使用mock框架Mockito去创建一个Quest接口的mock实现。通过这个mock对象,就可以创建一个新的BraveKnight实例,并通过构造器注入这个mock Quest。当调用embarkOnQuest()方法时,你可以要求Mockito框架验证Quest的mock实现的embark()方法仅仅被调用了一次。

现在BraveKnight类可以接受你传递给它的任意一种Quest的实现,但该怎样把特定的Query实现传给它呢?假设,希望BraveKnight所要进行探险任务是杀死一只怪龙,那么程序清单1.5中的SlayDragonQuest也许是挺合适的。程序清单1.5 SlayDragonQuest是要注入到BraveKnight中的Quest
实现

package com.springination.knights;
import java.io.PrintStream;

public class SlayDragonQuest implements Quest{
    private PrintStream stream;
    public SlayDragonQuest(PrintStream stream){
        this.stream = stream;
    }
    
    public void embark(){
        stream.println("Embarking on quest to slay the dragon!");
    }
}

我们可以看到,SlayDragonQuest实现了Quest接口,这样它就适合注入到BraveKnight中去了。与其他的Java入门样例有所不同,SlayDragonQuest没有使用System.out.println(),而是在构造方法中请求一个更为通用的PrintStream。这里最大的问题在于,我们该如何将SlayDragonQuest交给BraveKnight呢?又如何将PrintStream交给SlayDragonQuest呢?
创建应用组件之间协作的行为通常称为装配(wiring)。Spring有多种装配bean的方式,采用XML是很常见的一种装配方式。程序清单1.6展现了一个简单的Spring配置文件:knights.xml,该配置文件将BraveKnight、SlayDragonQuest和PrintStream装配到了一起。
程序清单1.6 使用Spring将SlayDragonQuest注入到BraveKnight中

<?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="knight" class="com.springframework.knight.BraveKnight">
        <constructor-arg reg="quest"/>
    </bean>
    <bean id="quest" class="com.springination.knights.SlayDragonQuest">
        <constructor-arg value="#{T(System).out}"/>
    </bean>
</beans>

对应 Java 实现:

package com.springination.knights.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.springinaction.Knights.BraveKnight;
import com.springinaction.Knights.Knight;
import com.springinaction.Knights.Quest;
import com.springinaction.Knights.SlayDragonQuest;

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

尽管BraveKnight依赖于Quest,但是它并不知道传递给它的是什么类型的Quest,也不知道这个Quest来自哪里。与之类似,SlayDragonQuest依赖于PrintStream,但是在编码时它并不需要知道这个PrintStream是什么样子的。只有Spring通过它的配置,能够了解这些组成部分是如何装配起来的。这样的话,就可以在不改变所依赖的类的情况下,修改依赖关系。

Spring通过应用上下文(Application Context)装载bean的定义并把它
们组装起来。程序清单1.8 KnightMain.java加载包含Knight的Spring上下文

package com.springinaction.knights;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class KnightMain{
    public static void main(String[] args) throws Exception{
        ClassPathXmlApplicationContext context = 
                new ClassPathXmlApplicationContext("META-INF/spring/knights.xml");
        Knight knight = context.getBean(Knight.class);
        knight.embarkOnQuest();
        context.close();
    }
}

这里的main()方法基于knights.xml文件创建了Spring应用上下文。随后它调用该应用上下文获取一个ID为knight的bean。得到Knight对象的引用后,只需简单调用embarkOnQuest()方法就可以执行所赋予的探险任务了。注意这个类完全不知道我们的英雄骑士接受哪种探险任务,而且完全没有意识到这是由BraveKnight来执行的。只有knights.xml文件知道哪个骑士执行哪种探险任务。

应用切面
DI能够让相互协作的软件组件保持松散耦合,而面向切面编程(aspect-oriented programming,AOP)允许你把遍布应用各处的功能分离出来形成可重用的组件。先来看下面这张图:

image.png

AOP能够使这些服务模块化,并以声明的方式将它们应用到它们需要影响的组件中去。所造成的结果就是这些组件会具有更高的内聚性并且会更加关注自身的业务,完全不需要了解涉及系统服务所带来复杂性。总之,AOP能够确保POJO的简单性。


image.png

AOP 应用:
下面程序展示了,吟游诗人吟唱其实的英勇事迹:

package com.springinaction.knights;

import java.io.PrintStream;

public class Minstrel{
    private PrintStream stream;
    
    public Minstrel(PrintStream stream) {
        this.stream = stream;
    }
    
    public void singBeforeQuest() {
        stream.println("Fa la la, the knight is so brave!");
    }
    
    public void singAfterQuest() {
        stream.println("Tee hee hee, the brave knight" + "did embark on a quest!");
    }
}
package com.springinaction.knights;

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() throws QuestException{
        minstrel.singBeforeQuest();
        quest.embark();
        minstrel.singAfterQuest();
    }
}

这样应该能达到效果,在骑士行动前后,诗人歌颂骑士。但是诗人应该是独立的个体,但是这个程序复杂化了,这个骑士居然去管理诗人,其实跟诗人应该是没有关系的。
所以下面要将诗人声明为一个切面:

<?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/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/beans
http://www/springframework.org/schema/beans/spring-beans.xsd">
    <bean id="knight" class="com.springination.knights.BraveKnight">
        <constructor-arg ref="quest"/>
    </bean>

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

    <bean id="minstrel" class="com.springination.knights.Minstrel"> //声明 Minstrel bean
        <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>

使用模板消除样板式代码
程序清单1.12 许多Java API,例如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 =?");
            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{
                    stmt.close();
                }catch(SQLException e){}
            }
            if(conn != null){
                try{
                    conn.close();
                }catch(SQLException e){}
            }
        }
        return null;
    }

这段代码很恶心,就是一个查询数据库的功能要写很多重复的代码,很多重复的异常处理。
使用Spring的JdbcTemplate(利用了 Java 5特性的JdbcTemplate实现)重写的getEmployeeById()方法仅仅关注于获取员工数据的核心逻辑,而不需要迎合JDBC API的需求。程序清单1.13展示了修订后的getEmployeeById()方法。

    public Employee getEmployeeById(long id){
        return jdbcTemplate.queryForObject("select id, firstname, lastname, salary" +
                "from employee where id=?",
                new RowMapper<Employee>(){
                    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);
    }

这样代码简单多了。

容纳你的 bean

Spring自带了多个容器实现,可以归为两种不同的类型。bean工厂(由org.springframework. beans.factory.eanFactory接口定义)是最简单的容器,提供基本的DI支持。应用上下文(由org.springframework.context.ApplicationContext接口定义)基于BeanFactory构建,并提供应用框架级别的服务,例如从属性文件解析文本信息以及发布应用事件给感兴趣的事件监听者。

Spring自带了多种类型的应用上下文。下面罗列的几个是你最有可能
遇到的。
-AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文。
-AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文。
-ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。
-FileSystemXmlapplicationcontext:从文件系统下的一个或多个XML配置文件中加载上下文定义。
-XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件中加载上下文定义。

Bean 的生命空间

image.png

1.Spring对bean进行实例化;
2.Spring将值和bean的引用注入到bean对应的属性中;
3.如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBean-Name()方法;
4.如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;
5.如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来;
6.如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessBeforeInitialization()方法;
7.如果bean实现了InitializingBean接口,Spring将调用它们的after-PropertiesSet()方法。类似地,如果bean使用initmethod声明了初始化方法,该方法也会被调用;
8.如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessAfterInitialization()方法;
9.此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
10.如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用。

上一篇 下一篇

猜你喜欢

热点阅读