Spring实战-第1章Spring之旅

摘要

本章内容:
1.Spring的bean容器
2.介绍Spring的核心模块
3.更为强大的Spring生态系统
4.Spring的新功能

1.1 简化Java开发

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

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

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

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

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

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

1.1.2 依赖注入

任何一个有实际意义的应用(肯定比Hello World示例更复杂)都会由两个或者更多的类组成,这些类相互之间进行协作来完成特定的业务逻
辑。按照传统的做法,每个对象负责管理与自己相互协作的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码。

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

看下面这个例子:

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();
    }
}

BraveKnight没有自行创建探险任务,而是在构造的时候把探险任务作为构造器参数传入。这是依赖注入的方式之一,即构造器注入。

为了测试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。

创建应用组件之间协作的行为通常称为装配。Spring有多种装配bean的方式,采用XML是很常见的一种装配方式。

使用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>

Spring还支持使用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);
        }
    }
}

Spring通过应用上下文(Application Context)装载bean的定义并把它们组装起来。Spring应用上下文全权负责对象的创建和组装。Spring自带
了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置。

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();
    }
}

1.1.3 应用切面

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

先来看下面这张图:



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


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>

使用模板消除样板式代码
许多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的需求。

    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);
    }

1.2 容纳你的Bean

在基于Spring的应用中,你的应用对象生存于Spring容器中。Spring容器负责创建对象,装配它们,配置它们并管理它们的整个生命周期,从生存到死亡(在这里,可能就是new到finalize())。

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

Spring自带了多种类型的应用上下文。下面罗列的几个是你最有可能
遇到的。

1.AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加载Spring应用上下文。
2.AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中加载Spring Web应用上下文。
3.ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源。
4.FileSystemXmlapplicationcontext:从文件系统下的一个或多个XML配置文件中加载上下文定义。
5.XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件中加载上下文定义。

bean的生命周期

在传统的Java应用中,bean的生命周期很简单。使用Java关键字new进行bean实例化,然后该bean就可以使用了。一旦该bean不再被使用,则由Java自动进行垃圾回收。

相比之下,Spring容器中的bean的生命周期就显得相对复杂多了。


在bean准备就绪之前,bean工厂执行了若干启动步骤:

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声明了销毁方法,该方法也会被调用。

Spring模块

总结

Spring致力于简化企业级Java开发,促进代码的松散耦合。成功的关键在于依赖注入和AOP。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容