尚硅谷springboot3场景整合

环境准备

Docker 安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sudo yum install -y yum-utils

sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo systemctl enable docker --now

#测试工作
docker ps
# 批量安装所有软件
docker compose

创建/prod文件夹,准备以下文件

prometheus.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
global:
scrape_interval: 15s
evaluation_interval: 15s

scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']

- job_name: 'redis'
static_configs:
- targets: ['redis:6379']

- job_name: 'kafka'
static_configs:
- targets: ['kafka:9092']

docker-compose.yml

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
version: '3.9'

services:
redis:
image: redis:latest
container_name: redis
restart: always
ports:
- "6379:6379"
networks:
- backend

zookeeper:
image: bitnami/zookeeper:latest
container_name: zookeeper
restart: always
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
networks:
- backend

kafka:
image: bitnami/kafka:3.4.0
container_name: kafka
restart: always
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
ALLOW_PLAINTEXT_LISTENER: yes
KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
networks:
- backend

kafka-ui:
image: provectuslabs/kafka-ui:latest
container_name: kafka-ui
restart: always
depends_on:
- kafka
ports:
- "8080:8080"
environment:
KAFKA_CLUSTERS_0_NAME: dev
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
networks:
- backend

prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: always
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
networks:
- backend

grafana:
image: grafana/grafana:latest
container_name: grafana
restart: always
depends_on:
- prometheus
ports:
- "3000:3000"
networks:
- backend

networks:
backend:
name: backend

启动环境

1
docker compose -f docker-compose.yml up -d

验证

NoSQL

Redis整合

场景整合

依赖导入

1
2
3
4
<dependency>  
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置

1
2
3
spring.data.redis.host=192.168.96.136  
spring.data.redis.port=6379
spring.data.redis.password=

测试

1
2
3
4
5
6
7
8
@Autowired
StringRedisTemplate redisTemplate;

@Test
void redisTest(){
redisTemplate.opsForValue().set("a","1234");
Assertions.assertEquals("1234",redisTemplate.opsForValue().get("a"));
}

redisTemplate针对redis各种数据结构的方法

1
2
3
4
5
6
7
8
9
10
//普通字符串  
redisTemplate.opsForValue();
//列表
redisTemplate.opsForList();
//集合
redisTemplate.opsForSet();
//有序集合
redisTemplate.opsForZSet();
//hash-map结构
redisTemplate.opsForHash();

自动配置原理

  1. \META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports中导入了RedisAutoConfiguration、RedisReactiveAutoConfiguration、RedisRepositoriesAutoConfiguration,所有属性绑定在RedisProperties中
  2. RedisReactiveAutoConfiguration属于响应式编程,RedisRepositoriesAutoConfiguration属于JPA操作
  3. RedisAutoConfiguration 配置了以下组件:
    • LettuceConnectionConfiguration:给容器中注入了连接工厂LettuceConnectionFactory和操作redis的客户端DefaultClientResources。
    • RedisTemplate<Object, Object>:可以给redis中存储任意对象,默认使用jdk默认的序列化方式。
    • StringRedisTemplate:给redis中存储字符串,如果要存对象,需要开发人员自己进行序列化,key-value都是字符进行操作。

定制化

序列化机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class AppRedisConfiguration {


/**
* 允许Object类型的key-value,都可以被转为json进行存储。
* @param redisConnectionFactory 自动配置好了连接工厂
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//把对象转为json字符串的序列化工具
template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}

redis客户端

RedisTemplate、StringRedisTemplate :操作redis的工具类

  • 要从redis的连接工厂获取连接才能操作redis
  • redis客户端
    • Lettuce:默认
    • Jedis:可以使用以下切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 将默认的Lettuce从依赖中排除,然后引入Jedis依赖,即可将客户端切换成Jedis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.cliets</groupId>
<artifactId>jedis</artifactId>
</dependency>

配置参考

1
2
3
4
5
6
7
8
9
10
11
12
spring.data.redis.host=192.168.96.136
spring.data.redis.port=6379
#spring.data.redis.client-type=lettuce

#设置lettuce的底层参数
#spring.data.redis.lettuce.pool.enabled=true
#spring.data.redis.lettuce.pool.max-active=8
# 如果只有某一个客户端的依赖,不需要设置此项会自动使用;
# 如果存在多个客户端的依赖,不设置此项默认使用Lettuce,设置了则使用指定客户端
spring.data.redis.client-type=jedis
spring.data.redis.jedis.pool.enabled=true
spring.data.redis.jedis.pool.max-active=8

接口文档

OpenAPI3与Swagger

Swagger可以快速生成实时接口文档,方便前后端开发人员进行协调沟通,遵循OpenAPI规范。
文档:OpenAPI 3 Library for spring-boot (springdoc.org)

OpenAPI3架构

OpenApi3架构图示

整合

导入场景

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>

配置

1
2
3
4
5
6
7
8
# /api-docs endpoint custom path 默认 /v3/api-docs
springdoc.api-docs.path=/api-docs

# swagger 相关配置在 springdoc.swagger-ui
# swagger-ui custom path
springdoc.swagger-ui.path=/swagger-ui.html

springdoc.show-actuator=true

使用

常用注解

注解 标注位置 作用
@Tag controller类 标识controller作用
@Parameter 参数 标识参数作用
@Parameters 参数 参数多重说明
@Schema model层的JavaBean 描述模型作用以及每个属性
@Operation 方法 描述方法作用
@ApiResponse 方法 描述方法响应状态码等

Docket配置

如果有多个Docket,配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//配置分组,匹配某一种类型的请求路径放在一个组内,可以在页面切换查看
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("springshop-public")
.pathsToMatch("/public/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("springshop-admin")
.pathsToMatch("/admin/**")
.addMethodFilter(method -> method.isAnnotationPresent(Admin.class))
.build();
}

如果只有一个Docket,可以配置如下

1
2
springdoc.packagesToScan=package1, package2
springdoc.pathsToMatch=/v1, /api/balance/**

OpenApi配置

1
2
3
4
5
6
7
8
9
10
11
@Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info().title("SpringShop API")
.description("Spring shop sample application")
.version("v0.0.1")
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
.externalDocs(new ExternalDocumentation()
.description("SpringShop Wiki Documentation")
.url("https://springshop.wiki.github.org/docs"));
}

springFox迁移

注解变化

原注解 现注解 作用
@Api @Tag 描述Controller
@ApiIgnore @Parameter(hidden = true)
@Operation(hidden = true)
@Hidden
描述忽略操作
@ApiImplicitParam @Parameter 描述参数
@ApiImplicitParams @Parameters 描述参数
@ApiModel @Schema 描述对象
@ApiModelProperty(hidden = true) @Schema(accessMode = READ_ONLY) 描述对象属性
@ApiModelProperty @Schema 描述对象属性
@ApiOperation(value = “foo”, notes = “bar”) @Operation(summary = “foo”, description = “bar”) 描述方法
@ApiParam @Parameter 描述参数
@ApiResponse(code = 404, message = “foo”) @ApiResponse(responseCode = “404”, description = “foo”) 描述响应

Docket配置

以前写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Bean
public Docket publicApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("org.github.springshop.web.public"))
.paths(PathSelectors.regex("/public.*"))
.build()
.groupName("springshop-public")
.apiInfo(apiInfo());
}

@Bean
public Docket adminApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("org.github.springshop.web.admin"))
.paths(PathSelectors.regex("/admin.*"))
.apis(RequestHandlerSelectors.withMethodAnnotation(Admin.class))
.build()
.groupName("springshop-admin")
.apiInfo(apiInfo());
}
新写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("springshop-public")
.pathsToMatch("/public/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("springshop-admin")
.pathsToMatch("/admin/**")
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Admin.class))
.build();
}
添加OpenAPI组件
1
2
3
4
5
6
7
8
9
10
11
@Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info().title("SpringShop API")
.description("Spring shop sample application")
.version("v0.0.1")
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
.externalDocs(new ExternalDocumentation()
.description("SpringShop Wiki Documentation")
.url("https://springshop.wiki.github.org/docs"));
}

远程调用

RPC(Remote Procedure Call):远程过程调用
RPC图示
本地过程调用:a(); b(); a(){b();};不同方法都在同一个JVM运行
远程过程调用:

  • 服务提供者
  • 服务消费者
  • 通过连接对方服务器进行请求/响应,来实现调用效果

API/SDK的区别是什么?

  • api:接口(Application Programming Interface)
    • 远程提供功能
  • sdk:工具包(Software Development Kit)
    • 导入jar包,直接调用功能即可

开发过程中,我们经常需要调用别人写的功能

  • 如果是内部微服务,可以通过依赖cloud、注册中心、openfeign等进行调用
  • 如果是外部暴露的,可以发送http请求、或遵循外部协议进行调用

SpringBoot 整合提供了很多方式进行远程调用

  • 轻量级客户端方式
    • RestTemplate:普通开发
    • RestClient:阻塞式
    • WebClient:响应式编程开发
    • Http Interface:声明式编程
  • Spring Cloud分布式解决方案方式
    • Spring Cloud OpenFeign
  • 第三方框架
    • Dubbo
    • gRPC
    • …

RestClient

创建与配置

引入依赖

1
2
3
4
5
<!-- RestClient也是在web场景中,是spring6.1提供的新功能 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

创建RestClient

1
2
3
4
5
6
//创建RestClient并设置url
RestClient restClient = RestClient.create("http://127.0.0.1:8081/get");
//或者
RestClient restClient = RestClient.builder().baseUrl("http://127.0.0.1:8081/get").build();
//或者创建时不设置,基础路径,调用时再设置
RestClient restClient = RestClient.builder().build();

更多参数可以参考WebClient

获取响应

通过retrieve()方法来声明如何提取响应数据。

1
2
3
4
5
6
7
8
9
RestClient restClient = RestClient.create("http://127.0.0.1:8081");
//获取完整数据
ResponseEntity<Person> entity = restClient.post().uri("/get")
.contentType(MediaType.APPLICATION_JSON)
.body(person).retrieve().toEntity(Person.class);
//只获取body中的数据
Person person1 = restClient.post().uri("/get")
.contentType(MediaType.APPLICATION_JSON)
.body(person).retrieve().body(Person.class);

定义请求体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String name = "小红书";  
Map<String,Object> map = new HashMap<>();
map.put("name",name);
//可以使用map进行传值(根据名称对应)
//Person person1 = restClient.post().uri("/{name}/get", map)
//也可以使用可变数组进行传值(根据顺序对应)
Person person1 = restClient.post().uri("/{name}/get", "小红书")
//也可以在请求头中设置参数
.header("","")
.contentType(MediaType.APPLICATION_JSON)
//设置body
.body(person)
.retrieve()
//设置响应body的接受类型
.body(Person.class);

WebClient

非阻塞、响应式HTTP客户端

创建与配置

发请求:

  • 请求方式:GET/POST/DELETE/XXX
  • 请求路径:/xxx
  • 请求参数:aa=bb&cc=dd&xxx
  • 请求头:aa=bb,cc=dd
  • 请求体

创建WebClient非常简单:

  • WebClient.create()
  • WebClient.create(String baseUrl)
    还可以使用WebClient.builder()配置更多参数项:
  • uriBuilderFactory():自定义uriBuilderFactory,定义baseUrl
  • defaultUriVariables() :默认url变量
  • defaultHeader():每个请求默认头
  • defaultCookie():每个请求默认Cookie
  • defaultRequest():Consumer自定义每个请求
  • filter():过滤client发送的每个请求
  • exchangeStrategies():HTTP消息 reader/writer 自定义
  • clientConnector():HTTP client库设置
1
WebClient client = WebClient.create("https://example.org");

获取响应

retrieve()方法用来声明如何提取响应数据,比如

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
//获取响应完整信息
WebClient client = WebClient.create("https://example.org");

Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);

//只获取body
WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);

//stream数据
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);

//定义错误处理
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);

定义请求体

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
//1、响应式-单个数据
Mono<Person> personMono = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);

//2、响应式-多个数据
Flux<Person> personFlux = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);

//3、普通对象
Person person = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);

HTTP Interface

Spring允许我们通过定义接口的方式,给任意位置发送http请求,实现远程调用,可以用来简化HTTP远程访问,需要webflux场景才可(web场景可以使用RestClient)

导入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

定义接口

1
2
3
4
public interface BingService {
@GetExchange(url = "/search")
String search(@RequestParam("q") String keyword);
}

创建代理&测试

WebClient

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
@SpringBootTest
class Boot05TaskApplicationTests {

@Test
void contextLoads() throws InterruptedException {
//1、创建客户端
WebClient client = WebClient.builder()
.baseUrl("https://cn.bing.com")
.codecs(clientCodecConfigurer -> {
clientCodecConfigurer
.defaultCodecs()
.maxInMemorySize(256*1024*1024);
//响应数据量太大有可能会超出BufferSize,所以这里设置的大一点
})
.build();
//2、创建工厂
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(client)).build();
//3、获取代理对象
BingService bingService = factory.createClient(BingService.class);


//4、测试调用
Mono<String> search = bingService.search("尚硅谷");
System.out.println("==========");
search.subscribe(str -> System.out.println(str));

Thread.sleep(100000);
}
}

RestClient

1
2
3
4
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder().exchangeAdapter(RestClientAdapter.create(RestClient.builder().build())).build(); 

PersonInterface personInterface = factory.createClient(PersonInterface.class)
Person person = personInterface.get();

消息服务

Apache Kafka

消息队列-场景

异步

异步图示

解耦

解耦图示

削峰

削峰图示

缓冲

缓冲图示

消息队列-Kafka

消息模式

kafka消息模式图示

Kafka工作原理

kafka工作原理图示

SpringBoot整合

Spring Kafka

1
2
3
4
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>

配置

1
spring.kafka.bootstrap-servers=192.168.96.136:9092

消息发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest
class Boot07KafkaApplicationTests {

@Autowired
KafkaTemplate kafkaTemplate;
@Test
void contextLoads() throws ExecutionException, InterruptedException {
StopWatch watch = new StopWatch();
watch.start();
CompletableFuture[] futures = new CompletableFuture[10000];
for (int i = 0; i < 10000; i++) {
CompletableFuture send = kafkaTemplate.send("order", "order.create."+i, "订单创建了:"+i);
futures[i]=send;
}
CompletableFuture.allOf(futures).join();
watch.stop();
System.out.println("总耗时:"+watch.getTotalTimeMillis());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

@Component
public class MyBean {

private final KafkaTemplate<String, String> kafkaTemplate;

public MyBean(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}

public void someMethod() {
this.kafkaTemplate.send("someTopic", "Hello");
}
}

消息监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class OrderMsgListener {

@KafkaListener(topics = "order",groupId = "order-service")
public void listen(ConsumerRecord record){
System.out.println("收到消息:"+record); //可以监听到发给kafka的新消息,以前的拿不到
}

@KafkaListener(groupId = "order-service-2",topicPartitions = {
@TopicPartition(topic = "order",partitionOffsets = {
@PartitionOffset(partition = "0",initialOffset = "0")
})
})
public void listenAll(ConsumerRecord record){
System.out.println("收到partion-0消息:"+record);
}
}

参数配置

消费者

1
2
3
spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer
spring.kafka.consumer.properties[spring.json.value.default.type]=com.example.Invoice
spring.kafka.consumer.properties[spring.json.trusted.packages]=com.example.main,com.example.another

生产者

1
2
3
#序列化
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
spring.kafka.producer.properties[spring.json.add.type.headers]=false

自动配置原理

kafka 自动配置在`KafkaAutoConfiguration

  1. 容器中放了 KafkaTemplate 可以进行消息收发
  2. 容器中放了 KafkaAdmin 可以进行 Kafka 的管理,比如创建 topic 等
  3. kafka 的配置在 KafkaProperties 中
  4. @EnableKafka可以开启基于注解的模式

kafka知识点

web安全

  • Apache Shiro
  • Spring Security
  • 自研:Filter

Spring Security

安全架构

认证:Authentication

who are you? 你是谁
登录系统,用户系统

授权:Authorization

what are you allowed to do? 你能做什么?
权限管理,用户授权

攻击防护

  • XSS(Cross-site scripting)
  • CSRF(Cross-site request forgery)
  • CORS(Cross-Origin Resource Sharimg)
  • SQL注入
  • …

扩展:权限模型

BAC(Role Based Access Controll)

用户 -> 角色 -> 权限

一个用户拥有一个或者多个角色,每个角色下拥有不同的权限。
用户不直接对应权限,而是对应角色,然后由角色再对应权限。

ACL(Access Controll List)

用户 -> 权限

用户直接和权限对应,直接给用户分配对应权限。

Spring Security原理

过滤器链架构

Spring Security 利用 FilterChainProxy 封装一系列拦截器链,实现各种安全拦截功能。
Servlet 三大组件:Servlet、Filter、Listenter

当接收到一个请求,会经过各种Filter,也会经过Security提供的FilterChainProxy(过滤器链)。
过滤器链架构示意图

FilterChainProxy

FilterChainProxy 过滤器链,允许用户针对不同的请求定义不同的过滤器链。
FilterChainProxy示意图

SecurityFilterChain

SecurityFilterChain图示

使用

HttpSecurity

早期版本的Security需要实现WebSecurityConfigurerAdapter接口,才能操作HttpSecurity,最新版的只需要直接注入即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration  
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((request) -> {
//必须拥有某一个角色才能访问某一个路径
request.requestMatchers("/match1/user").hasRole("USER")
.requestMatchers("/match2/spam").hasRole("SPAM")
//所有请求都必须登录之后才能访问(除了上面配置的)
.anyRequest().authenticated();
});
return http.build();
}
}

MethodSecurity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootApplication
//开启方法注解,可以通过注解来限制必须存在某些角色或者权限的时候才能访问某个方法
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

@Service
public class MyService {

@Secured("ROLE_USER")
public String secure() {
return "Hello Security";
}

}

核心:

  • @EnableMethodSecurity :开启全局方法安全配置
    • @Secured:方法鉴权(不常用)
    • @PreAuthorize:方法鉴权,在方法调用之前
    • @PostAuthorize:方法鉴权,在方法调用之后
  • UserDetailsService:
    • 去数据库查询用户详细信息的service(用户基本信息、用户角色、用户权限)

Security场景的自动配置类:

  • SecurityAutoConfiguration
    • Security有关的配置在:SecurityProperties,以spring.security开头
    • SpringBootWebSecurityConfiguration
      • 默认的 SecurityFilterChain ,提供了一些默认的安全策略
        • 所有的请求都需要认证
        • 开启了表单登录
        • 开启了httpbasic登录
      • @EnableWebSecurity 生效
        • WebSecurityConfiguration
          • web安全配置(生效)
        • SpringWebMvcImportSelector
        • OAuth2ImportSelector
        • HttpSecurityConfiguration
          • http安全配置(生效)
        • @EnableGlobalAuthentication
          • 开启全局认证(生效)
          • AuthenticationConfiguration
    • SecurityDataConfiguration 未生效
  • SecurityFilterAutoConfiguration

实战

引入依赖

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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
<!-- Temporary explicit version to fix Thymeleaf bug -->
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

页面

首页
1
<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
Hello页
1
<h1>Hello</h1>
登录页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" /> </label>
</div>
<div>
<label> Password: <input type="password" name="password" /> </label>
</div>
<div><input type="submit" value="Sign In" /></div>
</form>
</body>
</html>

配置类

视图控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.securingweb;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("index");
registry.addViewController("/").setViewName("index");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
Security配置
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
package com.atguigu.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

/**
* @author lfy
* @Description
* @create 2023-03-08 16:54
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {


http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage("/login")
.permitAll()
)
.logout((logout) -> logout.permitAll());

return http.build();
}

@Bean
public UserDetailsService userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("USER")
.build();

return new InMemoryUserDetailsManager(user);
}
}

改造Hello页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6"
>
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">
Hello <span th:remove="tag" sec:authentication="name">thymeleaf</span>!
</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>

可观测性

可观测性 Observability

对线上应用进行观测、监控、预警….

  • 健康状态【组件状态、存活状态】Health
  • 运行指标【cpu、内存、垃圾收集、吞吐量、响应成功率】Metrics
  • 链路追踪
  • …

    SpringBoot Actuator

实战

场景引入

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

暴露指标

1
2
3
4
5
6
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露

访问数据

Endpoint

常用端点

ID 描述
auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件。
beans 显示应用程序中所有Spring Bean的完整列表。
caches 暴露可用的缓存。
conditions 显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops 显示所有@ConfigurationProperties。
env 暴露Spring的属性ConfigurableEnvironment
flyway 显示已应用的所有Flyway数据库迁移。
需要一个或多个Flyway组件。
health 显示应用程序运行状况信息。
httptrace 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info 显示应用程序信息。
integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core。
loggers 显示和修改应用程序中日志的配置。
liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics 显示当前应用程序的“指标”信息。
mappings 显示所有@RequestMapping路径列表。
scheduledtasks 显示应用程序中的计划任务。
sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown 使应用程序正常关闭。默认禁用。
startup 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup。
threaddump 执行线程转储。
heapdump 返回hprof堆转储文件。
jolokia 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core。
logfile 返回日志文件的内容(如果已设置logging.file.name或logging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus。

重要端点:threaddump、heapdump、metrics

定制端点

  • 健康监控:返回存活、死亡
  • 指标监控:次数、率
HealthEndpoint
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class MyHealthIndicator implements HealthIndicator {

@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
return Health.up().build();
}

}

构建Health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
1
2
3
4
5
management:
endpoint:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息
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
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {

/**
* 真实的检查方法
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//mongodb。 获取连接进行测试
Map<String,Object> map = new HashMap<>();
// 检查完成
if(1 == 2){
// builder.up(); //健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
// builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}


builder.withDetail("code",100)
.withDetails(map);

}
}
MetricsEndpoint
1
2
3
4
5
6
7
8
9
10
class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter");
}

public void hello() {
counter.increment();
}
}

监控案例落地

监控数据流向图示

安装 Prometheus + Grafana

1
2
3
4
5
6
7
#安装prometheus:时序数据库
docker run -p 9090:9090 -d \
-v pc:/etc/prometheus \
prom/prometheus

#安装grafana;默认账号密码 admin:admin
docker run -d --name=grafana -p 3000:3000 grafana/grafana

导入依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.10.6</version>
</dependency>
1
2
3
4
5
management:
endpoints:
web:
exposure: #暴露所有监控的端点
include: '*'

访问: http://localhost:8001/actuator/prometheus 验证,返回 prometheus 格式的所有指标

  • 部署java应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#安装上传工具
yum install lrzsz

#安装openjdk
# 下载openjdk
wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz

mkdir -p /opt/java
tar -xzf jdk-17_linux-x64_bin.tar.gz -C /opt/java/
sudo vi /etc/profile
#加入以下内容
export JAVA_HOME=/opt/java/jdk-17.0.7
export PATH=$PATH:$JAVA_HOME/bin

#环境变量生效
source /etc/profile

# 后台启动java应用
nohup java -jar boot3-14-actuator-0.0.1-SNAPSHOT.jar > output.log 2>&1 &

确认可以访问到: http://192.18.96.136:9999/actuator/prometheus

配置Prometheus拉取数据

1
2
3
4
5
6
7
8
## 修改 prometheus.yml 配置文件
scrape_configs:
- job_name: 'spring-boot-actuator-exporter'
metrics_path: '/actuator/prometheus' #指定抓取的路径
static_configs:
- targets: ['192.168.200.1:8001']
labels:
nodename: 'app-demo'

配置 Grafana 监控面板

  • 添加数据源(Prometheus)
  • 添加面板。可去 dashboard 市场找一个自己喜欢的面板,也可以自己开发面板;Dashboards | Grafana Labs

    效果

监控效果图示

AOT

AOT与JIT

AOT:Ahead-of-Time(提取编译):程序执行前,全部被编译成机器码
JIT:Just in Time(即时编译):程序边编译,边运行

编译:

  • 源代码(.c、.cpp、.go、.java…)===> 编译 ===> 机器码

语言:

  • 编译型语言:编译器
  • 解释型语言:解释器

Complier 与 Interpreter

Java:半编译半解释
在线编程

编译与解释语言图示

对比项 编译器 解释器
机器执行速度 快,因为源代码只需被转换一次 慢,因为每行代码都需要被解释执行
开发效率 慢,因为需要耗费大量时间编译 快,无需花费时间生成目标代码,更快的开发和测试
调试 难以调试编译器生成的目标代码 容易调试源代码,因为解释器一行一行地执行
可移植性(跨平台) 不同平台需要重新编译目标平台代码 同一份源码可以跨平台执行,因为每个平台会开发对应的解释器
学习难度 相对较高,需要了解源代码、编译器以及目标机器的知识 相对较低,无需了解机器的细节
错误检查 编译器可以在编译代码时检查错误 解释器只能在执行代码时检查错误
运行时增强 无 可以动态增强

AOT与JIT对比

JIT AOT
优点 1.具备实时调整能力
2.生成最优机器指令
3.根据代码运行情况优化内存占用
1.速度快,优化了运行时编译时间和内存消耗
2.程序初期就能达最高性能
3.加快程序启动速度
缺点 1.运行期边编译速度慢
2.初始编译不能达到最高性能
1.程序第一次编译占用时间长
2.牺牲高级语言一些特性

在OpenJDK的官方Wiki上,介绍了HotSpot虚拟机一个相对全面的、即时编译器(JIT) 中采用的优化技术列表
技术列表图1

技术列表图

可使用:-XX:+PrintCompilation 打印JIT编译信息

JVM架构

.java ====> .class ====> 机器码
JVM架构图示

JVM:既有解释器,又有编译器(JIT:即时编译)

Java的执行过程

建议阅读:

流程概要

java代码执行流程示意图

详细流程

热点代码:调用次数非常多的代码
详细流程图示

JVM编译器

JVM中集成了两种编译器,Client Compiler和Server Compiler

  • Client Compiler注重启动速度和局部的优化
  • Server Compiler更加关注全局优化,性能更好,但

Client Compiler

  • HotSpot VM带有一个 Client Compiler C1编译器
  • 这种编译器启动速度快,但是性能比较 Server Compiler 来说会差一点
  • 编译后的机器码执行效率没有C2的高

Server Compiler

  • HotSpot虚拟机中使用的Server Compiler有两种:C2和Graal
  • 在HotSpot VM中,默认的Server Compiler是C2编译器

分层编译

Java7开始引入了分层编译(Tiered Compiler)的概念,它结合了C1和C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将JVM的执行状态分为了五个层次,五个层次分别是:

  • 解释执行;
  • 执行不带profiling的C1代码;
  • 执行仅带方法调用次数以及循环回边执行次数的profiling的C1代码;
  • 执行带所有profiling的C1代码;
  • 执行C2代码。

profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数。
分层编译图示

  • 图中第①条路径,代表编译的一般情况,热点方法从解释执行到被3层的C1编译,最后被4层的C2编译。
  • 如果方法比较小(比如java服务中常见的getter/setter方法),3层的profiling没有收集到有价值的数据,JVM就会断定该方法对于C1代码和C2代码执行效率相同,就会执行图中第②条路径。在这种情况下,JVM会在3层编译之后,放弃进入C2编译,*直接选择用1层的C1编译运行***。
  • 在C1忙碌的情况下,执行图中第③条路径,在解释执行过程中对程序进行profiling,根据信息直接由第4层的C2编译。
  • 前文中提到C1中的执行效率是1层>2层>3层,第3层一般要比第2层慢35%以上,所以在C2忙碌的情况下,执行图中第④条路径。这时方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间。
  • 如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第⑤条执行路径代码的就是反优化。

总的来说,C1编译速度更快,C2的编译质量更高,分层编译的不同编译路径,也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK8开始,JVM默认开始分层编译。

存在的问题:

  • java应用如果用jar,解释执行,热点代码才编译成机器码;初始启动速度慢,初始处理请求数量少。
  • 大型云平台,要求每一种应用都必须秒级启动,每个应用都要求效率高。

希望的效果:

  • java应用也能提前被编译成机器码,随时急速启动,一启动就急速运行,最高性能
  • 编译成机器码的好处:
    • 在其他服务器不需要安装Java环境,可以直接运行(同类型服务器)

原生镜像:native-image (机器码、本地镜像)

  • 把应用打包成能适配本机平台的可执行文件(机器码、本地镜像)

GeaalVM

GraalVM

GraalVM是一个高性能的JDK,旨在加速用Java和其他JVM语言编写的应用程序的执行,同时还提供JavaScript、Python和许多其他流行语言的运行时。
GraalVM提供了两种运行Java应用程序的方式:

    1. 在HotSpot JVM上使用Graal即时(JIT) 编译器
    1. 作为预先编译(AOT) 的本机可执行文件运行(本地镜像)
      GraalVM的多语言能力使得在单个应用程序中混合多种编程语言成为可能,同时消除了外部语言调用的成本。

架构

GraalVM架构图示

安装

  • 跨平台提供原生镜像原理
    跨平台原理图示

VisualStudio

GraalVM在Window平台上打包原生镜像需要依赖于Visual Studio 环境

VisualStudio安装图示1

VisualStudio安装图示2
别选中文
VisualStudio安装图示3

VisualStudio安装图示4

记住安装地址。

GraalVM

安装

下载 GraalVM + native-image
GraalVM安装图示1

GraalVM安装图示2

GraalVM安装图示3

GraalVM安装图示4

配置

修改 JAVA_HOME 与 path,指向新的bin(GraalVM是用来平替JDK的)
修改后打开cmd执行java --version命令,验证环境为GraalVM提供的即可。

依赖

最新版的GraalVM中已经携带了native-image,无需再进行下面的安装配置
安装 native-image 依赖:

  1. 网络环境好:参考:https://www.graalvm.org/latest/reference-manual/native-image/#install-native-image
1
gu install native-image
  1. 网络不好,使用我们下载的离线jar;native-image-xxx.jar文件
    1
    gu install --file native-image-installable-svm-java17-windows-amd64-22.3.2.jar
    验证
    1
    native-image

测试

创建项目

创建普通java项目,编写HelloWorld类

  • 使用mvn clean package进行打包
  • 确认jar包是否可以执行java -jar xxx.jar

普通maven项目打可执行jar可以使用插件maven-jar-plugin指定主类入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<build>  
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.ys.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>

或者使用插件maven-shade-plugin,可以把依赖的jar也一起打进jar包

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
<build>  
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.ys.App</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

编译镜像

  • 编译为原生镜像(native-image):使用native-tools终端
    native-tools终端图示

-H:-CheckToolchain环境检查,打包之前会自动检查本地环境,检查有可能会不通过,如果确认本地环境没有问题,那么可以添加此参数跳过环境检查

1
2
3
4
#通过jar包打原生镜像,需要指定入口主类,通过入口编译整个jar
native-image -H:-CheckToolchain -cp boot3-09-common-1.0-SNAPSHOT.jar com.ys.App -o App
#通过class字节文件打包原生镜像
native-image -H:-CheckToolchain -cp classes com.ys.App -o App

Linux平台测试

  • 安装gcc等环境
    1
    2
    yum install lrzsz
    sudo yum install gcc glibc-devel zlib-devel
1
2
3
4
5
6
7
8
tar -zxvf graalvm-ce-java17-linux-amd64-22.3.2.tar.gz -C /opt/java/

sudo vim /etc/profile
#修改以下内容
export JAVA_HOME=/opt/java/graalvm-ce-java17-22.3.2
export PATH=$PATH:$JAVA_HOME/bin

source /etc/profile
  • 安装native-image

    1
    gu install --file native-image-installable-svm-java17-linux-amd64-22.3.2.jar
  • 使用native-image编译jar为原生程序

    1
    native-image -cp xxx.jar org.example.App

    SpringBoot整合

    依赖导入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <build>
    <plugins>
    <plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    </plugin>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>

生成native-image

  1. 运行aot提前处理命令: mvn springboot:process
  2. 运行native打包: mvn -Pnative native:build
    1
    2
    # 推荐加上 -Pnative
    mvn -Pnative native:build -f pom.xml

常见问题

可能提示如下各种错误,无法构建原生镜像,需要配置环境变量;

  • 出现cl.exe找不到
  • 出现乱码
  • 提示no include path set
  • 提示fatal error LNK1104: cannot open file 'LIBCMT.lib'
  • 提示各种其他找不到

需要修改三个环境变量:Path、INCLUDE、lib

  • Path:添加如下值
    1
    D:\APP\develop\VS\Enterprise\VC\Tools\MSVC\14.35.32215\bin\Hostx64\x64
  • 新建INCLUDE环境变量(个人电脑win11下不存在在对应路径无法配置):值为
    1
    C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um;C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\winrt
  • 新建lib环境变量(不存在对应路径),值为
    1
    C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\lib\x64;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\um\x64;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\ucrt\x64