《精通Spring:JavaWeb开发与SpringBoot高级功能》读书笔记

最近在学习Spring及相关技术栈,因此上图灵买了这本书《精通Spring:Java Web开发与Spring Boot高级功能》,本文主要是相关的读书笔记。

<!--more-->

1. 依赖注入

当在一个类A中实例化了另外一个类B的实例,则他们产生了紧密耦合关系,这导致A类很难测试,因为我们无法为期编写简单的单元测试。

解决这个问题的办法是在类A之外实例化类B,并将其传递给A,这个工作交给容器

Spring IoC容器负责根据应用程序开发人员的配置设置创建bean并将它们装配在一起,需要解决问题

  • Spring IoC容器怎么知道要创建哪些bean?具体来说,Spring IoC容器怎么知道要为BusinessServiceImpl和DataServiceImpl类创建bean?
  • Spring IoC容器怎么知道如何将bean装配在一起?具体来说,Spring IoC容器怎么知道要将DataServiceImpl类的实例注入BusinessServiceImpl类?
  • Spring IoC容器怎么知道在什么地方搜索bean?搜索类路径(classpath)中的所有包效率不高。

首先需要创建IoC容器,可以通过Bean工厂或应用程序上下文创建,一般使用后者。通过@Configuration注解

第一个问题,开发人员通过配置告诉容器需要创建的bean,类似于声明各种依赖项,方便其他类查找并依赖,使用@Repository、@Component或@Service注解

  • @Component注解是定义Spring bean的最常用方法
  • @Service注解用在业务服务组件中
  • @Repository注解用在数据访问对象(DAO)组件中

容器创建的Bean可以有多个作用于,默认作用域为单例(singleton),只存在一个实例;可以通过@Scope(xxx)声明,常见的作用域有

  • singleton,默认,每个Spring容器分配一个单例对象,主要用于不包含状态的bean
  • prototype,每次从容器请求bean都实例一个,用于包含独立状态的bean
  • 只在Spring Web中可用的特定bean,包括request、session和application

第二个问题,开发人员需要在为每个类声明其依赖项,使用@Autowired注解,这样,容器在合适的时机会实例化并注入依赖,实际上是通过setter实现的注入

public class BusinessServiceImpl {
  private DataService dataService;
  // 通过setter注入
  public void setDataService(DataService dataService) {
    this.dataService = dataService;
  }
}

另外一种注入方式是通过构造函数注入

public class BusinessServiceImpl {
  private DataService dataService;
  // 通过构造参数的形式注入
  public BusinessServiceImpl(DataService dataService) {
    super();
    this.dataService = dataService;
  }
}

第三个问题,在创建应用程序上下文的时候可以指定扫描目录

1.1. 扩展 面向切面编程

参考:什么是面向切面编程AOP?

面向对象三大特点:继承、封装和多态,其中封装就是把不同的功能分散在不同的类中,降低每个类的复杂程度,使类可复用;带来的缺点是,在分散功能的同时,增加了代码的重复性。

比如某个打印日志的方法,需要在不同的地方都调用打印日志,向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。因此,期望有一种在我们需要的时候随意插入代码的方案。这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。

一种实现AOP的方案是通过代理来实现

2. Spring MVC构建Web应用

在最初的Model1 架构中,JSP直接处理浏览器需求,甚至需要查询数据库,这导致JSP非常臃肿复杂,难以维护

Model2架构中,明确划分了模型、视图与控制器之间的职责

  • 模型:表示要用于生成视图的数据。
  • 视图:使用模型来渲染屏幕。
  • 控制器:控制流并获取浏览器提出的请求,填充模型以及重定向到视图,主要由Servlet负责

对于一些控制器公共的逻辑,如在请求前进行用户鉴权等,可以通过前置控制器处理,被称为DispatcherServlet

@Controller
public class BasicController {
  @RequestMapping(value = "/welcome")
  @ResponseBody
  public String welcome() {
    return "Welcome to Spring MVC";
  }
}
  • @Controller:这定义一个可以包含请求映射(将URL映射到控制器方法)的Spring MVC控制器。
  • @RequestMapping(value = "/welcome"):这定义URL /welcome到welcome方法的映射。浏览器向/welcome发送请求时,Spring MVC开始运行并执行welcome方法。
  • @ResponseBody:在这个特定的上下文中,welcome方法返回的文本会作为响应内容发送给浏览器。@ResponseBody提供了许多功能——特别是在构建REST服务方面
  • 此外还有限定HTTP请求方法,获取请求参数等注解

一般不会直接在控制器中拼接返回内容,而是交给JSP处理,为了实现这种方式,需要

  • 需要创建jsp视图
  • 需要配置视图解析器,用于根据名字找到具体位置的jsp视图模板
  • 在控制器中声明需要渲染的视图名字,在接收到请求时,视图解析器会自动查找对应的模板。

我们还希望通过控制器将数据(Model)传递到视图(View)上,可以通过

  • ModelMap,控制器添加一个新的ModelMap类型参数,Spring MVC会实例化一个模型,并使它对此方法可用。放入模型中的属性将可以在视图中使用。
  • ModelViewk,控制器方法可以返回一个ModelAndView对象,在模型中填入视图名称和相应属性,视图中可以使用Model相关属性

POJO指简单Java对象。通常,它用于表示遵循常规Java bean约定的bean。一般情况下,它包含私有成员变量(带有getter、setter)以及没有参数的构造函数。

3. 向微服务和云原生应用程序进化

使用Spring的典型Web应用程序架构包括

  • Web层:这一层通常负责控制Web应用程序流(控制器或前端控制器)并渲染视图,主要取决于如何向用户公开业务逻辑,是网站还是RESTful Web服务
  • 业务层:所有业务逻辑都是在这一层编写的。大多数应用程序从业务层开始进行事务管理。
  • 数据层:这一层负责检索Java对象中的数据并将它们持久化到数据库表中。此外,它还负责与数据库进行交互。
  • 集成层:应用程序需要通过队列或通过调用Web服务与其他应用程序交互。集成层负责与其他应用程序建立此类连接。
  • 横切关注点:这些是横跨不同层的关注点,如日志记录、安全性、事务管理等。由于Spring IoC容器负责管理bean,它可以通过面向切面编程(AOP)将这些关注点织入bean。

默认情况下,Spring Framework支持使用大量设计模式。下面提供了一些示例。

  • 依赖注入或控制反转:这是Spring Framework支持的基本设计模式。它可以实现松散耦合和可测试性。
  • 单例:默认情况下,所有Spring bean均为单例bean。
  • 工厂模式:使用bean工厂对bean进行实例化是工厂模式的典型示例。
  • 前端控制器:Spring MVC将DispatcherServlet用作前端控制器。因此,在使用Spring MVC开发应用程序时,将采用前端控制器模式,感觉这一项命名为请求拦截器更合理一点
  • 模板方法:帮助我们避免样板代码。许多基于Spring的类——JdbcTemplate和JmsTemplate——实现了这种模式。

如果系统设计合理,可满足当前的需求,并且不断进化,经过全面测试,那么就可以轻松地对它进行重构,以满足未来的需求

有一个软件开发原则是尽量精简,几乎适用于我们讨论的任何话题:变量的作用域,以及方法、类、包或组件的大小。我们希望所有这些要素都尽可能地精简。

微服务就是这个原则的简单延伸。它是一种专注于构建基于能力、可独立部署的小型服务的架构风格

  • 根据业务能力识别模块。也就是说,模块提供了哪种功能。
  • 每个模块都可以单独构建和部署,甚至部署在不同的机器上

微服务的特点

  • 小型轻量级微服务,,微服务应遵循单一职责原则,经验法则是,应该可以在5分钟内构建和部署微服务
  • 与基于消息的通信进行互操作,使用各种技术在系统不同服务之间进行通信。实现互操作的最佳途径是使用基于消息的通信
  • 能力一致的微服务,微服务具有清晰的边界至关重要。通常,每个微服务都能非常高效地提供一种业务能力,一种常见的划分是领域驱动设计
  • 可独立部署的单元,每个微服务都可以单独构建和部署
  • 无状态,理想的微服务没有状态,不会在请求之间存储任何信息。创建响应所需的所有信息均存放在请求中
  • 自动化构建和发布流程, 微服务采用自动化构建和发布流程
  • 事件驱动的架构,用户使用事件驱动的架构来构建微服务,而不是常规的按顺序在某一个代码块处理所有逻辑
  • 独立团队,微服务由独立的团队开发。该团队掌握开发、测试和部署微服务所需的各种技能

微服务的优势

  • 更快地发布模块和功能单元
  • 技术进化,不同模块可以使用不同的技术、语言等,紧跟技术发展趋势
  • 可用性和扩展性,每个模块承担的功能和工作量不同,可以扩展某些服务的容量和机器等
  • 可以组件更小型独立的团队

微服务的挑战

  • 自动化需求日益增长,服务增加,手动执行的效率会非常低
  • 需要非常清晰地定义子系统的边界
  • 可见性和监视,为了降低与多个微服务和基于异步事件的协作关联的复杂性,必须增强可见性
  • 容错性,需要保证某个服务中断时系统的健壮性
  • 最终一致性,
  • 运营团队的需求增加
  • 感觉还应该加上服务通信带来的性能损耗问题

4. 使用Spring Boot构建微服务

使用Spring Boot的主要目标如下。

  • 使用基于Spring的项目快速开始构建微服务。
  • 采用固定设置。基于常见用法做出默认假设,并提供配置选项来处理偏离默认设置的情况。
  • 开箱即提供一系列非功能性特性。
  • 不生成代码并避免大量使用XML配置。

spring-boot-starter-parent依赖项包含要使用的默认Java版本、Spring Boot使用的依赖项的默认版本,以及Maven插件的默认配置。spring-boot-starter-parent依赖项继承自spring-boot-dependencies。

spring-boot-dependencies为Spring Boot使用的所有依赖项提供了默认依赖项管理

4.1. RESTful服务

无论什么时候,要在Spring Boot中构建应用程序,都需要开始寻找starter项目。starter项目是专为不同目的而定制的简化版依赖项描述符。spring-boot-starter-web是使用Spring MVC构建Web应用程序(包括RESTful)所需的starter,它将Tomcat作为默认的嵌入式容器

package com.example.test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }
}
  • SpringApplication类可用于从Java main方法中引导和启动Spring应用程序,主要会执行下面步骤
    • 创建Spring ApplicationContext的实例。
    • 启用相关功能,以接受命令行参数,并将它们公布为Spring属性。
    • 根据配置加载所有Spring bean。
  • @SpringBootApplication注解是以下3个注解的缩写
    • @Configuration:指出这是一个Spring应用程序上下文配置文件。
    • @EnableAutoConfiguration:启用自动配置——Spring Boot的重要特性。我们将独辟一章介绍自动配置。
    • @ComponentScan:在这个类的包和所有子包中扫描Spring bean。

REST服务的最佳做法之一,是根据执行的操作采用适当的HTTP请求方法

一个简单的的Restful控制器结构

  • GET 读取——检索资源的详细信息
  • POST 创建——创建新项目或资源
  • PUT 更新/替换
  • PATCH 更新/修改资源的某个部分
  • DELETE 删除
package com.example.test;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BasicController {
    @GetMapping("/welcome")
    public String welcome() {
        return "Hello World";
    }
    // 返回pojo 对应的JSON对象
    @GetMapping("/welcome-with-object")
    public WelcomeBean welcomeWithObject() {
        return new WelcomeBean("Hello World");
    }
    // 获取路径参数
    private static final String helloWorldTemplate = "Hello World, %s!";
    @GetMapping("/welcome-with-parameter/name/{name}")
    public WelcomeBean welcomeWithParameter(@PathVariable String name)
    {
        return new WelcomeBean(String.format(helloWorldTemplate, name));
    }
}
  • @RestController:@RestController注解是@ResponseBody与@Controller注解的组合。它常用于创建REST控制器。
  • @GetMapping("welcome"):@GetMapping是@RequestMapping(method = RequestMethod.GET)的缩写。此注解是一个可读替代项。使用此注解的方法将处理指向welcome URI的GET请求。

使用Spring Initializr可以快速创建Spring boot应用,IDEA也内置了相关插件。

HATEOAS是REST应用程序架构的约束条件之一。它可以向RESTful服务显示给定资源的相关链接。返回特定资源的详细信息时,我们还返回了可以对该资源执行的操作的链接以及相关资源的链接。如果服务消费方可以使用响应中的链接进行交易,就不需要对所有链接进行硬编码。

大多数资源不经常使用HATEOAS,但是,在降低服务提供方与消费方之间的耦合度方面,它确实很有用处。

4.2. Spring Boot的配置文件

开发人员会一次性构建应用程序(采用JAR包或WAR包),然后将其部署到多个环境中,这时,一个良好的做法是将不同环境之间的配置变更存储到外部设置文件或数据库中。

在Spring Boot中,application.properties是用于从中选择配置值的默认文件。Spring Boot可以从类路径的任何位置选择application.properties文件。通常,application.properties位于src\main\resources,下面是一些可以通过application.properties配置的重要特性

  • 日志记录logging.xxx
  • 嵌入式服务器配置server.xxx
  • Spring MVC配置spring.mvc.xxx
  • Spring starter安全性 security.xxx
  • 数据源、JDBC和JPA spring.datasource.xxx

此外还可以配置自定义属性

somedataservice.url=http://abc.service.com/something

然后通过@Value("${somedataservice.url}")注解访问。

如果我们希望能够在不同环境中为同一属性定义不同的值,则需要使用配置文件

首先在application.properties中配置活动配置文件

spring.profiles.active=dev

配置活动配置文件后,即可在application-{profile-name}.properties中定义特定于该配置文件的属性。对于dev配置文件,属性文件的名称为application-dev.properties

在代码中,可以通过@Profile注解标注使用不同的配置文件,如@Profile("dev")

除了上面提到的两种常用配置方法,还可以通过命令行参数、操作系统环境变量等方式来配置应用属性。

下面介绍使用YAML文件来进行配置,YAML是“YAML Ain't Markup Language”(YAML不是一种标记语言)的缩写(哈哈哈第一次知道

相比于application.properties,YAML配置的可读性更高,因为它可以对属性进行更合理的分组;YAML的另一个优势在于它允许在单一设置文件中为多个配置文件指定配置

4.3. 嵌入式服务器

传统上,使用Java Web应用程序时,我们会构建Web应用程序档案(WAR)或企业应用程序档案(EAR),然后将它们部署到服务器上

Spring Boot引入了嵌入式服务器的概念,其中的Web服务器是应用程序可部署包(JAR)的一部分。要使用嵌入式服务器部署应用程序,在服务器上安装Java就足够了。使用Spring Boot构建应用程序时,默认做法是构建JAR。使用spring-boot-starter-web时,默认嵌入式服务器为Tomcat。

5. Spring Data

从第一版Java EE开始,JDBC就用于与关系型数据库交互。JDBC使用SQL查询来操作数据

常规项目通常包含数千行JDBC代码。编写和维护JDBC代码非常麻烦。为了在JDBC之上提供另一个层,以下两个框架变得流行起来。

  • MyBatis(之前称为iBatis):使用MyBatis不需要手动编写代码来设置参数并检索结果。它提供了简单的XML或基于注解的配置,以将Java POJO映射到数据库。
  • Hibernate:Hibernate是一种对象/关系映射(ORM)框架。ORM框架有助于将对象映射到关系型数据库表。使用Hibernate的好处在于,开发人员不需要手动编写代码。一旦映射了项目与表之间的关系,Hibernate就使用这些映射来创建查询并填充/检索数据。

每种数据存储都通过不同的方式来连接和检索/更新数据。Spring Data旨在提供一致的模型——另一个抽象层——来访问不同类型的数据存储中的数据

6. Spring Cloud

Spring Cloud旨在为我们在云端构建系统时常见的一些模式提供解决方案。它的一些重要特性包括:

  • 用于管理分布式微服务配置的解决方案;
  • 使用名称服务器注册和发现服务;
  • 在多个微服务实例间实现负载均衡;
  • 更多采用熔断机制的容错服务;
  • API网关用于支持聚合、路由和缓存;
  • 跨微服务分布式跟踪功能。

Spring Cloud不是指单个项目,它包含一组子项目,这些子项目旨在解决与部署到云端的应用程序有关的问题

每个微服务实例可以有它自己的配置——不同数据库、使用的不同外部服务,等等。例如,如果某微服务部署到5个环境,每个环境中有4个实例,那么,此微服务可能一共有20个不同的配置。很难单独维护不同微服务的配置,这里的解决方案是搭建集中式配置服务器。

集中式配置服务器存放了属于各种不同微服务的所有配置。这有助于将配置与应用程序可部署包分离开来,Spring Cloud Config支持集中式微服务配置。

如果存在大量微服务实例,则对每个实例执行更新配置等操作十分麻烦,解决办法是使用Spring Cloud Bus将配置更改通过消息代理(如RabbitMQ)传播到多个实例

Feign有助于我们以最少的配置和代码为REST服务创建REST客户端。你只需要定义一个简单的接口,并使用正确的注解即可。Feign与Ribbon(客户端负载均衡)和Eureka(名称服务器)进行了紧密集成。

微服务是云原生架构最重要的构建块。微服务实例将根据特定微服务的负载进行向上和向下扩展,需要确保在不同微服务实例之间平均分配负载。Spring Cloud Netflix Ribbon通过在不同微服务实例之间执行轮询调度算法,实现了客户端负载均衡

微服务架构包含大量彼此交互的小型微服务。此外,每个微服务可能还有多个实例。由于我们会动态地创建并销毁新的微服务实例,因此,手动维护外部服务连接和配置会很难。名称服务器提供了服务注册和服务发现功能。使用名称服务器,微服务可以自主注册,还可以发现它们希望与之交互的其他微服务的URL。

  • 所有微服务(不同微服务和它们的所有实例)都会在每个微服务启动时自主注册到名称服务器。服务消费方希望获取特定微服务的位置时,它会向名称服务器提出请求。
  • 每个微服务都会分配有唯一的微服务ID。在注册请求和查找请求中,此ID会作为键使用。
  • 微服务可以自动注册并自主注销。任何时候服务消费方以微服务ID向名称服务器提出查找请求,都会收到该微服务的实例列表。

微服务直接与彼此进行交互时,但存在很多相似的横切关注点,如鉴权、动态路由等,一个最常用的解决方案是使用API网关。向微服务提出以及它们之间的所有服务调用应通过API网关完成。

典型的微服务架构包含大量组件,而一次典型的调用过程可能会涉及四五个或更多的组件,常见的解决方案是通过仪表板提供集中式日志记录,将所有微服务日志整合到一个位置,并提供一个仪表板来进行管理。

微服务架构往往包含大量微服务组件。如果某个微服务中断,会出现什么情况?微服务架构应该有弹性,并能够妥善处理服务错误。Hystrix为微服务提供了容错功能。

7. Spring Cloud Data Flow

集成应用程序有如下两种方式。

  • 同步:服务消费方调用服务提供方并等待响应。这要求服务提供方一直可用,如果不可用,则需要服务消费方等待提供方恢复后重新执行
  • 异步:服务消费方通过在消息代理上发送消息来调用服务提供方,但不等待响应。优势在于服务提供方关闭一段时间后恢复,仍旧可以处理之前的请求信息,虽然可能会出现延迟,但数据最终会保持一致

微服务架构倡导采用基于消息的通信。反应式编程的一个重要原则是基于事件(或消息)构建应用程序。反应式系统的重要特点

  • 即时响应性:系统会及时响应用户请求。设置了明确的响应时间要求,在各种情况下,系统都能满足这些要求。
  • 回弹性:分布式系统使用多个组件构建,其中任何组件都可能会发生故障。反应式系统应设计为将故障限制在本地范围内,例如在每个组件内。这可以防止整个系统在出现本地故障时崩溃。
  • 弹性:反应式系统能够灵活处理各种不同的工作量。工作量巨大时,系统可以添加额外的资源,而工作量减少时释放资源。弹性通过商用硬件和软件来实现。
  • 消息驱动:反应式系统由消息(或事件)驱动。这确保了组件间的松散耦合,同时可以单独扩展不同的系统组件。使用非阻塞通信可以确保线程在较短时间内保持活动状态。

反应式方法通常包括以下3个步骤

  • 订阅事件。
  • 事件发生。
  • 注销。

8. Spring最佳实践

使用Maven标准目录布局

  • src/main/java:所有应用程序相关的源代码。
  • src/main/resources:所有应用程序相关的资源——Spring上下文文件、属性文件、日志记录配置等。
  • src/main/webapp:Web应用程序相关的所有资源——视图文件(JSP、视图模板、静态内容等)。
  • src/test/java:所有单元测试代码。
  • src/test/resources:单元测试相关的所有资源。

使用分层架构,为每一层提供不同的Spring上下文。这有助于分离每一层的关注点;在每个层中创建独立的api和实现模块

  • 表示层:在微服务中,REST控制器位于表示层。在典型的Web应用程序中,这一层还包含与视图有关的内容——JSP、模板和静态内容。表示层与服务层交互。
  • 服务层:这一层充当业务层的外观层。由于存在不同的视图——移动设备、Web和平板计算机,因此,这一层可能需要不同类型的数据。服务层了解它们的需求,并会基于表示层提供适当的数据。
  • 业务层:这一层保存所有业务逻辑。另一个最佳实践是将大多数业务逻辑存放到领域模型中。业务层与数据层交互,以获取数据并在此基础上添加业务逻辑。
  • 持久层:这一层负责在数据库中检索和存储数据,通常包含JPA映射或JDBC代码。

异常处理,有以下两种类型的异常。

  • 受检异常:服务方法引发此异常时,所有消费方方法应处理或引发异常。会导致消费方需要添加额外的异常处理代码
  • 未受检异常:服务方法引发异常时,不需要消费方方法处理或引发异常。
  • Spring将大多数异常作为未受检异常,决定方法将引发哪种异常时,要始终考虑方法的消费方

确保简化Spring配置,管理Spring项目的依赖项版本,单元测试,集成测试

要构建高性能应用程序,配置缓存至关重要。你不希望始终访问外部服务或数据库。不常变化的数据可以存入缓存。

Spring和Spring Boot依赖于Commons Logging API。它们不依赖于任何其他日志记录框架。Spring Boot提供了starter来简化特定日志记录框架的配置

9. 在Spring中使用Kotlin

Kotlin是一种开源静态类型语言,可以使用它来创建在JVM、Android和JavaScript平台上运行的应用程序,完全兼容Java代码。Kotlin旨在解决Java语言面临的一些重要问题,并提供一种简洁的替代语言。

可以使用Kotlin 代替Java来初始化 SpringBoot等项目。

10. 小结

本书主要介绍了Spring及相关众多技术栈,但是整体来说各部分都比较简略,可以大致了解Spring相关生态,用来入门还是可以的,但感觉没必要精读。