环境准备
云服务器
- 阿里云、腾讯云、华为云 服务器开通; 按量付费,省钱省心
- 安装以下组件
- docker
- redis
- kafka
- prometheus
- grafana
- https://github.com/kingToolbox/WindTerm/releases/download/2.5.0/WindTerm_2.5.0_Windows_Portable_x86_64.zip下载windterm
开通云服务器之后,要在安全组里面设置规则,放行端口
Docker安装
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
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
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
启动环境
docker compose -f docker-compose.yml up -d
验证
- Redis:你的ip:6379
- 填写表单,下载官方可视化工具:
- Redis Insight
- Kafka:你的ip:9092
- idea安装大数据插件
- Prometheus:你的ip:9090
- 直接浏览器访问
- Grafana:你的ip:3000
- 直接浏览器访问
NoSQL
Redis整合
场景整合
依赖导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置
spring.data.redis.host=服务器的ip
spring.data.redis.port=6379
测试
package com.ling.boot3.redis;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import java.util.UUID;
@SpringBootTest
class Boot309RedisApplicationTests {
@Autowired
StringRedisTemplate redisTemplate;
/**
* string: 普通字符串 redisTemplate.opsForValue()
*/
@Test
void contextLoads() {
redisTemplate.opsForValue().set("haha", UUID.randomUUID().toString());
String haha = redisTemplate.opsForValue().get("haha");
System.out.println("haha = " + haha);
}
/**
* list: 列表 redisTemplate.opsForList()
*/
@Test
void testList(){
String listName = "listtest";
redisTemplate.opsForList().leftPush(listName, "1");
redisTemplate.opsForList().leftPush(listName, "2");
redisTemplate.opsForList().leftPush(listName, "3");
Assertions.assertEquals("3",redisTemplate.opsForList().leftPop(listName));
}
/**
* set: 集合 redisTemplate.opsForSet()
*/
@Test
void testSet(){
String setName = "settest";
// 给集合中添加元素
redisTemplate.opsForSet().add(setName, "1", "2", "3", "3");
Boolean member = redisTemplate.opsForSet().isMember(setName, "2");
Assertions.assertTrue(member);
Boolean member1 = redisTemplate.opsForSet().isMember(setName, "5");
Assertions.assertFalse(member1);
}
/**
* zset: 有序集合 redisTemplate.opsForZSet()
*/
@Test
void testZSet(){
String zsetName = "zsettest";
redisTemplate.opsForZSet().add(zsetName, "ling", 90.00);
redisTemplate.opsForZSet().add(zsetName, "张三", 99.00);
redisTemplate.opsForZSet().add(zsetName, "李四", 9.00);
redisTemplate.opsForZSet().add(zsetName, "王五", 97.10);
ZSetOperations.TypedTuple<String> popMax = redisTemplate.opsForZSet().popMax(zsetName);
String value = popMax.getValue();
Double score = popMax.getScore();
System.out.println(value + " " + score);
}
/**
* hash: 哈希表 k:v redisTemplate.opsForHash()
*/
@Test
void testHash(){
String hashName = "hashtest";
redisTemplate.opsForHash().put(hashName, "name", "张三");
redisTemplate.opsForHash().put(hashName, "age", "18");
System.out.println(redisTemplate.opsForHash().get(hashName, "name"));
}
}
自动配置原理
- META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中导入了RedisAutoConfiguration、RedisReactiveAutoConfiguration 和RedisRepositoriesAutoConfiguration。所有属性绑定在 RedisProperties 中
- RedisReactiveAutoConfiguration属于响应式编程,不用管。RedisRepositoriesAutoConfiguration属于 JPA 操作,也不用管
- RedisAutoConfiguration 配置了以下组件
- LettuceConnectionConfiguration: 给容器中注入了连接工厂LettuceConnectionFactory,和操作 redis 的客户端 DefaultClientResources
- RedisTemplate<Object, Object>: 可给 redis 中存储任意对象,会使用 jdk 默认序列化方式。
- StringRedisTemplate<Object, Object>:给 redis 中存储字符串,如果要存对象,需要开发人员自己进行序列化。key-value 都是字符串进行操作
-
package com.ling.boot3.redis.controller; import com.ling.boot3.redis.entity.Person; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Date; /** * @program: spring-boot-3 * @author: lingsuu * @create: 2024-12-30 10:38 */ @RestController public class RedisController { @Autowired StringRedisTemplate stringRedisTemplate; // 为了后来系统的兼容性,应该将所有的对象用json的方式进行保存 @Autowired // 如果给redis中保存数据,会使用默认的序列化机制,导致redis保存的对象不可视 RedisTemplate<Object, Object> redisTemplate; @GetMapping("/count") public String count(){ Long hello = stringRedisTemplate.opsForValue().increment("hello"); // 常见的数据类型 k : v value可以有很多类型 // string: 普通字符串 redisTemplate.opsForValue() // list: 列表 redisTemplate.opsForList() // set: 集合 redisTemplate.opsForSet() // zset: 有序集合 redisTemplate.opsForZSet() // hash: 哈希表 k:v redisTemplate.opsForHash() return "访问了【" + hello + "】次"; } @GetMapping("/person/save") public String savePerson(){ Person person = new Person(1L, "张三", 18, new Date()); // 1、序列化: 将对象转为字符串 redisTemplate.opsForValue().set("person", person); return "ok"; } @GetMapping("/person/get") public Person getPerson(){ Person person = (Person) redisTemplate.opsForValue().get("person"); return person; } }
-
定制化
序列化机制
@Configuration
public class AppRedisConfiguration {
/**
* 允许Object类ing的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:可以使用以下切换
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce.core</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 切换jedis自拍围殴擦破自拍redis的底层客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
配置参考
spring.data.redis.host=47.120.61.151
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
spring.data.redis.client-type=jedis
spring.data.redis.jedis.pool.enabled=true
spring.data.redis.jedis.pool.max-active=8
接口文档
OpenAPI 3 与 Swagger
Swagger 可以快速生成实时接口文档,方便前后开发人员进行协调沟通。遵循 OpenAPI 规范。
OpenAPI 3 架构
整合
导入场景
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
配置
# /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,配置如下
/**
* 分组设置
* @return
*/
@Bean
public GroupedOpenApi empApi() {
return GroupedOpenApi.builder()
.group("员工管理")
.pathsToMatch("/emp/**", "/emps")
.build();
}
@Bean
public GroupedOpenApi deptApi() {
return GroupedOpenApi.builder()
.group("部门管理")
.pathsToMatch("/dept/**", "/depts")
.build();
}
OpenAPI配置
@Bean
public OpenAPI docOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("SpringBoot3-CRUD API")
.description("专门测试接口文档")
.version("v0.0.1")
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
.externalDocs(new ExternalDocumentation()
.description("哈哈 Documentation")
.url("https://springshop.wiki.github.org/docs"));
}
远程调用
RPC(Remote Producedure Call):远程过程调用
- 本地过程调用:
- 就好比两个方法:a(); b();
- a如果想调用b,那么就需要这两个方法都在同一个JVM上运行
- a() { b(); };
- 远程过程调用:
- 服务提供者
- 服务消费者:调用服务提供者提供的功能
- 通过连接对方服务器进行请求\响应交互,来实现调用效果
API/SDK的区别是什么?
- api:接口(Application Programming Interface)
- 远程提供功能
- sdk:开发工具包(Software Development Kit)
- 导入 jar 包,直接调用功能即可
开发过程中,我们经常需要调用别人写的功能
- 如果是内部微服务,可以通过依赖cloud、注册中心、openfeign等进行调用
- 如果是外部暴露的,可以发送 http 请求、或遵循外部协议进行调用
SpringBoot 整合提供了很多方式进行远程调用
- 轻量级客户端方式
- RestTemplate: 普通开发
- WebClient: 响应式编程开发
- Http Interface: 声明式编程
- Spring Cloud分布式解决方案方式
- Spring Cloud OpenFeign
- 第三方框架
- Dubbo
- gRPC
- ...
WebClient
是一种非阻塞、响应式的HTTP的客户端
创建与配置
发请求:
- 请求方式:GET\POST\DELETE\XXXX
- 请求路径:/xxx
- 请求参数:aa=bb&cc=dd&xxx
- 请求头:aa=bb,cc=dd
- 请求体
创建 WebClient:
- WebClient.create()
- WebClient.create(String baseUri)
还可以使用 WebClient.builder() 配置更多参数项:
- uriBuilderFactory: 自定义UriBuilderFactory ,定义 baseurl.
- defaultUriVariables: 默认 uri 变量.
- defaultHeader: 每个请求默认头.
- defaultCookie: 每个请求默认 cookie.
- defaultRequest: Consumer 自定义每个请求.
- filter: 过滤 client 发送的每个请求
- exchangeStrategies: HTTP 消息 reader/writer 自定义.
- clientConnector: HTTP client 库设置.
// 创建一个给 http://ling.org 发送请求的客户端
WebClient client = WebClient.create("http://ling.org");
获取响应
retrieve() 方法用来声明如何提取响应数据。比如:
//获取响应完整信息
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、响应式-单个数据
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);
调用天气预报的API
需要注意的是
在这里我们不能通过阻塞的方法来模拟响应式编程
所以不需要在定义请求体的末尾加上 .block()
package com.ling.boot3.rpc.service;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
/**
* @program: spring-boot-3
* @author: lingsuu
* @create: 2025-01-05 14:53
*/
@Service
public class WeatherService {
public Mono<String> weather(String city){
// 远程调用阿里云的API
// 1、创建一个远程调用的客户端
// 创建客户端的时候有两种方法,一种是直接在创建的时候添加路径
// WebClient webClient = WebClient.create("https://ali-weather.showapi.com/area-to-weather-date");
// 另外一种是不添加路径,那么你在定义请求行为的时候就要手动添加路径,并且如果有参数/属性的话也可以在定义请求行为的时候直接定义
WebClient webClient = WebClient.create();
// 由于定义路径中若想传入参数的值,那么就需要一个Map类型的参数
Map<String, String> params = new HashMap<>();
params.put("area", city);
// 2、定义请求行为
// String json = webClient.get() // 定义请求方式
// .uri("https://ali-weather.showapi.com/area-to-weather-date?area={area}", params)
// .accept(MediaType.APPLICATION_JSON) // 定义相应的内容类型
// .header("Authorization", "APPCODE eade682e5c5c4b83ae3a63015a70a6c5") // 定义请求头
.attribute("area", "西安") // 由于我们的请求方式是GET,实际上area并不是请求参数,我们将其归于属性一类
// .retrieve() // 这个时候就可以获取到json串了
// .bodyToMono(String.class) // 定义响应的数据类型
// .block();// 运用阻塞方法 如果运用阻塞,那么我们获取到的就是Mono对象中的值,如果不阻塞,那么我们获取到的就是Mono对象本身
Mono<String> mono = webClient.get() // 定义请求方式
.uri("https://ali-weather.showapi.com/area-to-weather-date?area={area}", params)
.accept(MediaType.APPLICATION_JSON) // 定义相应的内容类型
.header("Authorization", "APPCODE eade682e5c5c4b83ae3a63015a70a6c5") // 定义请求头
// .attribute("area", "西安") // 由于我们的请求方式是GET,实际上area并不是请求参数,我们将其归于属性一类
.retrieve() // 这个时候就可以获取到json串了
.bodyToMono(String.class);// 定义响应的数据类型
return mono;
}
}
HTTP Interface
Spring 允许我们通过定义接口的方式,给任意位置发送 http 请求,实现远程调用,可以用来简化 HTTP 远程访问。需要webflux场景才可
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
定义接口
public interface BingService {
@GetExchange(url = "/search")
String search(@RequestParam("q") String keyword);
}
创建代理&测试
@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);
}
}
消息服务
消息队列——场景
异步
解耦
削峰
缓冲
消息队列——Kafka
消息模式
Kafka工作原理
SpringBoot整合
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
配置
spring.kafka.bootstrap-servers[0]=8.155.47.26:9092
修改
C:\Windows\System32\drivers\etc\hosts
文件,配置8.155.47.26 kafka
消息发送
@SpringBootTest
class Boot312MessageApplicationTests {
@Autowired
KafkaTemplate kafkaTemplate;
@Test
void contextLoads() {
StopWatch stopWatch = new StopWatch();
CompletableFuture[] futures = new CompletableFuture[10000];
stopWatch.start();
for (int i = 0; i < 10000; i++) {
CompletableFuture future = kafkaTemplate.send("news", "haha", "哈哈哈");
futures[i] = future;
}
CompletableFuture.allOf(futures).join();
stopWatch.stop();
long totalTimeMillis = stopWatch.getTotalTimeMillis();
System.out.println("10000个消息发送完成,时间:" + totalTimeMillis);
}
@Test
void send() {
CompletableFuture future = kafkaTemplate.send("news", "person", new person(1L, "张三", "haha@qq.com"));
future.join();
System.out.println("消息发送完成");
}
}
消息监听
@Component
public class MyHahaTopicListener {
// 默认的监听是从消息队列最后一个消息开始的
@KafkaListener(topics = "news", groupId = "haha")
public void haha(ConsumerRecord record) {
// 1. 获取当前消息的各种详细信息
String topic = record.topic();
Object key = record.key();
Object value = record.value();
System.out.println("topic:"+topic+" key:"+key+" value:"+value);
}
// 拿到之前的消息
@KafkaListener(groupId = "hehe", topicPartitions = {
@TopicPartition(topic = "news", partitionOffsets = {
@PartitionOffset(partition = "0", initialOffset = "0")
})
})
public void hehe(ConsumerRecord record) {
Object key = record.key();
Object value = record.value();
System.out.println("key:"+key+" value:"+value);
}
}
参数配置
消费者
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
生产者
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
spring.kafka.producer.properties[spring.json.add.type.headers]=false
自动配置原理
kafka 自动配置在KafkaAutoConfiguration
- 容器中放了 KafkaTemplate 可以进行消息收发
- 容器中放了 KafkaAdmin 可以进行 Kafka 的管理,比如创建 topic 等
- kafka 的配置在KafkaProperties中
- @EnableKafka可以开启基于注解的模式
Web安全
- Apache Shiro
- Spring Security
- 自研:Filter
Spring Security
安全架构
认证:Authentication
who are you?
登录系统,用户系统
授权
what are you allowed to do?
权限管理,用户授权
攻击防护
- XSS(Cross-site scripting)
- CSRF(Cross-site request forgery)
- CORS(Cross-Origin Resource Sharing)
- SQL注入
- ...
扩展——权限模型
RBAC(Role Based Access Controll)
- 用户(t_user)
- id,username,password,xxx
- 1,zhangsan
- 2,lisi
- 用户_角色(t_user_role)【N对N关系需要中间表】
- zhangsan, admin
- zhangsan,common_user
- lisi, hr
- lisi, common_user
- 角色(t_role)
- id,role_name
- admin
- hr
- common_user
- 角色_权限(t_role_perm)
- admin, 文件r
- admin, 文件w
- admin, 文件执行
- admin, 订单query,create,xxx
- hr, 文件r
- 权限(t_permission)
- id,perm_id
- 文件 r,w,x
- 订单 query,create,xxx
ACL(Access Controll List)
直接用户和权限挂钩
- 用户(t_user)
- zhangsan
- lisi
- 用户_权限(t_user_perm)
- zhangsan,文件 r
- zhangsan,文件 x
- zhangsan,订单 query
- 权限(t_permission)
- id,perm_id
- 文件 r,w,x
- 订单 query,create,xxx
Spring Security原理
过滤器链架构
Spring Security利用 FilterChainProxy 封装一系列拦截器链,实现各种安全拦截功能
Servlet三大组件:Servlet、Filter、Listener
FilterChainProxy
SecurityFilterChain
使用
HttpSecurity
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/match1/**")
.authorizeRequests()
.antMatchers("/match1/user").hasRole("USER")
.antMatchers("/match1/spam").hasRole("SPAM")
.anyRequest().isAuthenticated();
}
}
MethodSecurity
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}
@Service
public class MyService {
@Secured("ROLE_USER")
public String secure() {
return "Hello Security";
}
}
核心
- WebSecurityConfigurerAdapter
- @EnableGlobalMethodSecurity: 开启全局方法安全配置
-
- @Secured
- @PreAuthorize
- @PostAuthorize
- UserDetailService: 去数据库查询用户详细信息的service(用户基本信息、用户角色、用户权限)
实战
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</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.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
页面
首页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Welcome to Spring Boot Security
<a th:href="@{/hello}">hello</a>
<a th:href="@{/world}">world</a>
</body>
</html>
登录页
<!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>
Security配置
package com.ling.boot3.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
/**
* @program: spring-boot-3
* @author: lingsuu
* @create: 2025-03-11 20:52
*/
/**
* 1、自定义请求的授权规则 http.authorizeHttpRequests
* 2、自定义登陆规则 http.formLogin
* 3、自定义用户信息的查询规则
* 4、开启方法级别的精确权限控制
*/
@EnableMethodSecurity
@Configuration
public class AppSecurityConfiguration {
@Bean
@Order(2147483642)
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(register -> {
register.requestMatchers("/").permitAll() // 首页所有人都可以访问
.anyRequest().authenticated(); // 其他页面需要认证
});
// 开启表单登录
http.formLogin(formLogin -> {
formLogin.loginPage("/login").permitAll(); // 登录页面所有人都可以访问
});
return http.build();
}
@Bean // 查询用户信息
UserDetailsService userDetailsService(PasswordEncoder passwordEncoder){
UserDetails zhangsan = User.withUsername("zhangsan")
.password(passwordEncoder.encode("123456")) // 使用密码加密器加密密码
.roles("admin", "hr")
.authorities("file_read", "file_write")
.build();
UserDetails lisi = User.withUsername("lisi")
.password(passwordEncoder.encode("123456"))
.roles("hr")
.authorities("file_read")
.build();
UserDetails wangwu = User.withUsername("wangwu")
.password(passwordEncoder.encode("123456"))
.roles("admin")
.authorities("file_write","world_exe")
.build();
// 默认内存中保存所有用户信息
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(zhangsan,lisi,wangwu);
return manager;
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
可观测性
可观测性 Observability
对线上应用进行观测、监控、预警...
- 健康状况【组件状态、存活状态】Health
- 运行指标【cpu、内存、垃圾回收、吞吐量、响应成功率...】Metrics
- 链路追踪
- ...
SpringBoot Actuator
实战
场景引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
暴露指标
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露
访问数据
- 访问 http://localhost:8080/actuator;展示出所有可以用的监控端点
- http://localhost:8080/actuator/beans
- http://localhost:8080/actuator/configprops
- http://localhost:8080/actuator/metrics
- http://localhost:8080/actuator/metrics/jvm.gc.pause
- http://localhost:8080/actuator/endpointName/detailPath
Endpoint
常用端点
ID | 描述 |
| 暴露当前应用程序的审核事件信息。需要一个 |
| 显示应用程序中所有Spring Bean的完整列表。 |
| 暴露可用的缓存。 |
| 显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
| 显示所有 |
| 暴露Spring的属性 |
| 显示已应用的所有Flyway数据库迁移。 |
| 显示应用程序运行状况信息。 |
| 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个 |
| 显示应用程序信息。 |
| 显示Spring |
| 显示和修改应用程序中日志的配置。 |
| 显示已应用的所有Liquibase数据库迁移。需要一个或多个 |
| 显示当前应用程序的“指标”信息。 |
| 显示所有 |
| 显示应用程序中的计划任务。 |
| 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。 |
| 使应用程序正常关闭。默认禁用。 |
| 显示由 |
| 执行线程转储。 |
| 返回 |
| 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖 |
| 返回日志文件的内容(如果已设置 |
| 以Prometheus服务器可以抓取的格式公开指标。需要依赖 |
定制端点
- 健康监控:返回存活、死亡
- 指标监控:次数、率
HealthEndpoint
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();
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息
@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
class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter");
}
public void hello() {
counter.increment();
}
}
监控案例落地
基于 Prometheus + Grafana
安装 Prometheus + Grafana
#安装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
导入依赖
<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>
management:
endpoints:
web:
exposure: #暴露所有监控的端点
include: '*'
访问: http://localhost:8001/actuator/prometheus 验证,返回 prometheus 格式的所有指标
部署java应用
#安装上传工具
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 &
配置Prometheus拉取数据
## 修改 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.具备实时调整能力 | 1.速度快,优化了运行时编译时间和内存消耗 |
1.运行期边编译速度慢 | 1.程序第一次编译占用时间长 |
在 OpenJDK 的官方 Wiki 上,介绍了HotSpot 虚拟机一个相对比较全面的、即时编译器(JIT)中采用的优化技术列表。
可使用:-XX:+PrintCompilation 打印JIT编译信息
JVM架构
.java === .class === 机器码
JVM: 既有解释器,又有编辑器(JIT:即时编译);
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编译器。
分层编译
Java 7开始引入了分层编译(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根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK 8开始,JVM默认开启分层编译。
云原生:Cloud Native; Java小改版;
最好的效果:
存在的问题:
- java应用如果用jar,解释执行,热点代码才编译成机器码;初始启动速度慢,初始处理请求数量少。
- 大型云平台,要求每一种应用都必须秒级启动。每个应用都要求效率高。
希望的效果:
- java应用也能提前被编译成机器码,随时急速启动,一启动就急速运行,最高性能
- 编译成机器码的好处:
-
- 另外的服务器还需要安装Java环境
- 编译成机器码的,可以在这个平台 Windows X64 直接运行。
原生镜像:native-image(机器码、本地镜像)
- 把应用打包成能适配本机平台 的可执行文件(机器码、本地镜像)
GraalVM
GraalVM是一个高性能的JDK,旨在加速用Java和其他JVM语言编写的应用程序的执行,同时还提供JavaScript、Python和许多其他流行语言的运行时。
GraalVM提供了两种运行Java应用程序的方式:
- 1. 在HotSpot JVM上使用Graal即时(JIT)编译器
- 2. 作为预先编译(AOT)的本机可执行文件运行(本地镜像)。
GraalVM的多语言能力使得在单个应用程序中混合多种编程语言成为可能,同时消除了外部语言调用的成本。
架构
安装
跨平台提供原生镜像原理:
VisualStudio
GraaIVM
安装
下载 GraalVM + native-image
配置
修改 JAVA_HOME 与 Path,指向新bin路径
验证JDK环境为GraalVM提供的即可:
依赖
安装 native-image 依赖:
网络环境好:参考:Native Image
gu install native-image
网络不好,使用我们下载的离线jar;native-image-xxx.jar
文件
gu install --file native-image-installable-svm-java17-windows-amd64-22.3.2.jar
验证
native-image
测试
创建项目
- 创建普通java项目。编写HelloWorld类;
-
- 使用
mvn clean package
进行打包 - 确认jar包是否可以执行
java -jar xxx.jar
- 可能需要给
MANIFEST.MF
添加Main-Class: 你的主类
- 使用
编译镜像
- 编译为原生镜像(native-image):使用
native-tools
终端
#从入口开始,编译整个jar
native-image -cp boot3-15-aot-common-1.0-SNAPSHOT.jar com.atguigu.MainApplication -o Haha
#编译某个类【必须有main入口方法,否则无法编译】
native-image -cp .\classes org.example.App
Linux平台测试
安装gcc等环境
yum install lrzsz
sudo yum install gcc glibc-devel zlib-devel
下载安装配置Linux下的GraalVM、native-image
-
- 下载:Download GraalVM
- 安装:GraalVM、native-image
- 配置:JAVA环境变量为GraalVM
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
gu install --file native-image-installable-svm-java17-linux-amd64-22.3.2.jar
使用native-image编译jar为原生程序
native-image -cp xxx.jar org.example.App
SpringBoot整合
依赖导入
<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-aot
2、运行native打包:mvn -Pnative native:build
# 推荐加上 -Pnative
mvn -Pnative native:build -f pom.xml
常见问题
可能提示如下各种错误,无法构建原生镜像,需要配置环境变量;
- 出现
cl.exe
找不到错误 - 出现乱码
- 提示
no include path set
- 提示fatal error LNK1104: cannot open file 'LIBCMT.lib'
- 提示 LINK : fatal error LNK1104: cannot open file 'kernel32.lib'
- 提示各种其他找不到
需要修改三个环境变量:Path
、INCLUDE
、lib
- 1、 Path:添加如下值
-
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.33.31629\bin\Hostx64\x64
- 2、新建
INCLUDE
环境变量:值为
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
- 3、新建
lib
环境变量:值为
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