服务端开发复习

Kiyotaka Wang Lv3

复习

考试:每次课前的复习,就是考试重点

题目分多选题和问答题,问答题5道题

web开发框架的分层、web框架请求的处理过程、spring security中web请求的保护以及有哪些步骤、反应式编程的4个接口、AOP编程解决什么问题以及开发的要点、Oauth2基于授权码的授权以及系统组成和处理流程(那张图要重点看)、spring的集成流解决什么问题。

今年考试很抽象(但也还好),主要是最后一题这老师的语文表达能力实在不敢恭维,感觉我被坑了。。。

什么叫做Spring初始化数据库的时机,怎么全答的是课上说的3个方法,这不显然是问在启动的哪个阶段初始化吗???何况初始化数据库方法多的去了,又不是只有那三个,我创个initializingbean不是也行?😅

开发环境搭建

源代码仓库管理

也称为版本控制(version control)系统,常用工具有:GitLab、SVN(Subversion)、Bitbucket等。

需纳入版本控制的有:功能代码、测试代码、测试脚本、构建脚本、部署脚本、配置文件等。

Git关键概念

在把本地修改的代码添加到本地仓库之前,一般先存放到暂存区(index),git commit将暂存区的代码提交到本地仓库,从本地到远程push。

Spring是Java生态圈的主流开发框架

开发框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法;另一 种定义认为,框架是可被应用开发者定制的应用骨架

Spring的核心是提供一个容器(container),也叫Spring应用上下文,容器里存放的是bean,容器负责对象的创建和消亡。

Spring的衍生。

Spring Web开发框架的分层(重要)

JUnit测试框架

注意测试用例的写法。@Test注解,测试方法必须用void(public可选)修饰,并且不能有参数。

Spring Boot DevTools工具(重要)

但感觉也没什么考头其实。。。

这只是运行态下的功能,在编译的时候没有任何作用。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-devtools</artifactId>
	<scope>runtime</scope>
</dependency>

依赖注入(Dependency Injection)

Spring的模块组成

Spring的两个核心技术

DI(Dependency Injection)

又叫控制反转(IoC)。保留抽象接口,让组件(Component)依赖于抽象接口,当组件要与其他实际的对象发生依赖关系时,由抽象接口来注入依赖的实际对象。

AOP(Aspect Oriented Programming)

通过预编译方式和运行期间动态代理实现功能的统一维护的一种技术。

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率。

Spring的核心是提供了一个容器(container)

也就是applicationContext,spring上下文。

问:Spring如何把B对象注入到A对象当中?

  1. 通过构造方法
  2. 通过setter方法,设置一个属性
  3. 在私有的属性字段上加@Autowired注解

不可以通过方法的参数注入。

bean的生命周期

Spring配置方案

自动化配置相关的注解一共有4个:@Component,@ComponentScan,@Autowired,@Configuration

组件扫描

组件扫描在搜索包的时候也会递归搜索子包。

自动装配

JavaConfig

混合配置(留意,有这方面的题目)

开发时候不会将所有的bean都放在一个配置文件中,一般会有一个根配置,其他的配置可能会导入根配置类,根配置类也可能会导入其他配置类。

@Import导入其它配置类

@ImportResource导入xml文件

Bean的作用域

课上说不考。

@Scope 可以与 @Component@Bean 一起使用,指定作用域

  • Singleton,单例,不使用 @Scope 时默认,在整个应用中,只创建 bean 的一个实例
  • Prototype,原型,每次注入或者通过 Spring 应用上下文获取的时候,都会创建一个新 bean 实例
  • Session,会话,在 Web 应用中,为每个会话创建一个 bean 实例
  • Request,请求,在 Web 应用中,为每个请求创建一个 bean 实例

使用会话和请求作用域:

@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart(){

}

面向切面编程(AOP)

横切关注点(背下来这4个)

不需要知道4个横切关注点之间的业务逻辑,背下来就可以了。

日志、安全、事务、缓存

image-20240101163432054

AOP术语

  • 通知(Advice):切面做什么以及何时做
  • 切点(Pointcut):何处,如"execution(* concert.Performance.perform( .. ))"切点表达式
  • 切面(Aspect):Advice 和 Pointcut 的结合
  • 连接点(Join point):方法、字段修改、构造方法。就是允许作为切入点的资源,所有类的所有方法均可以作为连接点
  • 引入(introduction):引入新的行为和状态
  • 织入(Weaving):切面应用到目标对象的过程
@Aspect // 并不具有 @Component 的效果,不能在扫描时实例化,因此需要添加 @Component 注解或在 JavaConfig 类中主动实例化
public class Audience {
	@Before("execution(* concert.Performance.perform( .. ))")
	public void silenceCellPhones() {
		System.out.println("Silencing cell phones");
	}

	@Before("execution(* concert.Performance.perform( .. ))")
	public void takeSeats() {
		System.out.println("Taking seats");
	}

	@AfterReturning("execution(* concert.Performance.perform( .. ))")
	public void applause() {
		System.out.println("CLAP CLAP CLAP!!!");
	}

	@AfterThrowing("execution(* concert.Performance.perform( .. ))")
	public void demandRefund() {
		System.out.println("Demand a refund");
	}
}
@Aspect
public class Audience1 {
	@Pointcut("execution(* concert.Performance.perform( .. ))")
	public void performance() {
	}

	@Before("performance()")
	public void silenceCellPhones() {
		System.out.println("Silencing cell phones");
	}

	@Before("performance()")
	public void takeSeats() {
		System.out.println("Taking seats");
	}

	@AfterReturning("performance()")
	public void applause() {
		System.out.println("CLAP CLAP CLAP!!!");
	}

	@AfterThrowing("performance()")
	public void demandRefund() {
		System.out.println("Demand a refund");
	}
}
@Aspect
public class Audience2 {
	@Pointcut("execution(* concert.Performance.perform( .. )) ")
	public void performance() {
	}

	@Around("performance()")
	public void watchPerformance(ProceedingJoinPoint joinPoint) {
		try {
			System.out.println(".Silencing cell phones");
			System.out.println(".Taking seats");
			joinPoint.proceed();
			System.out.println(".CLAP CLAP CLAP!!!");
		} catch (Throwable e) {
			System.out.println(".Demanding a refund");
		}
	}
}
@Configuration
@EnableAspectJAutoProxy // 开启 AspectJ 的自动代理机制
public class ConcertConfig {
	@Bean
	public Performance concert() {
		return new Concert();
	}

	@Bean
	public EncoreableIntroducer encoreableIntroducer() {
		return new EncoreableIntroducer();
	}
}

@Controller@Service@Repository 三个注解本身有 @Component 的实例化效果。

通知(Advice)类型

@Before:在一个方法调用之前切

@After:在一个方法执行结束之后切

@AfterReturning:在一个方法成功执行之后切

@AfterThrowing:在一个方法抛出异常后切

@Around:在一个方法前后切(环绕)

多选题:在一个方法成功运行之后切可以用哪些注解?after、aftereturning、around。

织入时机

image-20240101164941435

AspectJ切点指示器(pointcut designator)重要

通过切点指示器可以达到的5种效果:

  1. 我们可以指定在哪些方法上来做切入
  2. 在切面逻辑里带有调用者带来的参数
  3. 指定在哪些包路径的对象进行切入
  4. 指定在哪些特定的bean上进行切入
  5. 限定注解,只在某些特定注解的方法上进行切入。如图中有@InnerAuth注解的方法。

引入接口(introduction) 重要

@Aspect
public class EncoreableIntroducer {
    @DeclareParents(value = "concert.Performance+",//后面的+表示应用到所有实现了该接口的Bean
            defaultImpl = DefaultEncoreable.class)
    public static Encoreable encoreable;
}

这个切面定义一个静态的字段,上面的@DeclareParents表示我需要给实现了这个接口的对象引入这个行为。每一个对象之后都会实例化这个被切入的Encoreable接口。

Spring MVC

Spring MVC

(重要:必考)Spring MVC的请求处理过程

  1. 请求先到DispatcherServlet(Spring官方开发)。
  2. 首先先根据url在Handler mapping中查找映射关系找到对应的控制器。
  3. 将请求转交给对应的控制器
  4. 从控制器拿到请求的参数,转给业务层,在此进行处理并且可能会对数据进行一些持久化,最后生成相应的模型Model和逻辑视图名
  5. 控制器将Model和逻辑视图名返还给DispatcherServlet,再将其转交给视图解析器
  6. 视图解析器根据处理结果中的逻辑视图名(View Name),将其解析为具体的视图对象。
  7. 视图渲染后的响应内容返回给浏览器端

前后端分离的场景就直接返回json格式串,就没有页面渲染那些步骤了。

Spring MVC获取参数的几种方式

@Slf4j注解是谁提供的

lombok提供的。

Model

Model和thymeleaf的关系,thymeleaf是负责渲染页面的,model是渲染页面的输入,model提供动态的一些输入数据。注意Model属性会复制到Servlet Request属性中,这样视图中就可以使用它们用于渲染页面。

处理表单请求

302状态码就是重定向。浏览器得到这个响应之后拿出返回的url再请求服务端。

Spring Data JDBC、JPA

三种方式:

  1. 基于JDBCTemplate模板
  2. Spring Data JDBC
  3. Spring Data JPA

重点分析三个例子的区别。

特点 JdbcTemplate Spring Data JDBC JDA
实现具体类 需要 不需要,只要写明继承关系 不需要,只要写明继承关系
定义实体类和数据库表的映射关系 不需要 需要 需要
程序员维护表之间的关系 需要 不需要 不需要
显式提供表结构(建表 SQL 脚本) 需要 需要 不需要,可以自动推断

使用 JdbcTemplate

参考代码 taco-cloud-jdbctemplate

特点:

  • 解决 RawJdbcIngredientRepository 样板式代码的问题,只需要提供查询逻辑;
  • 需要实现具体类 JdbcIngredientRepository 而其他两种方法不用;
  • 需要提供 src/main/resources/schema.sql 文件作为表结构的定义(建表脚本)。
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
	<groupId>com.h2database</groupId>
	<artifactId>h2</artifactId>
	<scope>runtime</scope>
</dependency>
@Repository
public class JdbcIngredientRepository implements IngredientRepository {

	private JdbcTemplate jdbcTemplate;

	public JdbcIngredientRepository(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	@Override
	public Iterable<Ingredient> findAll() {
		return jdbcTemplate.query(
				"select id, name, type from Ingredient",
				this::mapRowToIngredient);
	}

}

使用 Spring Data JDBC

参考代码 taco-cloud-jdbc

特点:

  • 需要定义实体类和数据库表的映射关系;
  • 不需要实现具体类,只需要写好继承关系;
  • 需要提供 src/main/resources/schema.sql 文件作为表结构的定义(建表脚本)。

CrudRepository<Ingredient, String>第一个类型对应查询的类,第二个对应id的类型

public interface IngredientRepository
		extends CrudRepository<Ingredient, String> {

}
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

@Data
@Table
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
public class Ingredient implements Persistable<String> {

	@Id
	private String id;

	private String name;
	private Type type;

	@Override
	public boolean isNew() {
		return true;
	}

	public enum Type {
		WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
	}

}

使用 JPA

参考代码 taco-cloud-jpa

特点:

  • 需要定义实体类和数据库表的映射关系;
  • 不需要实现具体类,只需要写好继承关系;
  • 依据实体类推断表结构,不需要建表脚本;
  • 可以自定义查询方法。

@Entity(来自javax.persistence.*)

/**
 * 原料仓库
 * 实体类型 Ingredient,ID 类型 String
 * @author EagleBear2002
 * @date 2023/04/07
 */
public interface IngredientRepository
		extends CrudRepository<Ingredient, String> {

}
import javax.persistence.Entity;
import javax.persistence.Id;

@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
public class Ingredient {

	@Id
	private String id;
	private String name;
	private Type type;

	public enum Type {
		WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
	}

}
@Data
@Entity
public class Taco {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	@NotNull
	@Size(min = 5, message = "Name must be at least 5 characters long")
	private String name;

	private Date createdAt = new Date();

	@Size(min = 1, message = "You must choose at least 1 ingredient")
	@ManyToMany()
	private List<Ingredient> ingredients = new ArrayList<>();

	public void addIngredient(Ingredient ingredient) {
		this.ingredients.add(ingredient);
	}

}

自定义的查询方法

定义查询方法,无需实现:

  • 领域特定语言(domain specific language DSL)spring data 的命名约定
  • 查询动词 + 主题 + 断言
  • 查询动词: get 、 read 、 find 、 count
  • 例子:
List<TacoOrder> findByDeliveryZip( String deliveryZip );

声明自定义查询(JDQL 面向对象查询语言):

不符合方法命名约定时,或者命名太长时

@Query("Order o where o.deliveryCity = 'Seattle'")
List<TacoOrder> readOrdersDeliveredInSeattle( );

Jpa、Hibernate、Spring Data Jpa 三者之间的关系

  • JPA 的宗旨是为 POJO 提供持久化标准规范;
  • Hibernate 作为厂家实现了这一规范;

Spring Data Mongodb、Redis

image-20240101193956770

MongoDB

文档存储一般用类似json的格式存储,存储的内容是文档型的。

Redis

Redis是key-value结构,value是某数据结构。内存数据库,主要用于缓存。通常部署在多台机器上做集群,冗余存储 + 加快读取速度。Redis是区分大小写的。

Redis数据类型

Jedis和Lettuce

RedisConnectionFactory接口配置了ip、端口、用户名、密码等信息。

序列化

不同于mongodb,redis需要提供序列化器。否则存入redis中的是字节码,可读性很差。

Spring Security

划分成两类权限控制。一是外部请求,外部的url的控制。二是方法级别的权限控制。

Cookie,有时也用其复数形式 Cookies。类型为“小型文本文件”,是某些网站为了辨别用户身份,进行Session跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息。

服务端和浏览

Spring Security当访问网站的时候请求头没有JSESSIONID时,就跳转到登录页面。(第一次访问时的处理)

(重要)Spring Security对Web请求拦截

在请求被处理之前会被若干过滤器拦截。

下面这张图很重要。开发人员需要做什么。

  1. UserDetailsService在Spring里就是一个bean对象,给Spring框架提供用户的详细信息。
  2. 实现PasswordEncoder,密码编码器,也是需要我们创建一个bean对象。
  3. 实现用户登录页面。
  4. 调用HttpSecurity进行权限设置。两种实现方式,一是放进SecurityFilterChain这个bean中,二是通过继承WebSecurityConfigurerAdapter。

在SecurityConfig的configure方法中这样修改,显示指定username和password读取的参数,就可以改变使用的表单中传输的参数,原先用来作为密码的password参数可以用于其他作用。(也就是当我不想用username和password作为参数名时这么改)

启用HTTP Basic认证

CSRF

image-20231016204211622
  1. 用户输入账号信息请求登录A网站。
  2. A网站验证用户信息,通过验证后返回给用户一个cookie
  3. 在未退出网站A之前,在同一浏览器中请求了黑客构造的恶意网站B
  4. B网站收到用户请求后返回攻击性代码,构造访问A网站的语句
  5. 浏览器收到攻击性代码后,在用户不知情的情况下携带cookie信息请求了A网站。此时A网站不知道这是由B发起的。那么这时黑客就可以进行一下骚操作了!

两个条件:a 用户访问站点A并产生了cookie

b 用户没有退出A同时访问了B

使用disable可以取消。ignoringAntMatchers可以对于某些服务不用生成csrf验证。

在内存当中存取一些用户

两种写法在内存中存取一些用户。

实现方法级别的安全

config类上要加一个@EnableGlobalMethodSecurity

@PreAuthorize

本质是AOP技术。

获取当前登录的用户

配置属性

属性来源

YAML文件

不允许使用Tab键,只允许使用空格

建立HTTPS安全通道

配置日志

在level可以配置包路径下的日志级别。

自定义配置属性

四个来源:profile、命令行、jvm属性、操作系统环境变量

@ConfigurationProperties(prefix="taco.orders")注解在Configration、Bean上

taco:
    orders:
        pageSize: 20

@Value("${user.name}")注解在变量上

user:
    name: zhangsan

还可以是数组,Map, @Value("${tools}") private String[] toolArray;

tools: car,train,airplane

Spring Profile

application-{profile名}.yml 同一个yml文件通过3个短线进行分隔多个profile,spring.profiles属性命名profile

spring:
    profiles:
        active: dev
---
spring:
    profiles: dev
server:
    port: 8081
---
spring:
    profiles: prod
server:
    port: 8082

激活profile的方式:

  1. application.yml中指定spring.profiles.active
  2. 命令行参数:--spring.profiles.active=dev
  3. jvm参数:-Dspring.profiles.active=dev
  4. @Profile注解(用在@Configration和@Bean上

Actuator

分布式系统的配置数据来源

Spring Cloud Config Server: 集中管理配置,通过git仓库管理配置文件,客户端通过http请求获取配置

REST API

REST原则(重要)

加了@RestController或@ResponseBody则控制器返回客户端的就是json格式串,而之前MVC时候@Controller返回的是一个视图名。

@RequestMapping的produces属性的作用是用于索取Response的格式

响应头与响应体

201状态码:服务端创建了一个新的资源。

接口设计(重要)

Client

RestTemplate: 用于发送HTTP请求,访问REST API

@Bean
public RestTemplate rest() {
    return new RestTemplate();
}
public class TacoCloudClient {
    private RestTemplate rest;
    private URI baseUrl;
    
    public TacoCloudClient(String url) {
        this.baseUrl = URI.create(url);
        this.rest = new RestTemplate();
    }
    
    public Ingredient getIngredientById(String ingredientId) {
        return rest.getForObject(baseUrl + "/ingredients/{id}", Ingredient.class, ingredientId);
    }
    
    public Ingredient addIngredient(Ingredient ingredient) {
        return rest.postForObject(baseUrl + "/ingredients", ingredient, Ingredient.class);
    }

    public Ingredient getIngredientById(String ingredientId) {
        ResponseEntity<Ingredient> responseEntity = rest.getForEntity(baseUrl + "/ingredients/{id}", Ingredient.class, ingredientId);
        log.info("Fetched time: " + responseEntity.getHeaders().getDate());
        return responseEntity.getBody();
    }

    //exchage: 底层方法,可以设置请求头
}

OAuth2

授权码授权模式(必考)

9步:

  1. 用户和客户端程序交互
  2. 如果未登录,返回状态码302和重定向的redirect_url,浏览器重定向到授权服务器
  3. 授权服务器向用户询问是否同意授权
  4. 用户同意授权
  5. 授权服务器重定向回到客户端程序,并且携带授权码
  6. 客户端用授权码向授权服务器请求access token
  7. 客户端向资源服务器请求资源(携带access token),为了确定签名的合法性,资源服务器会去向授权服务器索取公钥验证签名的合法性。
  8. 资源服务器返回资源
  9. 客户端向用户响应

  1. 授权服务器:所有的授权都在这边进行。需要我们自己实现。
  2. 资源服务器:是我们需要保护的。
  3. 客户端应用:消费资源服务器上的Rest API。第三方的应用。
  4. 用户

授权服务器有公钥和私钥,资源服务器向授权服务器获取公钥,验证json web token(jwt)的合法性。

第5步url参数中先返回一个code,第6步客户端再去向授权服务器交换token。

问题:为什么不直接返回token?

中间跳转很多,但是客户端后台的密码一直没有在浏览器间传来传去。后端服务器和授权服务器共享的secret是不会出现在浏览器,因此就算黑客从浏览器的response截获了code,也没有办法获取token。token一直只存储在客户端服务器中,从未出现在浏览器中。

可以看到授权服务器配置了clientSecret。

客户端后台在配置文件中也配置了client-secret。使用第三方的时候,你注册过oauth服务就会给你这个secret。

其他模式

消息中间件

broker

基于jms编程

需要注意的东西有ConnectionFactory、Connection、Session、Destination。这些是Jms规范定义的。

需要注意连接工厂用的是Activemq实现的工厂。

ActiveMQ现在也支持AMQP(Advanced Message Queuing Protocol, RabbitMQ也支持此协议),MQTT(Message Queue Telemetry Transport)协议

消息转换器(MessageConverter)

typeid告诉了对方这是什么类型,以便于反序列化。

SimpleMessageConverter: 只支持基本类型,如String、Integer、Map、List等 MappingJackson2MessageConverter: 支持JSON格式,需要添加依赖(常用)

消息包括消息头、属性集和消息体

发送消息

@Autowired
private JmsTemplate jmsTemplate;    //添加依赖后自动注入

@Override
public void sendOrder1(Order order) {
    jmsTemplate.convertAndSend("tacocloud.order.queue", order, this::addOrderSource);   //convertAndSend(destination, message, messagePostProcessor) 消息将被序列化,序列化逻辑在Config中通过@Bean注入Converter
}
//如果不提供Destination,则需要在application中配置spring.jms.template.default-destination

//处理消息
private Message addOrderSource(Message message) throws JMSException {
    message.setStringProperty("X_ORDER_SOURCE", "WEB");   //设置消息属性
    return message;
}

@Override
public void sendOrder2(Order order) {
    jmsTemplate.send(session -> {
        Message message = session.createObjectMessage(order);
        message.setStringProperty("X_ORDER_SOURCE", "WEB");
        return message;
    });    //比较原始
}

接收消息:拉取模式

需要注意无论是拉取还是推送模式都需要消息转换器。

接收消息:推送模式

RabbitMQ

支持的协议是AMQP协议。同样需要消息转换器,不过所属的包是不同的。

要使用先要去rabbit控制台创建交换机、Queue和binding(绑定关系)。routing key和binding key的概念在jms中是没有的!!!

  • ConnectionFactory、Connection、Channel(对应JMS的Session)
  • Exchange(交换机):Default、Direct(根据routing key路由到对应Binding的Queue)、Topic、Fanout(广播到所有绑定的Queue)、Headers、Dead letter
  • Queue
  • routing key(来自Sender,消息转发到对应Binding key的Queue)
  • Binding key(Queue绑定到交换机的key)

image-20240103092949243

Spring Integration

解决什么问题(重要)

解决企业在各个系统之间的交互。

EIP(Enterprise Integration Patterns,企业集成模式)

集成流(integration flow)

gateway是集成流的入口,adapter是与外部系统的交界。

  • Channels(通道):把消息从一个组件传到另一个
  • Filters(过滤器):判断消息能否进入下一个通道
  • Transformers(转换器):转换消息(值和类型)
  • Routers(路由器):多个输出通道,判断消息被路由到哪个通道
  • Splitters(切分器):把一个消息切分为多个消息输出到多个通道(内置一个路由器)
  • Aggregators(聚合器):把来自多个流的消息聚合为一个消息输出到一个通道
  • Service activators(服务激活器):消息会激活一个服务(Handler),可以只有输入通道(集成流到此结束),也可以有输出通道,消息会继续向下流动
  • Channel adapters(通道适配器):与外部系统交界,Inbound/Outbound
  • Gateways(网关):消息从外部应用进入集成流的入口,双向网关可以从返回通道得到一个消息

集成流配置

  1. XML配置:定义Gateway接口+编写XML文件
  2. Java配置:定义Gateway接口+transformer+fileWriter
  3. DSL(domain specific language)配置:Gateway接口+IntegrationFlows类
@Bean
public IntegrationFlow fileWriterFlow() {
    return IntegrationFlows
        .from(MessageChannels.direct("textChannel"))
        .<String, String>transform(t -> t.toUpperCase())
        .handle(
            Files
            .outboundAdapter(new File("."))
            .fileExistsMode(FileExistsMode.APPEND)
            .appendNewLine(true)
        )
        .get();
}

反应式编程基础

Reactive Programming: 反应式编程,异步非阻塞编程,基于事件驱动,数据流动,数据变化时通知订阅者

命令式 vs 反应式

反应式声明了一个流,流里面有很多环节,每一个环节需要做处理。

解决什么问题

  1. IO密集型
  2. 同步阻塞式编程:一个请求一个线程,线程池线程有限,线程切换开销大
  3. 管理多线程意味着更高的复杂度

IO的典型特点就是会有长时间的等待(延迟)。

Reactor项目(比较重要)

  • Reactive Streams: 制定于2013年的一种规范,旨在提供无阻塞回压的异步流处理标准,定义了异步流处理的接口,规范了流处理的行为,
  • Reactor: 一个实现了Reactive Streams规范的库,提供了异步流处理的实现,提供了Flux和Mono两个类,Flux表示的是包含0到N个元素的异步序列,Mono表示的是包含0或者1个元素的异步序列
  • Spring WebFlux: 基于Reactor实现,提供了基于反应式编程的Web编程框架,提供类似于MVC的注解编程模型

Reactive的stream和jdk的stream是不一样的。

jdk的流是同步的,反应式的流是异步的。

jdk9也提供了反应式流,叫Flow API。

反应式流规范定义的4个接口(非常重要)

  • Publisher: 发布者,定义了如何生产数据流的接口,包含一个subscribe方法,接收Subscriber作为参数,当Publisher有数据流时,通过Subscriber的onNext方法传递给Subscriber
  • Subscriber: 订阅者,定义了如何消费数据流的接口,包含onSubscribe、onNext、onError、onComplete四个方法,onSubscribe方法接收Subscription作为参数,用于接收Publisher的订阅,onNext方法用于接收Publisher传递的数据,onError方法用于接收Publisher传递的异常,onComplete方法用于接收Publisher传递的完成信号
  • Subscription: 订阅,定义了如何控制数据流的接口,包含request和cancel两个方法,request方法用于请求Publisher传递数据,cancel方法用于取消订阅,一个Subscription对应一个Subscriber。相当于是协调者,协调订阅者和发布者。发布者有数据了就通过subscription告诉订阅者,订阅者通过subscription告诉发布者发送多少数据给它。
  • Processor: 处理器,继承了Publisher和Subscriber接口,既是Publisher也是Subscriber,用于转换数据流

两个概念:Flux Mono

  • Flux:包含 0 到 N 个元素的异步序列
  • Mono:包含 0 或者 1 个元素的异步序列
  • 消息:正常的包含元素的消息、序列结束的消息和序列出错的消息
  • 操作符(Operator):对流上元素的操作

操作类型

Flux和Mono的操作都是一样的,所以就用Flux举例了。

创建Flux

一个简单的创建和消费的例子。

下面的作用是进行一个订阅。

基于jdk的list和stream创建flux。

按照一定时间间隔生成flux。

组合Flux流

过滤Flux流

转换Flux流

flatmap返回出来的也是一个流,map返回的则是一个具体的数据。

flatMap与map相比转换出来的是一个Mono。flatMap可以使得每个处理并行处理(异步),并且你可以控制是否异步。由于flatMap是并发的处理,因此流上的顺序是不可控的。

逻辑操作

3类消息

目前学过3种消息:反应式编程的消息,Spring Integration的消息,消息队列rabbitmq等的消息。

区别:

rabbitmq因为消息是远程传输,所以一定需要给它序列化和反序列化。而其余两种都是内部的消息流动,所以不需要序列化和反序列化。Spring Integration的消息是有消息头和消息体的。

Spring WebFlux

异步Web框架的事件轮询(event looping)机制

Spring MVC与Spring WebFlux的共性与不同

端到端全部是反应式的。

REST API

与MVC架构类似

@GetMapping(value="/flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> produceFlux(){
    Flux<String> stringFlux = Flux.fromStream(IntStream.range(1,6).mapToObj(i->{
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(" in Supplier thread: " + Thread.currentThread().getName());
        return "java north flux:"+i+", date time: "+LocalDateTime.now();
    }));
    System.out.println("thread: " + Thread.currentThread().getName() + ", time:" + LocalDateTime.now());
    return stringFlux;
}

当发送请求时,后端终端将先打印"thread: " + Thread.currentThread().getName() + ", time:" + LocalDateTime.now(),因为Flux已经返回了,但还没触发operate

Server

HandlerFunction: 处理请求,返回Mono或Flux

RouterFunction: 路由,将请求映射到HandlerFunction(代替@RequestMapping注解)

route(RequestPredicate, HandlerFunction)

HandlerFunction(handle函数): ServerRequest -> Mono

@Component
public class HelloHandler {
    public Mono<ServerResponse> hello(ServerRequest request) {
        return ok().body(BodyInserters.fromValue("Hello World"));
    }
}
@Bean
public RouterFunction<ServerResponse> route() {
    return route(GET("/hello"), 
        request -> ok().body(BodyInserters.fromValue("Hello World"));
    ).andRoute(GET("/hello2").and(accept(MediaType.APPLICATION_JSON)), 
        request -> ok().body(just("Hello World"), String.class);
    ).andRoute(GET("/hello3"),
        helloHandler::hello;
    );
}

Client

WebClient: 发送请求,返回Mono或Flux,类似于RestTemplate

public class HelloClient {
    private final WebClient client;
    
    public HelloClient(WebClient.Builder builder) {
        this.client = builder.baseUrl("http://localhost:8080").build();
    }
    
    public Mono<String> hello() {
        return client.get().uri("/hello").accept(MediaType.APPLICATION_JSON)
            .retrieve().bodyToMono(String.class);
    }

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(HelloClient.class, args);
        HelloClient client = context.getBean(HelloClient.class);
        client.hello().subscribe(System.out::println);
    }
}

Test

WebTestClient: 测试请求,返回Mono或Flux

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloHandlerTest {
    @Autowired
    private WebTestClient client;
    
    @Test
    public void testHello() {
        client.get().uri("/hello").accept(MediaType.APPLICATION_JSON)
            .exchange()
            .expectStatus().isOk()
            .expectBody(String.class).isEqualTo("Hello World");
    }
}

Docker

什么是容器?

必考问答题:Docker的三部分

  1. Docker engine:又叫docker daemon,是最核心的部分,是docker的服务端,负责管理容器的生命周期、镜像、network、volume等等资源。
  2. client:客户端,是一个命令行程序,负责和docker daemon交互。
  3. registry:容器的镜像仓库,负责管理容器镜像。

docker run命令(重要)

需要记住一些参数

-p 端口号的顺序,冒号左边是宿主机的端口号,右边是容器的端口

-v 冒号左边是宿主机的目录,右边是容器的目录

-d 是在后端运行容器

-it 是命令行交互方式运行容器

如何查容器的ip地址?

容器中:cat /etc/hosts, ip a

推荐使用vscode的docker插件来进行docker开发管理,非常方便。

镜像分层

写时复制,只有在容器中修改了文件,才会在容器中创建新的文件层,否则使用镜像层。

docker history {image}: 查看镜像分层及生成过程

dangling images: 无标签的镜像

docker image prune: 清理无用镜像

docker container prune: 清理无用容器

三种挂载方式

如果是宿主机目录直接挂载,必须为绝对路径

docker run -v {hostDir}:{containerDir} {image}: 挂载目录

docker volume create {volume}: 创建卷

docker run -v {volume}:{containerDir} {image}: 挂载volume

tmpfs: 内存挂载,docker run --tmpfs {containerDir} {image}

Volume数据卷

tmpfs如果容器挂了你的数据就丢了。要想持久化可以用volume或bind mounts。

容器镜像构建与编排

Dockerfile文件的指令

重要的几个,FROM、ADD、COPY(要知道ADD和COPY的区别)

docker build -t {image}:{tag} {DockerfileDir}: 构建镜像

docker run -d -p {hostPort}:{containerPort} {image}:{tag}: 运行镜像

注意生成镜像指令还有最后一个参数 .

代表上下文的环境

镜像分层

感觉不重要

docker-compose常用命令

注意up、ps这些指令

需要注意docker-compose ps/images显示的是你compose编排的容器和镜像,不是整个docker环境的

k8s

复习的时候记住k8s调度的最小单元是Pod,还有Pod、ingress、service、deployment的资源(主要了解service和ingress,ingress不可以路由到pod,只能路由到service,service有一个集群ip,这个ip是不变的,我们可以通过集群ip或服务名访问到服务,注意pod的ip可能是变的,所以用service的ip)。还有一个autoscale命令需要了解。

k8s基本架构

Node就是指docker的宿主机,k8s在其中一个节点上部署k8s的master,这个节点就是master节点。余下的其他节点是提供容器的节点,所以需要在上面安装docker,这样就组成了集群的环境。

master节点我们需要装Scheduler和Controller,在Node上装kubelet和kube-proxy,通过kebulet和master节点交互。flannel是容器网络的一个第三方的实现,提供虚拟的网络。etcd是一个类似redis的缓存的系统,分布式系统需要一个集中的数据存储,因此使用etcd(同时它也可以是分布式的,做主从复制这些)。

k8s中的资源

Ingress相当于k8s的网关。

Pod

Pod是k8s调度的最小单元,k8s不是以容器为单元而是以Pod为单元。

service

这张图清晰的解释了service和pod的关系。

Ingress

相当于路由,指定域名映射到指定service的端口

deployment

deployment相比于run是一个声明式的部署。

想记录和查看历史版本在create和set的时候要加上--record参数。

  • 标题: 服务端开发复习
  • 作者: Kiyotaka Wang
  • 创建于 : 2024-01-11 09:12:32
  • 更新于 : 2024-01-15 12:59:56
  • 链接: https://hmwang2002.github.io/2024/01/11/fu-wu-duan-kai-fa-fu-xi/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
此页目录
服务端开发复习