实战Spring简化 Java 开发

Spring 是一个开源框架,为了简化企业级应用开发而诞生的。

目录

1.1.1 激发POJO的潜能

1.1.2 依赖注入

DI功能是如何实现的

构造器注入(constructor injection)

装配(wiring)

Spring表达式语言(Spring Expression Language)

应用上下文(Application Context)

1.1.3 应用切面

面向切面编程(aspect-oriented programming,AOP)

AOP应用

1.1.4 使用模板消除样板式代码

什么叫样板式代码(bc)

JdbcTemplate实现

总结


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

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

1.1.1 激发POJO的潜能

先说一下什么是 pojo,全称是:

POJO(Plain Ordinary Java Object)简单的Java对象,实际就是普通JavaBeans,是为了避免和EJB混淆所创造的简称。

其中有一些属性及其getter setter方法的类,没有业务逻辑,有时可以作为VO(value -object)或dto(Data Transform Object)来使用.当然,如果你有一个简单的运算属性也是可以的,但不允许有业务方法,也不能携带有connection之类的方法。来源:百度百科

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

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

举例:

Spring的非侵入编程模型意味着这个类在Spring 应用和非Spring应用中都可以发挥同样的作用。Spring 尽可能的让开发者写的代码与其没有任何关联。

1.1.2 依赖注入

依赖注入这个词让人望而生畏,现在已经演变成一项复杂的编程技巧或设计模式理念。但事实证明,依赖注入并不像它听上去那么复杂。在项

目中应用DI,你会发现你的代码会变得异常简单并且更容易理解和测试。

DI功能是如何实现的

任何一个有实际意义的应用多个类组成,这些类相互之间进行协作来完成特定的业务逻辑。

按照传统的做法,每个对象负责管理与自己相互协作的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码。Spring 会管理这些业务代码,你只需要写业务即可。

举个例子
程 序 清 单 1.2 DamselRescuingKnight只 能 执 行 RescueDamselQuest探 险 任 务

package sia.knights;

public class DamselRescuingKnight implements Knight {

    private RescueDamselQuest quest;

    public DamselRescuingKnight() {
        // 与RescueDamselQuest 紧耦合
        this.quest = new RescueDamselQuest();
    }

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

}

可以看到,DamselRescuingKnight在它的构造函数中自行创建了Rescue DamselQuest。这使得DamselRescuingKnight紧密地 和RescueDamselQuest耦合到了一起,因此极大地限制了这个骑士执行探险的能力。如果一个少女需要救援,这个骑士能够召之即来。但 是如果一条恶龙需要杀掉,这位Knight 可能就干不了了,能力有限。

耦合具有两面性(two-headed beast)。

一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且典型地表现出“打地鼠”式的bug特性 (修复一个bug,将会出现一个或者更多新的bug)。

另一方面,一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。为了完成 有实际意义的功能,不同的类必须以适当的方式进行交互。

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

为了展示这一点,让我们看一看程序清单1.3中的BraveKnight,这个骑士不仅勇敢,而且能挑战任何形式的探险。

程序清单1.3 BraveKnight足够灵活可以接受任何赋予他的探险任务

package sia.knights;
  
/**
 * @author imenger
 */
public class BraveKnight implements Knight {

  private Quest quest;

  /**
   * 将 quest 注入
   *
   * @author imenger
   * @date 2020/12/8 6:44 下午
   * @param quest
   * @return
   */
  public BraveKnight(Quest quest) {
    this.quest = quest;
  }

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

}

构造器注入(constructor injection)

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

更重要的是,传入的探险类型是Quest,也就是所有探险任务都必须实现的一个接口。所以,BraveKnight能够响 应RescueDamselQuest、 SlayDragonQuest、 MakeRound TableRounderQuest等任意的Quest实现。

这里的要点是BraveKnight没有与任何特定的Quest实现发生耦合。对它来说,被要求挑战的探险任务只要实现了Quest接口,那么具体是 哪种类型的探险就无关紧要了。这就是DI所带来的最大收益——松耦合。如果一个对象只通过接口(而不是具体实现或初始化过程)来表明依 赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。说白了,更加体现代码的复用性。

将 Quest注 入 到 Knight中

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

程 序 清 单 1.5 SlayDragonQuest是 要 注 入 到 BraveKnight中 的 Quest实 现

package sia.knights;

import java.io.PrintStream;

public class SlayDragonQuest implements Quest {

  private PrintStream stream;

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

  @Override
  public void embark() {
    stream.println("Embarking on quest to slay the dragon!");
  }

}

我们可以看到,SlayDragonQuest实现了Quest接口,这样它就适合注入到BraveKnight中去了。SlayDragonQuest没有使用System.out.println(),而是在构造方法中请求一个更为通用的PrintStream。这里最大的问题在于,我们该如何将SlayDragonQuest交给BraveKnight呢?又如何将PrintStream交给SlayDragonQuest呢?

装配(wiring)

创建应用组件之间协作的行为通常称为装配(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">

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

    <!--bean的声明使用了Spring表达式语言-->
    <bean id="quest" class="sia.knights.SlayDragonQuest">
        <constructor-arg value="#{T(System).out}"/>
    </bean>

</beans>

Spring表达式语言(Spring Expression Language)

在这里,BraveKnight和SlayDragonQuest被声明为Spring中的bean。就BraveKnight bean来讲,它在构造时传入了 对SlayDragonQuest bean的引用,将其作为构造器参数。类似于,Java 声明,引用。同时,SlayDragonQuest bean的声明使用了Spring表达式语言(Spring Expression Language),将System.out(这是一个PrintStream)传入到了SlayDragonQuest的构造器中。

至于 SePL 不做过多叙述,移步 baidu

如果XML配置不符合你的喜好的话,Spring还支持使用Java来描述配置。比如,程序清单1.7展现了基于Java的配置,它的功能与程序清单1.6 相同。

程序清单1.7 Spring提供了基于Java的配置,可作为XML的替代方案

package sia.knights.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import sia.knights.BraveKnight;
import sia.knights.Knight;
import sia.knights.Quest;
import sia.knights.SlayDragonQuest;

@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通过它的配置,能够了解这些组成部分是如何装配起来的。这样的话,就可以在不改 变所依赖的类的情况下,修改依赖关系。

应用上下文(Application Context)

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

因为knights.xml中的bean是使用XML文件进行配置的,所以选择ClassPathXmlApplicationContext[1]作为应用上下文相对是比较合适 的。该类加载位于应用程序类路径下的一个或多个XML配置文件。程序清单1.8中的main()方法调 用ClassPathXmlApplicationContext加载knights.xml,并获得Knight对象的引用。

程 序 清 单 1.8 KnightMain.java加 载 包 含 Knight的 Spring上 下 文

package sia.knights;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class KnightMain {

    public static void main(String[] args) throws Exception {
        // 加载 Spring 上下文
        ClassPathXmlApplicationContext context =
                new ClassPathXmlApplicationContext(
                        "META-INF/spring/knight.xml");
        // 获取 knight bean
        Knight knight = context.getBean(Knight.class);
        knight.embarkOnQuest();
        context.close();
    }
}

执行结果:

> Task :KnightMain.main()
Embarking on quest to slay the dragon!

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

1.1.3 应用切面

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

面向切面编程(aspect-oriented programming,AOP)

面向切面编程,往往被定义为促使软件系统 实现关注点的分离的一项技术。

左边的业务对象与系统级服务结合得过于紧密。每个对象不但要知道它需要记日志、进行安全控制和参与事务,还 要亲自执行这些服务。

什么是横切关注点

系统由许多不同的组件组成,每个组件又负责一块特定的功能。除了实现自身核心功能外,还经常承担着比如日志,事务管理,安全等系统级服务,这些系统服务通常被称为和横切关注点。

如图1.3所示,我们可以把切面想象为覆盖在各个组件上的一个外壳,也就是业务模块(每个服务)。这时借助 AOP 可以使  各种功能层去包裹核心业务层。这些看不懂没事,下面一例子来表述一下。

AOP应用

假设我们需要使用吟游诗人这个服务类来记载骑士的所有事迹。

程序清单1.9 吟游诗人是中世纪的音乐记录器

package sia.knights;

import java.io.PrintStream;

/**
 * 吟游诗人 服务类
 *
 * @author imenger
 * @date 2021/1/19 3:45 下午
 */
public class Minstrel {

    private PrintStream stream;

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

    /**
     * 执行任务之前
     *
     * @param
     * @return void
     * @author imenger
     * @date 2021/1/19 3:49 下午
     */
    public void singBeforeQuest() {
        stream.println("啦啦啦,骑士真勇敢!");
    }

    /**
     * 完成任务之后
     *
     * @param
     * @return void
     * @author imenger
     * @date 2021/1/19 3:49 下午
     */
    public void singAfterQuest() {
        stream.println("Tee Hee Hee,这位勇敢的骑士踏上了探索之旅!");
    }
}

Minstrel 只有两个类,一个是在骑士执行任务之前,会调用singBeforeQuest()方法,完成任务之后,会调用singAfterQuest()方法。然后 minstrel 会通过构造器注入进来一个 stream 类,来歌唱。

程 序 清 单 1.10 BraveKnight必 须 要 调 用 Minstrel的 方 法

package sia.knights;

/**
 * 将 诗人 加到 勇敢的骑士里面
 *
 * @author imenger
 * @date 2020/9/15 4:12 下午
 */
public class BraveKnightMinstrel {

    private Quest quest;

    private Minstrel minstrel;

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

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

}

BraveKnightMinstrel 将刚刚新建的 minstrel 类引用进来,这时直接执行 main 方法是肯定不行的,还需要再 spring 配置里面将新建的这个 bean 注入到 这个构造器中。

如果你想直接调用 minstrel 这个类的方法,就失去了 aop 真正的意义了,或者换个话说,那样就太简单了。

也就是说 minstrel(吟游诗人)他又他自己的职责,不需要 braveKnight 来命令。接下来咱们利用 AOP,声明吟游诗人的职责,而骑士本身并不用直接访问 minstrel 这个类的方法。

程序清单1.11 将Minstrel声明为一个切面

<?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.xsd
		http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="knight" class="sia.knights.BraveKnight">
        <constructor-arg ref="quest"/>
    </bean>

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


    <!-- 声明 一个 minstrel bean-->
    <bean id="minstrel" class="sia.knights.Minstrel">
        <constructor-arg value="#{T(System).out}"/>
    </bean>

    <!-- 1:定义切点 2:声明前置通知 3 声明后置通知-->
    <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>

下面我分别介绍一下这些 标签是什么意思:

<bean id="minstrel" class="sia.knights.Minstrel">:将Minstrel 这个 bean 声明为一个切面;

<aop:aspect ref="minstrel">:引用该bean;
<aop:pointcut id="embark" 
expression="execution(* *.embarkOnQuest(..))"/>:声明一个切点,命名为:embark
其中 expression 属性来选择所应用的通知,其是就是来 execution embarkOnQuest 这个方法。
其中()属于切点表达式语言。至于怎么编写 表达式的语法,后续我会详细给大家介绍。

<aop:before pointcut-ref="embark" method="singBeforeQuest"/>

<aop:after pointcut-ref="embark" method="singAfterQuest"/>

为了进一步定义切面:

声明(使用<aop:before>)在embarkOnQuest()方法执行前调 用Minstrel的singBeforeQuest()方法,这种方式被称为前置通知(before advice)。

同时声明(使用<aop:after>) 在embarkOnQuest()方法执行后调用singAfter Quest()方法。这种方式被称为后置通知(afteradvice)。

其中 pointcut-ref 属性引用了 前面所说的 embark 切点。

通过对以上的描述,你现在应该知道了spring 在执行骑士探险任务的时候,前后会调用 Minstrel 的 before 和 after 方法。

这时候,如果单独看 Minstrel 这个 bean,依旧是一个简简单单的 POJO(Plain Ordinary Java Object)简单的Java对象,但是当我们设置了 spring 上下文之后,它实际上已经是一个切面了。

最重要的是:

Minstrel 现在就是咱们这个程序中的关键组成部分了,已经应用到了 BraveKnight 中,但是 BraveKnight 不需要直接去调用它,实际上无感知的存在。

我执行以下看看效果如何:

只需将 KnightMain 加载 Spring 上下文的 地址换一下,换成刚写的清单 1.11.1 就可以了,直接执行查看执行结果:

4:55:38 下午:执行任务  'KnightMain.main()' ...

> Task :compileJava
> Task :processResources UP-TO-DATE
> Task :classes

> Task :KnightMain.main()
啦啦啦,骑士真勇敢!
Embarking on quest to slay the dragon!
Tee Hee Hee,这位勇敢的骑士踏上了探索之旅!


BUILD SUCCESSFUL in 880ms
3 actionable tasks: 2 executed, 1 up-to-date
4:55:39 下午:任务执行完成  'KnightMain.main()'。

1.1.4 使用模板消除样板式代码

什么叫样板式代码(bc)

就是重复做造的轮子。通常为了实现通用的,简单的任务,你不得一遍一遍的重复编写一样的代码。

最常见的一个例子就是 使用 JDBC 访问数据库查询数据。(当然后续你会觉得 jdbc 是啥玩意儿,对吧,mybatis,hql,c3p0,dbcp,druid)

程序清单1.12 许多Java API,例如JDBC,会涉及编写大量的样板式代码

这段JDBC代码查询数据库获得员工姓名和薪水,一行一行看完其是也很简单,但是过于臃肿。有强迫症的你,最忌讳的就是感觉废话连篇,乱七八糟。

一句 sql 淹没在了一队 jdbc 样板代码中。

最后,毕竟该说的也说了,该做的也做了,你不得不清理战场,关闭数据库连接、语句和结果集。同样为了平息JDBC可能会出现的怒火,你 依然要捕捉SQLException。

SQLException,这是一个检查型异常,即使它抛出后你也做不了太多事情。

JdbcTemplate实现

举个例子,使用Spring的JdbcTemplate(利用了 Java 5特性的JdbcTemplate实现)重写的getEmployeeById()方法仅仅关注于获取员工数据的核心逻辑,而不需要迎合JDBC API的需求。程序清单1.13展示了修订后的getEmployeeById()方法。

程序清单1.13 模板能够让你的代码关注于自身的职责

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

总结

这篇文章简单的 描述了 Spring通过 面向POJO编程、DI、切面和模板技术来简化Java开发中的复杂性。

其中,使用XML配置 bean和切面,至于这些文件如何加载,下一篇 将给大家 讲述 Spring容器。好了,再见。

结语:有啥不明白的,或者我写的有错误的,直接留言,我每天都会查看留言的哦。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值