SpringCloud

1.微服务的定义

随着互联网行业的发展对于服务的要求也越来越高,服务架构也从单体架构逐渐演变为微服务架构

1.1 三种架构的对比

  • 单体架构

image-20240316220448340

  • 分布式架构
image-20240316220536632
  • 微服务(优化版分布式架构)

微服务的架构特征:==可以认为微服务是一种经过良好架构设计的分布式架构方案==

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
  • 自治:团队独立、技术独立、数据独立,独立部署和交付
  • 面向服务:服务提供统一标准的接口,与语言和技术无关
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

image-20240316220844109

1.2 SpringCloud

SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。

SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。

其中常见的组件包括:

image-20240316221239275

此外,要考虑很多版本兼容问题。主要是因为SpringCloud底层是依赖于Springboot实现的:

image-20240316221342911

2.服务拆分和远程调用

2.1 服务拆分原则

  • ==什么时候拆分?==

image-20240528141616591

  • ==怎么拆分?==

image-20240528141659955

  • ==服务拆分原则==

​ 不同微服务,不要重复开发相同业务

​ 微服务数据独立,不要访问其它微服务的数据库

​ 微服务可以将自己的业务暴露为接口,供其它微服务调用

image-20240316222315019

2.2 远程调用原则

  • ==远程调用原则==

    image-20240317152606026

3.提供者与消费者

在服务调用关系中,会有两个不同的角色:

服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)

服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)

image-20240317152829239

但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言【==相对的==】

如果服务A调用了服务B,而服务B又调用了服务C,服务B的角色是什么?

  • 对于A调用B的业务而言:A是服务消费者,B是服务提供者
  • 对于B调用C的业务而言:B是服务消费者,C是服务提供者

因此,服务B既可以是服务提供者,也可以是服务消费者。

可能会出现三种情况:

1.消费者该如何获取服务提供者具体信息?

2.如果有多个服务提供者,消费者该如何选择?

3.消费者如何感知服务提供者健康状态?

4.Eureka注册中心

4.1 Eureka结构

image-20240417162755947

组成部分:分为eureka-servereureka-client[服务消费者和服务提供者]两个部分

基本步骤:

1.client向注册中心server注册服务信息(告知有哪些服务和端口信息) –每隔30s就重新注册

2.服务消费者从service拉取服务提供者信息

3.服务消费者自己负载均衡配置

4.服务消费者远程调用

==解决上述可能出现的三种情况==

问题1:order-service如何得知user-service实例地址?

获取地址信息的流程如下:

  • user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册
  • eureka-server保存服务名称到服务实例地址列表的映射关系
  • order-service根据服务名称,拉取实例地址列表。这个叫服务发现/服务拉取

问题2:order-service如何从多个user-service实例中选择具体的实例?

  • order-service从实例列表中利用负载均衡算法选中一个实例地址
  • 向该实例地址发起远程调用

问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?

  • user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳
  • 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除
  • order-service拉取服务时,就能将故障实例排除了

注意:一个微服务,既可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端

==分三步进行==

image-20240417164603638

4.2 搭建三步

==总体图==

image-20240421162352729

4.2.1 搭建注册中心

image-20240421161828967

4.2.2 服务注册

image-20240421161959360

4.2.3 服务发现

分为四个步骤:

1和2步骤类似于服务注册,只有spring.application.name= orderservice

image-20240421163002614

3和4步骤更新具体操作

image-20240421162629385

spring会自动帮助我们从eureka-server端,根据userservice这个服务名称,获取实例列表,而后完成负载均衡

5.Ribbon负载均衡

4部分添加@LoadBalanced注解即可实现负载均衡功能

==补充:以前SpringMVC默认是Ribbon负载均衡,后来默认是loadbalancer负载均衡==

5.1 负载均衡原理

其实就是我们发出的请求是http://userservice/user/1,根据负载均衡变成了http://localhost:8081

SpringCloud底层其实是利用了一个名字为==Ribbon组件==来实现负载均衡

image-20240417205259950

5.2 源码跟踪

image-20240417205452975

基本流程如下:

  • 拦截我们的RestTemplate请求http://userservice/user/1
  • RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
  • DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
  • eureka返回列表,localhost:8081、localhost:8082
  • IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
  • RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求

5.3 负载均衡策略(IRule接口)

5.3.1 负载均衡策略

负载均衡的规则都定义在==IRule接口==中,而IRule有很多不同的实现类:

image-20240417213754364

不同规则的含义如下:

内置负载均衡规则类 规则描述
RoundRobinRule 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。
AvailabilityFilteringRule 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule[默认] 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。[集群内优先]
BestAvailableRule 忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule 随机选择一个可用的服务器。
RetryRule 重试机制的选择逻辑

5.3.2 自定义负载均衡策略(消费者)

通过定义IRule实现可以修改负载均衡规则,有两种方式:

代码方式:在order-service中的OrderApplication启动类上定义一个新的IRule:

1
2
3
4
5
//启动类定义一个bean,放入ioc容器
@Bean
public IRule randomRule(){
return new RandomRule();
}

配置文件方式:在order-service的application.yml文件中,添加新的配置:

1
2
3
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡
  • 注意,一般用默认的负载均衡规则,不做修改。

5.4 饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

1
2
3
4
ribbon:
eager-load:
enabled: true
clients: userservice

6.Nacos

SpringCloudAlibaba推出了一个名为Nacos的注册中心,现在是SpringCloud中的一个组件,相比Eureka功能更加丰富,在国内受欢迎程度较高。

主要涉及nacos注册中心和nacos配置管理

6.1 Nacos结构

image-20240421180436014

6.2 搭建三步

6.2.1 搭建注册中心

  • 1.下载

在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码:

GitHub主页:https://github.com/alibaba/nacos

GitHub的Release下载页:https://github.com/alibaba/nacos/releases

  • 2.解压

image-20240421164955642

  • 3.端口配置

Nacos的默认端口是8848,如果你电脑上的其它进程占用了8848端口,请先尝试关闭该进程。

如果无法关闭占用8848端口的进程,也可以进入nacos的conf目录,修改配置文件中的端口:

image-20240421165044792

  • 4.启动nacos

image-20240421165146961

6.2.2 服务注册

image-20240421164336864

6.2.3 服务发现

和服务注册类似,导入nacos依赖

image-20240421165732561

6.3 服务分级存储模型

一个服务可以有多个实例,例如我们的user-service,可以有:

  • user-service:127.0.0.1:8081
  • user-service:127.0.0.1:8082
  • user-service:127.0.0.1:8083

假如这些实例分布于全国各地的不同机房,例如:

  • 127.0.0.1:8081,在上海机房
  • 127.0.0.1:8082,在上海机房
  • 127.0.0.1:8083,在杭州机房

Nacos就将同一机房内的实例 划分为一个集群

也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:

image-20240421170021358

微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:

image-20240421170136721

6.3.1user-service服务提供者配置集群

image-20240421170739795

6.3.2 order-service服务消费者配置集群

image-20240421171305421

6.4 Nacos负载均衡(NacosRule实现)

Ribbon默认的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。[Eureka默认是同一个zone区域轮询]

6.4.1 修改负载均衡规则

注意:==原来默认是集群间轮询,后来设定nacosrule之后是本地集群优先(本地集群又是随机的 –>可以设置权重提高访问频率),如果本地的都挂了那就会跨集群==

因此Nacos中提供了一个NacosRule的实现,可以优先从==同集群中挑选实例==

修改order-service服务消费者的application.yml文件,修改负载均衡规则:

image-20240421171140243

6.4.2 修改权重

服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。

但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。

因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。

image-20240421171620311

  • 注意:如果权重修改为0,则该实例永远不会被访问

==如果设备更新/版本升级,那我就可以将权重设置低一点然后少数用户测试,慢慢增大权重==

6.5 环境隔离

类似于rabbitMQ消息队列的virtueHost虚拟主机空间可以造成环境隔离

  • nacos中可以有多个namespace
  • namespace下可以有group、service等
  • 不同namespace之间相互隔离,例如不同namespace的服务互相不可见

image-20240421172511733

6.5.1 网页创建namespace

默认情况下,所有service、data、group都在同一个namespace,名为public:

image-20240421172717407

我们可以点击页面新增按钮,添加一个namespace:

image-20240421172854688

然后,填写表单:

image-20240421172907671

就能在页面看到一个新的namespace:

image-20240421172925184

6.5.2 服务消费者配置

给==微服务配置namespace==只能通过修改配置来实现

image-20240421173139937

6.6 Nacos服务实例

Nacos的服务实例分为两种类型:

  • 临时实例:【默认类型】如果实例宕机超过一定时间,会从服务列表剔除。

  • 非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。

配置一个服务实例为永久实例:

image-20240421173358174

6.7 Nacos和Eureka对比

  • Nacos与eureka的共同点

    • 都支持服务注册和服务拉取

    • 都支持服务提供者心跳方式做健康检测

  • Nacos与Eureka的区别

    Eureka Nacos
    客户端 ==搭建新的EurekaServer项目==<br 1.pom.xml引入xxx-client依赖
    2.启动类添加@EnableEurekaServer注解
    3.application.yml配置eureka地址
    ==启动app==
    1.父工程引入依赖
    2.pom.xml引入依赖
    服务提供者 1.pom.xml引入xxx-server依赖
    2.application.yml配置eureka地址
    在application.yml配置nacos信息[server-addr地址,cluster-name集群名]
    服务消费者 1.pom.xml引入xxx-server依赖
    2.application.yml配置eureka地址
    3.RestTemlate引入@LoadBalanced注解
    4.修改具体业务
    在application.yml配置nacos信息[server-addr地址和cluster-name集群名,namespace空间名,ephemeral实例类型]
    临时实例 心跳模式【默认每30s进行检测,不正常的会被剔除】 心跳模式【默认每30s进行检测,不正常的会被剔除】
    非临时实例 × 主动监测模式【不会被剔除】
    集群方式 AP方式 【默认】AP方式
    如果集群中存在非临时实例会变成CP模式
    服务列表变更的消息推送模式 定时拉取服务pull 主动推送变更消息push
    负载均衡策略[服务消费者] 【默认】随机
    【IRule】同Zone轮询
    【默认】集群内轮询
    【NacosRule】优先本地集群内 –> 修改权重还可以提高访问概率

6.8 Nacos配置管理

当微服务部署的实例越来越多[达到数百以上],逐步修改微服务配置就会让你抓狂。 –> 所以我们需要一种==统一配置管理方案==,可以集中管理所有实例配置

image-20240421230353712

Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新

6.8.1 统一配置管理

6.8.1.1 网站添加配置

image-20240421230927123

注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。

6.8.1.2 微服务拉取配置

微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动

但是如果未读取application.yml文件就不能得到nacos地址。【属于是卡bug呢】

–> 因此Spring引入一种新的配置文件:==bootstrap.yaml==文件【会在application.yml之前被读取】

流程如下:

image-20240421231210518

进行步骤:

image-20240423174007971

②引入nacos-config依赖
首先,在user-service服务中,引入nacos-config的客户端依赖:

1
2
3
4
5
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

③添加bootstrap.yaml

然后,在user-service中添加一个bootstrap.yaml文件,内容如下:

image-20240423174156884

④尝试读取nacos配置

在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置:

image-20240423174209359

6.8.2 配置热更新

最终目的是修改nacos中的配置后,微服务无需重启即可让配置生效,也就是配置热更新

  • 方式一:在@Value注入变量所在类上添加@RefreshScope注解

image-20240423173252661

  • 方式二:使用@ConfigurationProperties注解

image-20240423173338466

6.8.3 多环境配置共享

可能不同环境下有不同的yaml文件[像单体架构的时候properties,yml,yaml等情况],因此当出现相同属性时就有优先级:==名字越长越牛逼==

image-20240423173524235

6.9 搭建Nacos集群

==日后学习到了补充==

7.HTTP客户端Feign

之前使用的RestTemplate发起远程调用的代码:

image-20240423202621703

存在下面的问题:

•代码可读性差,编程体验不统一

•参数复杂URL难以维护

==Feign==是一个声明式的http客户端。其作用是帮助我们优雅地实现http请求发送,解决了上述的问题

7.1 使用操作

image-20240423211551585

7.1.1 导入依赖

image-20240423211732453

7.1.2 启动类添加注解

启动类上添加注解@EnableFeignClients

image-20240423211805626

7.1.3 编写子服务接口

这样可以相当于http://userservice/user/id

image-20240423211942883

7.1.4 实际操作

底层也有ribbon负载均衡,可以避免了访问ip地址的麻烦。Feign可以将调用步骤放在接口里面,这样使得我们看起来都是直接调用方法统一了。

image-20240423212526274

7.2 自定义配置

Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:

image-20240423213315647

【注意:一般我们需要配置的就是日志级别】

==下面以日志为例来演示如何自定义配置==

而日志的级别分为四种:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据

7.2.1 配置文件yml方式–日志

image-20240423213829442

7.2.2 Java代码方式–日志

image-20240423214701673

7.3 Feign使用优化

==Feign底层发起http请求,依赖于其它的框架==。其底层客户端实现包括:

  • URLConnection:[默认]不支持连接池

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

7.3.1 连接池优化

==由此可见,对于默认的URLConnection可以更改为Apache HttpClient或者OKHttp方式==

以HttpClient为例:

①pom.xml文件引入依赖

1
2
3
4
5
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

②yml配置文件

1
2
3
4
5
6
feign:
httpclient:
enabled: true # 开启feign对HttpClient的支持
#线程池的核心值需要压测和实际情况调整!!!!!!!!!!!1
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数

7.3.2 日志级别优化

==日志级别最好是用basic或者none==

7.4 最佳实践方案

像7.1使用操作里面用feign替换掉RestTemplate

image-20240423233327633

这种情况下会发现usercontroller和feign客户端代码相似,因此可以提出继承方式和抽取方式

7.4.1 继承方式

一样的代码可以通过继承来共享:

1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。

2)Feign客户端和Controller都集成该接口

image-20240423233410310

优点:

  • 简单
  • 实现了代码共享

缺点:

  • 服务提供方、服务消费方紧耦合

  • 参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解

7.4.2 抽取方式

将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign日志配置都放到这个模块中,提供给所有消费者使用。

例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。

image-20240423233624757

举例:

image-20240423234056492

8.Gateway服务网关

Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API 路由管理方式

Gateway网关是我们服务的守门神,==所有微服务的统一入口==

网关的核心功能特性:

  • 1.请求路由和负载均衡:一切请求都必须先经过gateway,只根据某种规则把请求转发到某个微服务。如果转发的目标服务有多个时就需要负载均衡
  • 2.权限控制:通过拦截器校验用户是否有请求资格,没有就进行拦截
  • 3.限流:当请求流量过高时,网关中按照下流的微服务能够接受的速度放行请求,避免服务压力过大

image-20240424170653509

在SpringCloud中网关的实现包括两种:

  • gateway【基于Spring5提供的WebFlux,属于响应式编程】
  • zuul【基于Servlet实现,属于阻塞式编程】

Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能

8.1 Gateway快速入门

演示下网关的基本路由功能。基本步骤如下:

  1. pom.xml导入依赖
  2. application.yml配置基础配置和路由规则
  3. 启动测试

8.3.1 导入依赖

1
2
3
4
5
6
7
8
9
10
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖,也是一个微服务需要注册到Nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

8.3.2 编写规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
server:
port: 10010 #网关端口号
spring:
application:
name: gateway #gateway名称
cloud:
nacos:
server-addr: localhost:8848 #nacos地址
gateway:
routes:
#第一个
- id: user-service #路由表示,必须唯一
uri: lb://userservice #路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: #路由断言,判断是否符合规则
- Path=/user/** #路径断言,判断路径是否以/user开头 --> /user/**转为lb://userservice

#请求头过滤器
#添加一个头结点 --> {Truth,Itcast is freaking awesome!}
filters:
- AddRequestHeader=Truth,Itcast is freaking awesome!
#第二个
- id: order-service #路由表示,必须唯一
uri: lb://orderservice #路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: #路由断言,判断是否符合规则
- Path=/order/** #路径断言,判断路径是否以/order开头 --> /order/**转为lb://orderservice

#默认过滤器
default-filters: # 默认过滤项
- AddRequestHeader=Truth,Itcast is freaking awesome!

#跨域问题
globalcors:
add-to-simple-url-handler-mapping: true #解决options请求被拦截问题
cors-configurations:
'[/**]': #拦截一切请求
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期

总结:

1
2
3
4
路由id:路由唯一标示
uri:路由目的地,支持lb和http两种
predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地
filters:路由过滤器,处理请求或响应

8.3.3 启动测试

image-20240424171458156

8.2 路由参数3-断言工厂—–==判断是否符合规则==

我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件

例:Path=/user/**是按照路径匹配,这个规则由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理

==要符合条件才能访问转换匹配==

8.2.1 断言工厂分类

从这张图可以看出来可以有很多判断情况来匹配转换,可以通过访问时间,访问ip,访问范围,访问参数等等

image-20240424172200496

8.3 路由参数4-过滤器工厂—–==请求时添加信息==

GatewayFilter是网关中提供的一种过滤器,可以对进入==网关的请求==和==微服务返回的响应==做处理:

image-20240424172659481

8.3.1 路由过滤器种类

image-20240424172758297

8.3.2 请求头过滤器

以AddRequestHeader 为例来讲解:

需求:给所有进入userservice的请求添加一个请求头:Truth=itcast is freaking awesome!

image-20240424173001746

8.3.3 默认过滤器

对所有的路由都生效,需要写到default下面

image-20240424173035471

8.3.4 全局过滤器

需求:自定义拦截请求,前端访问时必须有一个authorization=admin才可以

  • 实现GlobalFilter接口

image-20240424173746934

在filter中编写自定义逻辑,可以实现下列功能:

  • 登录状态判断
  • 权限校验
  • 请求限流等

8.3.5 执行顺序总结

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter

请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)[==适配器模式]==中,排序后依次执行每个过滤器:

==原因:可以合并是因为路由过滤器和默认过滤器在yml配置的是范围不同,但是底层都是GatewayFilter同一类。而全局过滤器实现GatewayFilter,内部是适配成GatewayFilter==

==总结图:==

image-20240424174011858

  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
  • GlobalFilter过滤器的顺序: 实现Ordered接口/@Order注解,由我们自己指定[@order(数字)]
  • 路由过滤器和defaultFilter的顺序: 由Spring指定,默认是按照声明顺序从1递增[yml文件的书写顺序]。
  • 当三大类过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行

详细内容,可以查看源码:

org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。

org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链

8.4 跨域问题

==跨域==:域名不一致就是跨域,主要包括:

跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题

解决方案:

8.4.1 Gateway配置

在gateway服务的application.yml文件中,添加下面的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
cloud:
gateway:
# 解决跨域问题
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期

8.5 Gateway流程图

image-20240424172300811

发送请求后,可以根据过滤器加一些修改,可以增加头拦截[过滤器]。然后http://localhost:10010/user/1根据断言工厂的断言路径判断/user/**就找到lb:/userservice 就去负载均衡发送给http://localhost:xxxx/user/1

如果是访问orderservice的话在上述过程之后还会使用feign发送请求,feign里面配置的去找userservice/user/id

8.6 yml配置文件详解

image-20240607111353328

9.Springcloud总结

image-20240424175219756

1.服务注册:将网关,userservice,orderservice这些微服务要注册到nacos注册中心

2.服务配置:nacos进行配置管理

3.网关配置和Feign配置:

4.访问转换路径:我们现在访问一个http://localhost:10010/order/101?authorization=songyaxiang,网关这时候分为两个步骤:①经过路由要对符合路由规则的order/**转换为配置的lb或者http:/orderservice微服务去,由于lb还要考虑负载均衡,②要对输入和输出的信息进行修改【可能是新增/删除一些信息】,这时候要考虑过滤器的优先级;之后我们进入orderservice微服务访问使用RestTemplate【会很死板,出现Ip地址信息】/Feign【不会出现IP地址等,选择这个】发送http请求给userservice微服务;最终将结果返回

JUC

1.并发和并行

目前CPU运算速度已经达到百亿次每秒,甚至更高的量级,家用电脑维持操作系统正常运行的进程会有数十个,线程更是数以百计。

所以在现实常见场景中,为了提高生产率和高效地完成任务,处处均采用==多线程==和==并发==的运行方式。

  • ==并发(Concurrency)== 某个时间段内,多任务交替处理的能力

每个CPU将 –(可执行时间)均匀分成若干份–> 每个进程执行一段时间后,记录当前工作状态,释放相关执行资源进入等待状态,让其他进程抢占CPU资源

  • ==并行(Parallelism)== 同时处理多任务的能力

目前CPU已经是多核(可以同时执行多个互不依赖的指令及执行块)

以KTV唱歌为例: 核心在进程是否同时执行

并发指的是同一个话筒被多人轮流使用;

并行指的是有多少人可以使用话筒同时唱歌;

以医生坐诊为例: 并发和并行的

2.线程安全

3.

Apache-Echarts

1.介绍

Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表

官网地址:https://echarts.apache.org/zh/index.html

image-20240109171236231

2.入门案例

1.在 https://www.jsdelivr.com/package/npm/echarts 选择 dist/echarts.js,点击保存为 echarts.js 文件

image-20240109172602826

2.在保存echarts.js文件的目录下创建一个index.html文件,内容如下:

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="echarts.js"></script>
</head>
</html>

3.绘制一个简单图标 –body标签里面添加div块

1
2
3
4
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
</body>

4.通过script标签里面初始化echarts实例并且通过setOption方法生成xxx图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));

// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};

// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>

结果如下:

image-20240109173358917

5.完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECharts</title>
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="echarts.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));

// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};

// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>
</html>

6.具体解析

image-20240109173227149

前端option的大致对比:

image-20240109173517633

==后端只需要看看前端要什么==

微信小程序开发

1.小程序介绍

小程序是一种新的开放能力,开发者可以快速地开发一个小程序。可以在微信内被便捷地获取和传播,同时具有出色的使用体验

官方网址:https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zh_CN

image-20240106154758794

小程序主要运行微信内部,可通过上述网站来整体了解微信小程序的开发

2.准备工作

开发微信小程序之前需要做如下准备工作:

  • 注册小程序
  • 完善小程序信息
  • 下载开发者工具

1). 注册小程序

注册地址:https://mp.weixin.qq.com/wxopen/waregister?action=step1

image-20240106154849701

2). 完善小程序信息

登录小程序后台:https://mp.weixin.qq.com/

两种登录方式选其一即可

image-20240106154908699

完善小程序信息、小程序类目

image-20240106154921416

查看小程序的 AppID

image-20240106154938792

3). 下载开发者工具

下载地址: https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html

3.入门案例

3.1 创建小程序项目

image-20240106155417633

3.2 小程序目录结构

3.2.1 开发界面(五个部分)

3.2.2 小程序整体目录结构(三个部分)

image-20240106163632628

3.2.3 每个页面结构(四个部分)

image-20240106163848831

3.3 具体代码

  • 进入到index.wxml,编写页面布局
image-20240106164109283
  • 进入到index.js,编写业务逻辑代码
image-20240106164345127

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// index.js
Page({
data:{
msg:'hello world',
nickname: '',
url:''
},

//获取微信用户头像和昵称
getUserInfo(){
//调用weixin内置方法
wx.getUserProfile({
desc: '获取用户信息',
success: (res) =>{
//调试器输出
console.log(res.userInfo)
//为数据赋值
this.setData({
nickname:res.userInfo.nickName,
url:res.userInfo.avatarUrl
})
}
})
},

//获取微信用户的授权码
wxLogin(){
//调用weixin内置方法
wx.login({
desc: '获取微信用户授权码',
success: (res) =>{
//调试器输出
console.log(res.code) //每次登录的授权码不一样(只能使用一次)
//为数据赋值
this.setData({
code:res.code
})
}
})
},

//发送请求
sendRequest(){
//调用weixin内置方法
wx.request({
desc: '获取用户端查询店铺运营状态',
url: 'http://localhost:8080/user/shop/status', //调用后端的接口!!!!!!!!
method: 'GET',
success: (res) =>{
//调试器输出
console.log(res.data) //相应过来的整体json数据

}
})
}

})
  • 进行调试 -点击编译按钮

    image-20240106164444992

  • 一定要先打开后台程序,然后在模拟器中点击

    image-20240106164648572

3.4 发布小程序

image-20240106164811098

八股文整理

1.Java基础

1.1 Java语言有哪些特点

  • 1.==简单易学==(简化版C++)

  • 2.==面向对象==【封装(就是可以通过权限修饰符将属性设置权限,这样只能通过get/set方法获取),继承(父类和子类之间具有继承关系,子类可以继承父类的方法和属性,并且在此基础上进行修改),多态(编译看左边,运行看右边)】

  • 3.==平台无关性==(Java虚拟机实现平台无关性)

  • 4.==支持多线程==(Java支持多线程)

  • 5.==可靠性==(Java具备异常处理和自动内存管理机制)

  • 6.==安全性==(Java提供权限修饰符,限制程序直接访问操作系统资源)

  • 7.支持==网络编程==

  • 8.==跨平台性==:只要操作系统安装jvm就可以运行java程序 (一次编写,随处运行)

1.2 Java三个版本的比较

image-20240105211658267

  • ==Java SE(Java Platform,Standard Edition)==: Java 平台==标准版==,Java 编程语言的基础,它包含了支持 Java 应用程序开发和运行的核心类库以及虚拟机等核心组件。Java SE 可以用于构建==桌面应用程序== / ==简单的服务器应用程序==

  • ==Java EE(Java Platform, Enterprise Edition )==:Java 平台==企业版==,建立在 Java SE 的基础上,包含了支持==企业级应用程序开发==和部署的标准和规范(比如 Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS)。 Java EE 可以用于构建分布式、可移植、健壮、可伸缩和安全的服务端 Java 应用程序,例如 Web 应用程序

  • ==Java ME== 是 Java 的微型版本,主要用于开发==嵌入式消费电子设备的应用程序==,例如手机、PDA、机顶盒、冰箱、空调等。Java ME 无需重点关注,知道有这个东西就好了,现在已经用不上了。

简单来说,Java SE 是 Java 的基础版本,Java EE 是 Java 的高级版本。Java SE 更适合开发桌面应用程序或简单的服务器应用程序,Java EE 更适合开发复杂的企业级应用程序或 Web 应用程序

1.3 JVM & JDK & JRE 三者对比

image-20240105212044287
  • ==JDK==: Java程序开发工具包=jre+开发人员使用的工具(例如javac编译工具)
  • ==JRE==: Java程序运行时环境=jvm+运行时需要的核心类库 [有了jre其实就可以运行代码]
  • ==JVM==: 运行Java字节码的虚拟机,JVM针对不同系统有特定实现(使用相同的字节码.class,得出相同结果) –> 一次编译,随处可以运行

    image-20240105213023670

1.4 字节码(.class文件)

==字节码==:JVM可以理解的代码,不面向任何特定的处理器,只面向虚拟机JVM。

  • Java程序执行过程:
image-20240105213658490

引入==JIT编译器(运行时编译)==,完成第一次编译之后就会将字节码对应的机器码保存下来,下次可以直接使用。机器码效率 > java解释器,所以Java是编译和解释共存的语言

image-20240105213914539

HotSpot 采用了==惰性评估(Lazy Evaluation)==的做法,根据==二八定律==,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。

JDK、JRE、JVM、JIT 这四者的关系如下图所示

image-20240105214120303

1.5 Java语言是”编译和解释并存”

这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。

因为 Java 程序要经过==先编译,后解释==两个步骤,Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行

1.6 Java和C++区别

Java 不提供指针来直接访问内存,程序内存更加安全

Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。

Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。

C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。

1.7 Java三种注释

  • ==单行注释== 通常用于解释方法内某单行代码的作用
  • ==多行注释== 通常用于解释一段代码的作用
  • ==文档注释(Java特有)== 通常用于生成 Java 开发文档
image-20240105215518448

注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码(.class)中不保留注释)

代码的注释不是越详细越好。实际上好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。

若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述

1.8 Java结构和格式

  • Java结构和语法格式
1
2
3
4
5
6
7
8
9
10
权限修饰符 class 类名{
权限修饰符 方法1(){ //每一级都需要缩进 每一级都需要{}配对
语法1;
语法2;
}
权限修饰符 方法2(){
语法1;
语法2;
}
}
  • Java程序入口 –main方法
1
2
3
4
5
6
7
8
9
public static void main(String[] args){
//解释说明:
public:公共的,用它修饰的类或成员在任意位置可见 [权限修饰符]
static:静态的,用它修饰的方法,可以不用创建对象就可以调用 [标识了静态就可以调用静态和非静态的,但是非静态的只能调用非静态的]
void:表示该方法没有返回值 [return;即可]
main:Java的主方法名,JavaSE的程序入口
String[]:字符串数组,这是main方法的形参类型,可以通过命令行参数传值
args:这是main方法的形参名,如果要在main中使用命令行参数,可以遍历该args数组。
}
  • Java程序输出
1
2
3
4
5
6
//有两种情况
//1.换行 --输出内容(可以忽略)
System.out.println(输出内容);

//2.不换行 --输出内容(不可以忽略)
System.out.print(输出内容);
  • 源文件名和类名

    • ==一致性==:

      如果一个类写了很多类,==源文件名(.java)必须和public的class类名一致==(不一致会编译报错),可以和其他类不一致(不便于代码维护)

    • ==多样性==:

      一个源文件可以有多个类,但是一个源文件只有一个public类

1.9 标识符和关键字

  • ==标识符==:只要你自己要定义的都可以叫做标识符
    • 区别大小写
    • 长度无限制
    • 只能由这四种组成:
      • ①26个英文字母 (可以当做开头)
      • ②数字0-9 【如果可做开头的话就不知道指代数字本身还是变量所对应的值 int 123L=12; long l=123L; 这样的话l最后是123还是12】
      • ③ 下划线_ (可以当做开头)
      • ④ 美元符号$ (可以当做开头)
    • 可以包含关键字和保留字,但是不可以使用
    • 四种情况的命名规范:
      • ①包名 —所有的字母都小写 — java.lang com.at.bean
      • ②类名/接口名 —所有的单词首字母大写 —ShopController
      • ③变量名/方法名 —第一个单词的首字母小写,后面的单词首字母都大写 —getById()
      • ④常量名 —所有的字母大写,中间用下划线连接 — MAX_SHOP_VALUES
  • ==关键字==: 专门用途的字符串/单词
    • 全部为小写,一共有50个(const和goto是保留字,true和false和null是字面量)
image-20240105221810897

其中,default可以在程序控制,可以当做修饰符,还可以设置为访问权限

在程序控制中,当在 switch 中匹配不到任何情况时,可以使用 default 来编写默认匹配的情况。

在类,方法和变量修饰符中,从 JDK8 开始引入了默认方法,可以使用 default 关键字来定义一个方法的默认实现。

在访问控制中,如果一个方法前没有任何修饰符,则默认会有一个修饰符 default,但是这个修饰符加上了就会报错。

1.10 自增自减运算符

1.11 移位运算符

1.12 阿松大

Mybatis

1.Mybatis入门

1.1 Mybatis定义

  • MyBatis是一款优秀的 持久层 框架,用于简化JDBC的开发。

  • MyBatis本是 Apache的一个开源项目iBatis,2010年这个项目由apache迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。

  • 官网:https://mybatis.org/mybatis-3/zh/index.html

在上面我们提到了两个词:一个是持久层,另一个是框架

  • 持久层:指的是就是数据访问层(dao),是用来操作数据库的
image-20231223160804125
  • 框架:是一个半成品软件,是一套可重用的、通用的、软件基础代码模型。在框架的基础上进行软件开发更加高效、规范、通用、可拓展

1.2 入门程序分析

现在使用Mybatis操作数据库,就是在Mybatis中编写SQL查询代码,发送给数据库执行,数据库执行后返回结果

image-20231223161034687

Mybatis会把数据库执行的查询结果,使用实体类封装起来(一行记录对应一个实体类对象)

image-20231223161114930

1.3 入门程序实现步骤

  1. 准备工作(创建springboot工程、数据库表user、实体类User)
  2. 在pom.xml中引入Mybatis的相关依赖,
  3. yml文件中配置Mybatis(数据库连接信息)
  4. mapper层编写SQL语句(注解/XML)

1.3 解决mapper层编写sql语句不提醒

image-20231223161415812

1.4 JDBC

1.4.1 介绍

通过Mybatis可以很方便的进行数据库的访问操作。但是大家要明白,其实java语言操作数据库呢,只能通过一种方式:使用sun公司提供的 JDBC 规范

Mybatis框架,就是对原始的JDBC程序(Java语言操作关系型数据库的一套API)的封装

image-20231223161912481

本质:

  • sun公司官方定义的一套操作所有关系型数据库的规范,即接口。

  • 各个数据库厂商去实现这套接口,提供数据库驱动jar包。

  • 我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类。

1.4.2 代码

下面我们看看原始的JDBC程序是如何操作数据库的。操作步骤如下:

  1. 注册驱动
  2. 获取连接对象
  3. 执行SQL语句,返回执行结果
  4. 处理执行结果
  5. 释放资源

在pom.xml文件中已引入MySQL驱动依赖,我们直接编写JDBC代码即可

JDBC具体代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import com.itheima.pojo.User;
import org.junit.jupiter.api.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public class JdbcTest {
@Test
public void testJdbc() throws Exception {
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");

//2. 获取数据库连接
String url="jdbc:mysql://127.0.0.1:3306/mybatis";
String username = "root";
String password = "1234";
Connection connection = DriverManager.getConnection(url, username, password);

//3. 执行SQL
Statement statement = connection.createStatement(); //操作SQL的对象
String sql="select id,name,age,gender,phone from user";
ResultSet rs = statement.executeQuery(sql);//SQL查询结果会封装在ResultSet对象中

List<User> userList = new ArrayList<>();//集合对象(用于存储User对象)
//4. 处理SQL执行结果
while (rs.next()){
//取出一行记录中id、name、age、gender、phone下的数据
int id = rs.getInt("id");
String name = rs.getString("name");
short age = rs.getShort("age");
short gender = rs.getShort("gender");
String phone = rs.getString("phone");
//把一行记录中的数据,封装到User对象中
User user = new User(id,name,age,gender,phone);
userList.add(user);//User对象添加到集合
}
//5. 释放资源
statement.close();
connection.close();
rs.close();

//遍历集合
for (User user : userList) {
System.out.println(user);
}
}
}

DriverManager(类):数据库驱动管理类。

  • 作用:

    1. 注册驱动

    2. 创建java代码和数据库之间的连接,即获取Connection对象

Connection(接口):建立数据库连接的对象

  • 作用:用于建立java程序和数据库之间的连接

Statement(接口): 数据库操作对象(执行SQL语句的对象)。

  • 作用:用于向数据库发送sql语句

ResultSet(接口):结果集对象(一张虚拟表)

  • 作用:sql查询语句的执行结果会封装在ResultSet中

通过上述代码,我们看到直接基于JDBC程序来操作数据库,代码实现非常繁琐,所以在项目开发中,我们很少使用。 在项目开发中,通常会使用Mybatis这类的高级技术来操作数据库,从而简化数据库操作、提高开发效率

1.4.3 问题分析

原始的JDBC程序,存在以下几点问题:

  1. 数据库链接的四要素(驱动、链接、用户名、密码)全部硬编码在java代码中
  2. 查询结果的解析及封装非常繁琐
  3. 每一次查询数据库都需要获取连接,操作完毕后释放连接, 资源浪费, 性能降低
image-20231223162511117

1.4.4 mybatis优化点

分析了JDBC的缺点之后,我们再来看一下在mybatis中,是如何解决这些问题的:

  1. 数据库连接四要素(驱动、链接、用户名、密码),都配置在springboot默认的配置文件 application.properties中

  2. 查询结果的解析及封装,由mybatis自动完成映射封装,我们无需关注

  3. 在mybatis中使用了数据库连接池(Springboot默认Hikari追光者)技术,从而避免了频繁的创建连接、销毁连接而带来的资源浪费。

image-20231223162830839

1.5 数据库连接池(四种)

1.5.1 介绍

image-20231223163023536

没有使用数据库连接池:

  • 客户端执行SQL语句:要先创建一个新的连接对象,然后执行SQL语句,SQL语句执行后又需要关闭连接对象从而释放资源,每次执行SQL时都需要创建连接、销毁链接,这种频繁的重复创建销毁的过程是比较耗费计算机的性能。
image-20231223163123510
  • 程序在启动时,会在数据库连接池(容器)中,创建一定数量的Connection对象

允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个

  • 客户端在执行SQL时,先从连接池中获取一个Connection对象,然后在执行SQL语句,SQL语句执行完之后,释放Connection时就会把Connection对象归还给连接池(Connection对象可以复用)

释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏

  • 客户端获取到Connection对象了,但是Connection对象并没有去访问数据库(处于空闲),数据库连接池发现Connection对象的空闲时间 > 连接池中预设的最大空闲时间,此时数据库连接池就会自动释放掉这个连接对象

数据库连接池的好处:

  1. 资源重用
  2. 提升系统响应速度
  3. 避免数据库连接遗漏

1.5.2 四种连接池

常见的数据库连接池:

  • C3P0
  • DBCP
  • Druid
  • Hikari (springboot默认)

现在使用更多的是:Hikari、Druid (性能更优越)

  • Hikari(追光者) [默认的连接池]

    image-20231223163247807

  • Druid(德鲁伊)

​ Druid连接池是阿里巴巴开源的数据库连接池项目

​ 功能强大,性能优秀,是Java语言最好的数据库连接池之一

1.5.3 更换连接池

把默认的数据库连接池切换为Druid数据库连接池,只需要完成以下两步操作即可:

参考官方地址:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

1.在pom.xml文件中引入依赖

1
2
3
4
5
6
<dependency>
<!-- Druid连接池依赖 -->
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>

2.在配置文件中引入数据库连接配置(2种方式)

1
2
3
4
5
6
7
8
9
10
11
方式1:(datasource后面加druid)
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.druid.username=root
spring.datasource.druid.password=1234

方式2:(原始方式)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=1234

1.6 lombok(编译阶段)

1.6.1 介绍

Lombok是一个实用的Java类库,可以通过简单的注解来简化和消除一些必须有但显得很臃肿的Java代码

image-20231223163915779

通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,并可以自动化生成日志变量,简化java开发、提高效率

注解 作用
@Getter/@Setter 为所有的属性提供get/set方法
@ToString 为类自动生成易阅读的 toString 方法
@EqualsAndHashCode 为类提供拥有的非静态字段自动重写 equals 方法和 hashCode 方法
@Data 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode) 集成了前四个注解
@NoArgsConstructor 为实体类生成无参的构造器方法
@AllArgsConstructor 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。

1.6.2 使用步骤

第1步:在pom.xml文件中引入依赖

1
2
3
4
5
<!-- 在springboot的父工程中,已经集成了lombok并指定了版本号,故当前引入依赖时不需要指定version -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

第2步:在实体类上添加注解

1
2
3
4
5
6
7
8
9
10
11
12
import lombok.Data;

@Data //getter方法、setter方法、toString方法、hashCode方法、equals方法
@NoArgsConstructor //无参构造
@AllArgsConstructor//全参构造
public class User {
private Integer id;
private String name;
private Short age;
private Short gender;
private String phone;
}

2.Mybatis基础操作

2.1 新增(@Insert)

2.2 删除(@Delete)

2.3 查询(@Selete)

2.4 修改(@Update)

3.Mybatis两种书写sql语句的形式

3.1 mapper层注解

image-20231224213236692

3.2 xml映射文件

image-20231224213254184

3.2.1 创建xml文件

  • 整体图
image-20231224213633587

创建位置:必须要和mapper层保持同一个包级别和同名(==同包同名==)

image-20231224213417001

命名规则:要保证mapper的namespacce和mapper同全路径名,四大类语句的id要和方法名一致,resultType要和方法返回值(最里面的)一致

image-20231224213458370

4.动态sql(在xml映射文件)

4.1 if

4.1.1 作用

用于判断条件是否成立 –如果条件为true就拼接sql

4.1.2 使用位置

image-20231224214251931

4.2 where

4.2.1 作用

1.在子元素有内容的情况下插入where子句
2.自动去除子句开头的and/or

4.2.2 使用位置

image-20231224214332923

4.3 set

4.3.1 作用

1.动态的在行首插入set关键字
2.自动去除额外的逗号

4.3.2 使用位置

image-20231224214629066

4.4 foreach

4.4.1 作用

循环的时候批量处理一部分数据

4.4.2 使用位置

image-20231224214910899

4.5 sql和include

4.5.1 作用

将一些固定的(重复的/相同的)sql提取出来

4.5.2 使用位置

image-20231224214941397

4.6 choose-when-otherwise

4.6.1 作用

类似于Java的Switch语句,可以实现选择

4.6.2 使用位置

image-20240407133356683

4.7 trim

4.7.1 作用

生成sql语句的时候,在前后添加自定义的字符串

4.7.2 使用位置

prefix:加前缀

prefixOverrides:要覆盖的前缀

suffix:加后缀

suffixOverrides:要覆盖的后缀

image-20240407133446397

4.8 bind

4.8.1 作用

可以将表达式结果/特定内容绑定到一个变量,这个变量可以在sql语句中使用【类似于参数宏定义】

4.8.2 使用位置

image-20240407133530203

5.mybatis日志输出

在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。具体操作如下:

  1. 打开application.properties文件
  2. 开启mybatis的日志,并指定输出到控制台
1
2
#指定mybatis输出日志的位置, 输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

开启日志之后,我们再次运行单元测试,可以看到在控制台中,输出了以下的SQL语句信息:

image-20231224221349185

但是我们发现输出的SQL语句:delete from emp where id = ?,我们输入的参数16并没有在后面拼接,id的值是使用?进行占位。那这种SQL语句我们称为预编译SQL

6.预编译SQL

mybatis中mapper层的写法就是预编译sql,传入参数放入的形式

image-20231224221537386

预编译SQL有两个优势:

  1. 性能更高

    image-20231224221640562

  2. 更安全(防止SQL注入)

image-20231224221649839

7.参数占位符

image-20231224221713753

如果mapper接口方法形参只有一个普通类型的参数,#{…} 里面的属性名可以随便写

8.数据封装(mysql列名和实体类属性名不一致)

8.1 出现场景

image-20231224221958093 image-20231224222223153

8.2 三种解决方案

image-20231224222522754

1. mapper层sql语句内起别名

image-20231224222245625

2.mapper层方法上添加@Results和@Result注解手动映射

image-20231224222316757

3.yml配置文件打开驼峰命名(推荐)

image-20231224222337724

9.#{}和${}区别

#{}表达式为OGNL表达式

${}表达式为EL表达式

1)相同:都可以获取对象的信息。

(2)#{ }

执行SQL时,会将#{ }替换为?,生成预编译SQL,会自动设置参数值,防止SQL注入
使用时机:参数传递,都使用#{ }
(3)${ }

​ 拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题
​ 使用时机:如果对表名、列表进行动态设置时使用【东林微课堂创建每个月赛季表】

(4)#{ } 只能操作跟数据字表字段相关的列值,跟列值无关的只能用${ }

(5)#{ } 底层使用的是PreparedStatement,${ } 底层使用的是 Statement

SpringBootWeb请求

0.SpringBootWeb请求响应

0.1 如何请求和响应

在SpringBoot进行web程序开发时,它内置了一个核心的Servlet程序 DispatcherServlet,称之为 核心控制器
DispatcherServlet 负责接收页面发送的请求,然后根据执行的规则,将请求再转发给后面的请求处理器Controller,请求处理器处理完请求之后,最终再由DispatcherServlet给浏览器响应数据

image-20231222192514355

那将来浏览器发送请求,会携带请求数据,包括:请求行、请求头;请求到达tomcat之后,tomcat会负责解析这些请求数据,然后呢将解析后的请求数据会传递给Servlet程序的HttpServletRequest对象,那也就意味着 HttpServletRequest 对象就可以获取到请求数据。 而Tomcat,还给Servlet程序传递了一个参数 HttpServletResponse,通过这个对象,我们就可以给浏览器设置响应数据 。

image-20231222192352710

那上述所描述的这种浏览器/服务器的架构模式呢,我们称之为:BS架构。

image-20231222192804945

• BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。

那今天呢,我们的课程内容主要就围绕着:请求、响应进行。 今天课程内容,主要包含三个部分:

  • 请求
  • 响应
  • 分层解耦

0.2 自我理解

其实之前就是servlet去写响应和请求的信息,但是后来有了tomcat就可以省略了对于请求三部分和响应三部分的解析,然后对于后端可以使用HttpServletRequest和Response去接受请求和设置响应,但是也很麻烦,最后就是框架的注解解决了。请求就通过判断@RequestBody等注解获取前端信息,然后响应就是@ResponseBody注解返回(Springboot类的RestController是一个组合注解里面就是controller和responsebody解决了这个顾虑)。

0.3 前后端分离

之前我们课程中有提到当前最为主流的开发模式:前后端分离

image-20231222193516639

在这种模式下,前端技术人员基于”接口文档”,开发前端程序;后端技术人员也基于”接口文档”,开发后端程序。

由于前后端分离,对我们后端技术人员来讲,在开发过程中,是没有前端页面的,那我们怎么测试自己所开发的程序呢?

方式1:在浏览器中输入地址(只能测试get请求)

方式2:使用专业的接口测试工具(课程中我们使用Postman工具)

1. 请求

1.1 简单参数

简单参数:在向服务器发起请求时,向服务器传递的是一些普通的请求数据。

image-20220826180550583

那么在后端程序中,如何接收传递过来的普通参数数据呢?

我们在这里讲解两种方式:

  1. 原始方式
  2. SpringBoot方式

1.2.1 原始方式

在原始的Web程序当中,需要通过Servlet中提供的API:HttpServletRequest(请求对象),获取请求的相关信息。比如获取请求参数:

Tomcat接收到http请求时:把请求的相关信息封装到HttpServletRequest对象中

在Controller中,我们要想获取Request对象,可以直接在方法的形参中声明 HttpServletRequest 对象。然后就可以通过该对象来获取请求信息:

1
2
//根据指定的参数名获取请求参数的数据值
String request.getParameter("参数名")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
public class RequestController {
//原始方式
@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest request){
// http://localhost:8080/simpleParam?name=Tom&age=10
// 请求参数: name=Tom&age=10 (有2个请求参数)
// 第1个请求参数: name=Tom 参数名:name,参数值:Tom
// 第2个请求参数: age=10 参数名:age , 参数值:10

String name = request.getParameter("name");//name就是请求参数名
String ageStr = request.getParameter("age");//age就是请求参数名

int age = Integer.parseInt(ageStr);//需要手动进行类型转换
System.out.println(name+" : "+age);
return "OK";
}
}

以上这种方式,我们仅做了解。(在以后的开发中不会使用到)

1.2.2 SpringBoot方式

在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=10
// 第1个请求参数: name=Tom 参数名:name,参数值:Tom
// 第2个请求参数: age=10 参数名:age , 参数值:10

//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致
System.out.println(name+" : "+age);
return "OK";
}
}

postman测试( GET 请求):

image-20231222203422664

postman测试( POST请求 ):

image-20231222203429270

结论:不论是GET请求还是POST请求,对于简单参数来讲,只要保证==请求参数名和Controller方法中的形参名保持一致==,就可以获取到请求参数中的数据值。

1.2.3 参数名不一致

如果方法形参名称与请求参数名称不一致,controller方法中的形参还能接收到请求参数值吗?

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=20
// 请求参数名:name

//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(String username , Integer age ){//请求参数名和形参名不相同
System.out.println(username+" : "+age);
return "OK";
}
}

答案:运行没有报错。 controller方法中的username值为:null,age值为20

  • 结论:对于简单参数来讲,请求参数名和controller方法中的形参名不一致时,无法接收到请求数据

那么如果我们开发中,遇到了这种请求参数名和controller方法中的形参名不相同,怎么办?

解决方案:可以使用Spring提供的@RequestParam注解完成映射

在方法形参前面加上 @RequestParam 然后通过value属性执行请求参数名,从而完成映射。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=20
// 请求参数名:name

//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam("name") String username , Integer age ){
System.out.println(username+" : "+age);
return "OK";
}
}

注意事项:

@RequestParam中的required属性默认为true(默认值也是true),代表该请求参数必须传递,如果不传递将报错

image-20231222203507662

如果该参数是可选的,可以将required属性设置为false

1
2
3
4
5
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age){
System.out.println(username+ ":" + age);
return "OK";
}

1.3 实体参数

在使用简单参数做为数据传递方式时,前端传递了多少个请求参数,后端controller方法中的形参就要书写多少个。如果请求参数比较多,通过上述的方式一个参数一个参数的接收,会比较繁琐。

此时,我们可以考虑将请求参数封装到一个实体类对象中。 要想完成数据封装,需要遵守如下规则:请求参数名与实体类的属性名相同

image-20231222203541083

1.3.1 简单实体对象

定义POJO实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class User {
private String name;
private Integer age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

Controller方法:

1
2
3
4
5
6
7
8
9
@RestController
public class RequestController {
//实体参数:简单实体对象
@RequestMapping("/simplePojo")
public String simplePojo(User user){
System.out.println(user);
return "OK";
}
}

Postman测试:

  • 参数名和实体类属性名一致时

image-20231222203621791

  • 参数名和实体类属性名不一致时

image-20231222203610548

1.3.2 复杂实体对象

上面我们讲的呢是简单的实体对象,下面我们在来学习下复杂的实体对象。

复杂实体对象指的是,在实体类中有一个或多个属性,也是实体对象类型的。如下:

  • User类中有一个Address类型的属性(Address是一个实体类)
image-20231222203640652

复杂实体对象的封装,需要遵守如下规则:

  • 请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。

定义POJO实体类:

  • Address实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Address {
private String province;
private String city;

public String getProvince() {
return province;
}

public void setProvince(String province) {
this.province = province;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

@Override
public String toString() {
return "Address{" +
"province='" + province + '\'' +
", city='" + city + '\'' +
'}';
}
}
  • User实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class User {
private String name;
private Integer age;
private Address address; //地址对象

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public Address getAddress() {
return address;
}

public void setAddress(Address address) {
this.address = address;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}
}

Controller方法:

1
2
3
4
5
6
7
8
9
@RestController
public class RequestController {
//实体参数:复杂实体对象
@RequestMapping("/complexPojo")
public String complexPojo(User user){
System.out.println(user);
return "OK";
}
}

Postman测试:

image-20231222203658587

1.4 数组集合参数

数组集合参数的使用场景:在HTML的表单中,有一个表单项是支持多选的(复选框),可以提交选择的多个值。

image-20231222203724195

多个值是怎么提交的呢?其实多个值也是一个一个的提交。

image-20231222203752082

后端程序接收上述多个值的方式有两种:

  1. 数组
  2. 集合

1.4.1 数组

数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数

image-20231222203810385

Controller方法:

1
2
3
4
5
6
7
8
9
@RestController
public class RequestController {
//数组集合参数
@RequestMapping("/arrayParam")
public String arrayParam(String[] hobby){
System.out.println(Arrays.toString(hobby));
return "OK";
}
}

Postman测试:

在前端请求时,有两种传递形式:

方式一: xxxxxxxxxx?hobby=game&hobby=java

image-20231222203830857

方式二:xxxxxxxxxxxxx?hobby=game,java

image-20231222203840400

1.4.2 集合

集合参数:请求参数名与形参集合对象名相同且请求参数为多个,@RequestParam 绑定参数关系

默认情况下,请求中参数名相同的多个值,是封装到数组。如果要封装到集合,要使用@RequestParam绑定参数关系

image-20231222203855479

Controller方法:

1
2
3
4
5
6
7
8
9
@RestController
public class RequestController {
//数组集合参数
@RequestMapping("/listParam")
public String listParam(@RequestParam List<String> hobby){
System.out.println(hobby);
return "OK";
}
}

Postman测试:

方式一: xxxxxxxxxx?hobby=game&hobby=java

方式二:xxxxxxxxxxxxx?hobby=game,java

image-20231222203936944

1.5 日期参数

上述演示的都是一些普通的参数,在一些特殊的需求中,可能会涉及到日期类型数据的封装。比如,如下需求:

image-20231222203953214

因为日期的格式多种多样(如:2022-12-12 10:05:45 、2022/12/12 10:05:45),那么对于日期类型的参数在进行封装的时候,需要通过@DateTimeFormat注解,以及其pattern属性来设置日期的格式。

image-20231222204000143
  • @DateTimeFormat注解的pattern属性中指定了哪种日期格式,前端的日期参数就必须按照指定的格式传递。
  • 后端controller方法中,需要使用Date类型或LocalDateTime类型,来封装传递的参数。

Controller方法:

1
2
3
4
5
6
7
8
9
@RestController
public class RequestController {
//日期时间参数
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){
System.out.println(updateTime);
return "OK";
}
}

Postman测试:

image-20231222204008448

1.6 JSON参数

在学习前端技术时,我们有讲到过JSON,而在前后端进行交互时,如果是比较复杂的参数,前后端通过会使用JSON格式的数据进行传输。 (JSON是开发中最常用的前后端数据交互方式)

我们学习JSON格式参数,主要从以下两个方面着手:

  1. Postman在发送请求时,如何传递json格式的请求参数
  2. 在服务端的controller方法中,如何接收json格式的请求参数

Postman发送JSON格式数据:

image-20231222204018760

服务端Controller方法接收JSON格式数据:

  • 传递json格式的参数,在Controller中会使用实体类进行封装。

  • 封装规则:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。

  • image-20231222204031290@RequestBody注解:将JSON数据映射到形参的实体类对象中(JSON中的key和实体类中的属性名保持一致)

实体类:Address

1
2
3
4
5
6
public class Address {
private String province;
private String city;

//省略GET , SET 方法
}

实体类:User

1
2
3
4
5
6
7
public class User {
private String name;
private Integer age;
private Address address;

//省略GET , SET 方法
}

Controller方法:

1
2
3
4
5
6
7
8
9
@RestController
public class RequestController {
//JSON参数
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user){
System.out.println(user);
return "OK";
}
}

Postman测试:

image-20231222204102025

1.7 路径参数

传统的开发中请求参数是放在请求体(POST请求)传递或跟在URL后面通过?key=value的形式传递(GET请求)。

image-20231222204120762

在现在的开发中,经常还会直接在请求的URL中传递参数。例如:

1
2
http://localhost:8080/user/1		
http://localhost:880/user/1/0

上述的这种传递请求参数的形式呢,我们称之为:路径参数。

学习路径参数呢,主要掌握在后端的controller方法中,如何接收路径参数。

路径参数:

  • 前端:通过请求URL直接传递参数
  • 后端:使用{…}来标识该路径参数,需要使用@PathVariable获取路径参数
image-20231222204136790

Controller方法:

1
2
3
4
5
6
7
8
9
@RestController
public class RequestController {
//路径参数
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable Integer id){
System.out.println(id);
return "OK";
}
}

Postman测试:

image-20231222204146349

传递多个路径参数:

Postman:

image-20231222204159233

Controller方法:

1
2
3
4
5
6
7
8
9
@RestController
public class RequestController {
//路径参数
@RequestMapping("/path/{id}/{name}")
public String pathParam2(@PathVariable Integer id, @PathVariable String name){
System.out.println(id+ " : " +name);
return "OK";
}
}

2. 响应

前面我们学习过HTTL协议的交互方式:请求响应模式(有请求就有响应)

那么Controller程序呢,除了接收请求外,还可以进行响应。

2.1 @ResponseBody

在我们前面所编写的controller方法中,都已经设置了响应数据。

image-20231222204216090

controller方法中的return的结果,怎么就可以响应给浏览器呢?

答案:使用@ResponseBody注解

@ResponseBody注解:

  • 类型:方法注解、类注解
  • 位置:书写在Controller方法上或类上
  • 作用:将方法返回值直接响应给浏览器
    • 如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器

但是在我们所书写的Controller中,只在类上添加了@RestController注解、方法添加了@RequestMapping注解,并没有使用@ResponseBody注解,怎么给浏览器响应呢?

1
2
3
4
5
6
7
8
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
System.out.println("Hello World ~");
return "Hello World ~";
}
}

原因:在类上添加的@RestController注解,是一个组合注解。

  • @RestController = @Controller + @ResponseBody

@RestController源码:

1
2
3
4
5
6
7
8
9
10
11
@Target({ElementType.TYPE})   //元注解(修饰注解的注解)
@Retention(RetentionPolicy.RUNTIME) //元注解
@Documented //元注解
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}

结论:在类上添加@RestController就相当于添加了@ResponseBody注解。

  • 类上有@RestController注解或@ResponseBody注解时:表示当前类下所有的方法返回值做为响应数据
    • 方法的返回值,如果是一个POJO对象或集合时,会先转换为JSON格式,在响应给浏览器

下面我们来测试下响应数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RestController
public class ResponseController {
//响应字符串
@RequestMapping("/hello")
public String hello(){
System.out.println("Hello World ~");
return "Hello World ~";
}
//响应实体对象
@RequestMapping("/getAddr")
public Address getAddr(){
Address addr = new Address();//创建实体类对象
addr.setProvince("广东");
addr.setCity("深圳");
return addr;
}
//响应集合数据
@RequestMapping("/listAddr")
public List<Address> listAddr(){
List<Address> list = new ArrayList<>();//集合对象

Address addr = new Address();
addr.setProvince("广东");
addr.setCity("深圳");

Address addr2 = new Address();
addr2.setProvince("陕西");
addr2.setCity("西安");

list.add(addr);
list.add(addr2);
return list;
}
}

在服务端响应了一个对象或者集合,那私前端获取到的数据是什么样子的呢?我们使用postman发送请求来测试下。测试效果如下:

image-20231222204238847 image-20231222204246396

2.2 统一响应结果

大家有没有发现一个问题,我们在前面所编写的这些Controller方法中,返回值各种各样,没有任何的规范。

image-20231222204309511

如果我们开发一个大型项目,项目中controller方法将成千上万,使用上述方式将造成整个项目难以维护。那在真实的项目开发中是什么样子的呢?

在真实的项目开发中,无论是哪种方法,我们都会定义一个统一的返回结果。方案如下:

image-20231222204329099

前端:只需要按照统一格式的返回结果进行解析(仅一种解析方案),就可以拿到数据。

统一的返回结果使用类来描述,在这个结果中包含:

  • 响应状态码:当前请求是成功,还是失败

  • 状态码信息:给页面的提示信息

  • 返回的数据:给前端响应的数据(字符串、对象、集合)

定义在一个实体类Result来包含以上信息。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Result {
private Integer code;//响应码,1 代表成功; 0 代表失败
private String msg; //响应码 描述字符串
private Object data; //返回的数据

public Result() { }
public Result(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}

//增删改 成功响应(不需要给前端返回数据)
public static Result success(){
return new Result(1,"success",null);
}
//查询 成功响应(把查询结果做为返回数据响应给前端)
public static Result success(Object data){
return new Result(1,"success",data);
}
//失败响应
public static Result error(String msg){
return new Result(0,msg,null);
}
}

改造Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RestController
public class ResponseController {
//响应统一格式的结果
@RequestMapping("/hello")
public Result hello(){
System.out.println("Hello World ~");
//return new Result(1,"success","Hello World ~");
return Result.success("Hello World ~");
}

//响应统一格式的结果
@RequestMapping("/getAddr")
public Result getAddr(){
Address addr = new Address();
addr.setProvince("广东");
addr.setCity("深圳");
return Result.success(addr);
}

//响应统一格式的结果
@RequestMapping("/listAddr")
public Result listAddr(){
List<Address> list = new ArrayList<>();

Address addr = new Address();
addr.setProvince("广东");
addr.setCity("深圳");

Address addr2 = new Address();
addr2.setProvince("陕西");
addr2.setCity("西安");

list.add(addr);
list.add(addr2);
return Result.success(list);
}
}

使用Postman测试:

image-20231222204346669 image-20231222204353420

3. 分层解耦

3.1 三层架构

3.1.1 介绍

在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一些(单一职责原则)。

单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。

这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。

我们之前开发的程序呢,并不满足单一职责原则。下面我们来分析下之前的程序:

image-20231222210953103

那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:

  • 数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。
  • 逻辑处理:负责业务逻辑处理的代码。
  • 请求处理、响应数据:负责,接收页面的请求,给页面响应数据。

按照上述的三个组成部分,在我们项目开发中呢,可以将代码分为三层:

image-20231222211029851

  • Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
  • Service:业务逻辑层。处理具体的业务逻辑。
  • Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。

基于三层架构的程序执行流程:

image-20231222212336762
  • 前端发起的请求,由Controller层接收(Controller响应数据给前端)
  • Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
  • Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
  • Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)

思考:按照三层架构的思想,如何要对业务逻辑(Service层)进行变更,会影响到Controller层和Dao层吗?

答案:不会影响。 (程序的扩展性、维护性变得更好了)

3.1.2 代码拆分

我们使用三层架构思想,来改造下之前的程序:

image-20231222211133871

三层架构的好处:

  1. 复用性强
  2. 便于维护
  3. 利用扩展

3.2 分层解耦

刚才我们学习过程序分层思想了,接下来呢,我们来学习下程序的解耦思想。

解耦:解除耦合。

3.2.1 耦合问题

首先需要了解软件开发涉及到的两个概念:内聚和耦合。

  • 内聚:软件中各个功能模块内部的功能联系。

  • 耦合:衡量软件中各个层/模块之间的依赖、关联的程度。

软件设计原则:高内聚低耦合。

高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 “高内聚”。

低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。

程序中高内聚的体现:

  • EmpServiceA类中只编写了和员工相关的逻辑处理代码
image-20231223110557491

程序中耦合代码的体现:

  • 把业务类变为EmpServiceB时,需要修改controller层中的代码
image-20231223110817267

高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强。

image-20231223110830461

3.2.2 解耦思路(控制反转和依赖注入的原因)

之前我们在编写代码时,需要什么对象,就直接new一个就可以了。 这种做法呢,层与层之间代码就耦合了,当service层的实现变了之后, 我们还需要修改controller层的代码。

image-20231223110855213

那应该怎么解耦呢?

  • 不能new,就意味着没有业务层对象(程序运行就报错),怎么办呢?
    • 我们的解决思路是:
      • 提供一个容器,容器中存储一些对象(例:EmpService对象)
      • controller程序从容器中获取EmpService类型的对象

我们想要实现上述解耦操作,就涉及到Spring中的两个核心概念:

  • 控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。

    对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器

  • 依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。

    程序运行时需要某个资源,此时容器就为其提供这个资源。

    例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象

IOC容器中创建、管理的对象,称之为:bean对象

image-20231223111141495

4. IOC控制依赖和DI依赖注入

上面我们引出了Spring中IOC和DI的基本概念,下面我们就来具体学习下IOC和DI的代码实现。

4.1 IOC&DI基本步骤

任务:完成Controller层、Service层、Dao层的代码解耦

  • 思路:
    1. 删除Controller层、Service层中new对象的代码
    2. Service层及Dao层的实现类,交给IOC容器管理
    3. 为Controller及Service注入运行时依赖的对象
      • Controller程序中注入依赖的Service层对象
      • Service程序中注入依赖的Dao层对象

第1步:删除Controller层、Service层中new对象的代码

image-20231223111439496

第2步:Service层及Dao层的实现类,交给IOC容器管理

  • 使用Spring提供的注解:@Component,就可以实现类交给IOC容器管理

image-20231223111452170

第3步:为Controller及Service注入运行时依赖的对象

  • 使用Spring提供的注解:@Autowired ,就可以实现程序运行时IOC容器自动注入需要的依赖对象

image-20231223111517967

4.2 IOC详解

4.2.1 bean的声明(每个层一个注解)

前面我们提到IOC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象。

在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component

而Spring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:

  • @Controller (标注在控制层类上)

  • @Service (标注在业务层类上)

  • @Repository (标注在数据访问层类上)

要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:

注解 说明 位置
@Controller @Component的衍生注解 标注在控制器类上
@Service @Component的衍生注解 标注在业务类上
@Repository @Component的衍生注解 标注在数据访问类上(由于与mybatis整合 –mybatis用的@Mapper)
@Component 声明bean的基础注解 不清楚属于哪一个层上就用这个

在IOC容器中,每一个Bean都有一个属于自己的名字,可以通过注解的value属性指定bean的名字。如果没有指定,默认为类名首字母小写。

image-20231223112029205

注意事项:

  • 声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
  • 使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。

4.2.2 组件扫描

问题:使用前面学习的四个注解声明的bean,一定会生效吗?

答案:不一定。(原因:bean想要生效,还需要被组件扫描)

image-20231223112148802

推荐做法(如下图):

  • 将我们定义的controller,service,dao这些包呢,都放在引导类所在包com.itheima的子包下,这样我们定义的bean就会被自动的扫描到
image-20231223112123552

4.3 DI详解(4种依赖注入)

依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。

4.3.1 @Autowired注解

默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作) –如果有多个相同类型的就会报错,就要用其他三种注解解决

image-20231223112602006

4.3.2 @Primary注解(在相同类型的类上单独加优先级)

使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。

image-20231223112636963

4.3.3 @Qualifier注解 (在出错位置配合Autowired注解看用哪个)

使用@Qualifier注解:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。

  • @Qualifier注解不能单独使用,必须配合@Autowired使用

image-20231223112754645

4.3.4 @Resource注解(和Autowired同一位置,但是是按照名称注入)

使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。

image-20231223112926795

4.3.4 面试题和总结

面试题 : @Autowird 与 @Resource的区别

  • @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
  • @Autowired 默认是按照类型注入,而@Resource是按照名称注入
  • @Autowired 如果出现多个类型相同的话会出现异常(所以用其他三种注解解决)
  • 总结

image-20231223113125106

4.4 控制依赖和依赖注入总体图

image-20231223150424032

图解HTTP

1.基础概念

1.1 客户端和服务端

image-20231110211130071

1.2 TCP/IP协议族

1.2.1 定义

  • 定义
1
2
第一种:TCP和IP协议
第二种:IP协议的通信过程中,使用到的所有协议族(和互联网相关联的协议集合)的总称

image-20231110211754520

1.2.2 网络协议

image-20231110212714910

层名 传输的数据 作用 协议
应用层 报文 为特定应用程序提供数据传输服务 FTP(文件传输协议)
DNS(域名系统)
HTTP(超文本传输协议)
DHCP(动态主机配置协议)
TELNET(远程登录协议)
SMTP()
POP3()
IMAP()
传输层 TCP报文段
UDP用户数据包
为进程提供通用数据传输服务 TCP(传输控制协议)
UDP(用户数据报协议)
网络层 IP数据包 为主机提供数据传输服务 IP(网络互连协议)
ARP(地址解析协议)
ICMP(网际控制报文协议)
IGMP(网际组管理协议)
数据链路层 数据帧 为同一链路的主机提供数据传输服务 PPP协议

image-20231110213521338

1.2.3 网络层-IP协议

​ 1.作用:将各种数据包传送给对方

​ 2.两个重要条件:①IP地址:指明了节点被分配到的地址(与MAC地址进行配对,可变) ②MAC地址:指网卡所属的固定地址(基本不变)

1.2.4 传输层-TCP协议

1.2.4.1 三次握手

image-20231111104646583

1.2.5 应用层-DNS域名解析

image-20231111104743329

1.2.6 各层协议之间关系

image-20231111104953243

1.2.7 URL和URI

1.2.7.1 URL(统一资源定位符) —表示资源的地点

  • RFC3986:

image-20231112142348680

1.2.7.2 URI(统一资源标识符) —标识某一互联网资源

image-20231112143058269

1.3 HTTP请求报文和响应报文

1.3.1 HTTP请求报文

1.3.2 HTTP响应报文

,