Spring 学习笔记

2019-08-25  本文已影响0人  Whyn

简介

首先明确一下概念:

:现实生活中,我们一般讲到 Spring,一般就是指代 Spring Framework。因此,下文中所有出现 Spring 位置的地方,均指 Spring Framework,请知悉。

Spring 架构体系(5.0)

Spring Framework Runtime

Spring 框架是一个分层架构,从总体来看,Spring 分为3层,最底层是核心层,包括 IOC、AOP 等核心模块,中间层是封装的 JavaEE 服务、作为中间的驱动组件,最上层是各个应用。

组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。每个模块的功能如下:

Spring 优势

控制反转(IOC)和 依赖注入(DI)

以 OOP 的思想进行代码编写时,基本上我们都会创建多个类,类与类之间存在协作关系,共同完成某个功能。

比如,假设现在我想喝绿茶,那我们就会自己去泡杯绿茶,如下代码所示:

public class Me {

    public static void main(String[] args) {
        // 自己泡杯绿茶
        Tea tea = new GreenTea();
        Me.drink(tea);
    }

    public static void drink(Tea tea) {
        System.out.println(tea.flavor());
    }

    private interface Tea {
        String flavor();
    }

    private static class GreenTea implements Tea {

        public String flavor() {
            return "Green Tea";
        }
    }
}

但是现在我突然想喝红茶了(业务需求更改),那我们就把绿茶去掉,改为红茶:

public class Me {

    public static void main(String[] args) {
        // 自己泡杯红茶
        Tea tea = new RedTea();
        Me.drink(tea);
    }
    ...
    private static class RedTea implements Tea {

        public String flavor() {
            return "Red Tea";
        }
    }
}

到这里其实就可以看出,如果我们自己(Me)手动创建依赖类(GreenTea/RedTea),那么每次当业务需求更改时,我们都要手动更改业务代码,两者之间的正向依赖耦合太重。

其实一个更好的方法就是我们自己不去泡茶,而是直接向饮品店(第三方)进行购买,想买啥口味的直接跟饮品店说即可:

public static void main(String[] args) {
        // 向饮品店直接购买茶
        Tea tea = DrinkShop.makeTea("Red Tea");
        Me.drink(tea);
    }
    ...
    public static class DrinkShop {
        public static Tea makeTea(String flavor) {
            Tea tea = null;
            switch (flavor) {
                case "Red Tea":
                    tea = new RedTea();
                    break;
                case "Green Tea":
                    tea = new GreenTea();
                    break;
            }
            return tea;
        }
    }
}

我们通过一个第三方类DrinkShop就解耦了客户Me与具体饮品GreenTea/RedTea的耦合了。这其实就是工厂模式的应用,也是 IOC 的一个简单实现。

对于上文示例来说,Me从刚开始的自己动手泡茶new GreenTea()/new ReaTea(),到最后通过饮品店DrinkShop获取茶DrinkShop.makeTea,最后这个过程就是Me的控制被反转了(更具体来说,是Me获取依赖对象的过程被反转了)。

public class Me {
    private Tea tea;
    public static void main(String[] args) {
        // 构造函数注入依赖
        Me me = new Me(new GreenTea());
        me.drink();

        me = new Me();
        // setter注入依赖
        me.setTea(new RedTea());
        me.drink();
    }

    public Me(){
    }

    public Me(Tea tea){
        this.tea = tea;
    }
    public void setTea(Tea tea){
        this.tea = tea;
    }
    public void drink(){
        System.out.println(this.tea.flavor());
    }
    ...
}

IOCDI 其实是对同一概念的不同描述,两者都是为了解决类间耦合,但 IOC 关注的是由第三方容器管理被依赖类,强调的是控制反转,而 DI 关注的是被依赖类如何注入到依赖类,强调的是注入。

DI 其实可以借助 IOC容器 进行依赖注入:IOC容器 先进行依赖查找,再进行依赖注入。

不严格情况下,通常我们将 DI 等同于 IOC

Spring 中使用 IOC

这里使用 Spring IOC 复写上文示例代码,具体步骤如下:

  1. 首先创建一个 Maven 普通 Java 项目,然后添加 spring-context 依赖:
<packaging>jar</packaging>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.9.RELEASE</version>
    </dependency>
</dependencies>

spring-context 内部包含了 Spring 框架核心容器的所有功能,如下图所示:

spring-context
  1. resources资源目录下,创建配置文件bean.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置 bean 类,交由 Spring IOC容器进行管理 -->
    <bean id="redTea" class="com.yn.spring.ioc.Me$RedTea" />
    <bean id="greenTea" class="com.yn.spring.ioc.Me$GreenTea" />
</beans>
  1. 源码获取 IOC 容器管理对象,并获取依赖对象:
public class Me {
    public static void main(String[] args) {
        // 创建Spring框架核心容器对象,并加载配置文件
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        // 获取 bean 对象
        Tea tea = ac.getBean("greenTea", Tea.class);
        Me.drink(tea);

        // 获取 bean 对象
        tea = ac.getBean("redTea", Tea.class);
        Me.drink(tea);
    }

    public static void drink(Tea tea) {
        System.out.println(tea.flavor());
    }

    private interface Tea {
        String flavor();
    }

    private static class RedTea implements Tea {

        public String flavor() {
            return "Red Tea";
        }
    }

    private static class GreenTea implements Tea {

        public String flavor() {
            return "Green Tea";
        }
    }
}

Spring框架中 IOC 相关常用 api 讲解

Spring IOC 容器管理 bean 对象方式

Spring IOC 容器管理 bean 对象可以大致分为如下几种类型:

Spring IOC 常用注解

使用 XML 配置 IOC 容器时,配置与代码的隔离十分彻底,但是当配置项较多时,XML 配置会显得臃肿并且难以维护。因此,Spring 框架还为我们提供了注解配置方法。

我们将 Spring 的注解配置分为如下几类:

XML 配置和注解配置选择方案

无论是使用 XML 配置还是注解配置开发,其本质都是一样的。

通常,对于自己编写的代码,我们选择使用注解开发;对于第三方库提供的 api,我们使用 XML 配置开发。

由于使用了 XML 配置,因此 Spring IOC 容器的创建一般采用 ClassPathXmlApplicationContext,此时要使能注解开发,就需要在 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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 指定注解相关扫描类 -->
    <context:component-scan base-package="com.yn.spring.ioc" />

</beans>

具体代码如下:

@Component("me")
public class Me {
    ...
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        Me me = ac.getBean("me", Me.class);
        me.drink();
    }
    ...
}

Spring AOP 简介

Spring 框架提供的两个核心功能就是 IOC 和 AOP。

前面已对 IOC 进行了讲述,下面主要对 AOP 进行讲解。

很久之前写过一篇介绍 AOP 的文章:AOP简介,现在简单总结一下:

传统 OOP(Object-Oriented Programing,面向对象编程) 编程中,其思想是将事务对象化,一切皆对象,对象行为完全由自己控制。在实际编程中,对象的某个行为总是会附属带上其他一些辅助性操作(比如日志),使得行为的操作并不具备 单一职责,代码稍显混乱且臃肿。

而 AOP 具备“编织”代码能力,其可以将不同模块的内容在运行时很好地组织到一起,因此我们便可以将对象行为的核心功能和辅助功能进行切割,在运行时再通过 AOP 组织到一起。AOP 的出现可以让我们更加专注于模块开发,其很好地实现了各模块间的松散耦合。

简单举个例子:传统 OOP 编程中,日志打印需要嵌入到类的每个方法内部,而在 AOP 中,我们可以将日志打印当成一个辅助模块,并以声明的方式应用到需要日志的其他组件上。系统其他组件无须知道日志模块的存在,日志模块对其他组件无侵入,最后又能完整地实现 OOP 上所需的功能。

AOP 和 OOP 是两种不同的编程思想,AOP 很好地解决了 OOP 编程中存在的一些缺陷,可以说 AOP 是 OOP 的补充与完善。

AOP 中的一些术语

try{
    before;                     // 前置通知
    method.invoke(target,args); // 业务方法
    after-returning;            // 后置通知
    return;
}catch (Exception e){
    after-throwing;             // 异常通知
}finally {
    after;                      // 最终通知
}

粗暴理解如下

Spring 中使用 AOP

  1. 首先导入 Sprig IOC 支持包:spring-context,再导入 AOP 切入点表达式解析包:aspectjweaver
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>
  1. 创建一个业务类,模拟真实项目的业务方法:
public class TestService {
    public void doSomething(){
        System.out.println("执行业务方法!");
    }
}
  1. 创建一个日志类,模拟日志打印功能:
public final class Logger {

    public Object printLog(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("start log: " + joinPoint.getSignature().getName());
        Object obj = joinPoint.proceed();
        System.out.println("end log: "+joinPoint.getSignature().getName());
        return obj;
    }
}
  1. 配置文件中配置业务类和日志类 bean 对象,并配置一个 AOP 切面,实现业务类注入日志功能:
<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置业务类 -->
    <bean id="testService" class="com.yn.service.TestService" />
    <!-- 配置日志类 -->
    <bean id="logger" class="com.yn.utils.Logger" />

    <!-- 配置AOP -->
    <aop:config>
        <!-- 配置一个切入点:
                id:切入点名称
                expression:切入点表达式(执行切入的目标方法)
         -->
        <aop:pointcut id="logPointcut" expression="execution(* com.yn.service.TestService.*(..))"/>
        
        <!-- 配置一个切面:
                id:切面名称
                ref:切面通知引用的对象
        -->
        <aop:aspect id="log" ref="logger">
            <!-- 配置通知和切入点 -->
            <aop:around method="printLog" pointcut-ref="logPointcut" />
        </aop:aspect>
    </aop:config>
</beans>
  1. 创建一个测试类,运行业务类方法,查看日志是否切入成功:
public class TestServiceTest {

    @Test
    public void doSomething() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        TestService service = ac.getBean("testService", TestService.class);
        service.doSomething();
    }
}

运行结果如下:

运行结果

:上述例子中唯一一个需要了解的就是 切入点表示式(<aop:pointcut expression="表达式" />

切入点表达式的标准格式为:

[访问修饰符] 返回值 包名.包名.包名...类名.方法名(参数列表)

:访问修饰符是可选的。

比如,下面就是一个标准的切入点表达式写法:其作用就是指定要拦截的方法,织入切片代码

public void com.yn.service.TestService.doSomething()

切入点表达式支持通配符匹配,其中:

// 匹配任意包名下的 void TestService.doSomething() 方法
void *..TestService.doSomething()
// 匹配任意包名下的任意参数列表的 void TestService.doSomething 方法
void *..TestService.doSomething(..)

按上述所讲的切入点表达式写法,我们甚至可以写出一个匹配任何方法的全通配符表达式:

// 全通配符表达式:任意返回值 任意级包 任意类.任意方法(0个或多个任意参数)
* *..*.*(..)

Spring 基于注解的 AOP 配置

与 IOC 一样,Spring 同样为 AOP 提供了注解配置功能。

这里我们使用注解配置更改上面的 AOP 日志示例,具体步骤如下:

  1. 修改配置文件bean.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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 开启扫描 -->
    <context:component-scan base-package="com.yn"/>

    <!-- 配置Spring开启注解 AOP 支持 -->
    <aop:aspectj-autoproxy />

    <!-- 配置业务类 -->
    <bean id="testService" class="com.yn.service.TestService"/>
</beans>

:开启 Spring 注解支持也可以使用注解:@EnableAspectJAutoProxy,其相当于 XML 配置文件中的<aop:aspectj-autoproxy />

  1. 修改日志类,使用 AOP 相关注解:
@Component
@Aspect //表示该类是一个切面类
public final class Logger {

    // 定义一个切入点
    @Pointcut("execution(* com.yn.service.TestService.*(..))")
    public void logPointcut() {
    }

    // 定义一个通知,这里使用环绕通知
    @Around("logPointcut()")
    public Object printLog(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("start log: " + joinPoint.getSignature().getName());
        Object obj = joinPoint.proceed();
        System.out.println("end log: " + joinPoint.getSignature().getName());
        return obj;
    }
}

以上,我们便使用注解完成了 AOP 配置功能。

:Spring AOP 注解配置存在一个问题:在没有异常情况下,最终通知(after)会优先于后置通知(after-returning)执行,与正常执行顺序相反。因此,使用注解配置时,建议使用环绕通知(around)进行处理。

更多 AOP 注解使用方法,请查看:AspectJ之切点语法

Spring 整合 junit

  1. pom.xml中导入 JUnit:
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>        
  1. pom.xml'导入 spring-test 包:
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.1.9.RELEASE</version>
    <scope>test</scope>
</dependency>
  1. 使用 Spring 提供的运行器替换 JUnit 默认运行器:
@RunWith(SpringJUnit4ClassRunner.class)
  1. 告知 Spring 允许器配置方式(xml 或 注解配置),并提供配置文件路径:
// 注解配置,配置类为 SpringConfiguration.class
@ContextConfiguration(classes = SpringConfiguration.class)

// XML 配置,配置文件为 bean.xml
@ContextConfiguration(locations = "classpath:bean.xml")
  1. 到此,就可以直接获取 Spring IOC 容器内部的 bean 对象了:
// resources/bean.xml
<beans ...>
    <bean id="date" class="java.util.Date" />
</beans>
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class TestSpring {

    @Autowired
    private Date date;

    @Test
    public void testMerge(){
        Assert.assertNotNull(this.date);
        System.out.println(this.date);
    }
}

参考

上一篇 下一篇

猜你喜欢

热点阅读