SpringBoot3——场景整合

环境准备

云服务器

开通云服务器之后,要在安全组里面设置规则,放行端口

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
  • 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 规范。

文档:https://springdoc.org/v2/

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整合

参照:Overview :: Spring Kafka

<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

  1. 容器中放了 KafkaTemplate 可以进行消息收发
  2. 容器中放了 KafkaAdmin 可以进行 Kafka 的管理,比如创建 topic 等
  3. kafka 的配置在KafkaProperties中
  4. @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方式暴露
访问数据

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.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。

prometheus

以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-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:半编译半解释

https://anycodes.cn/editor

对比项

编译器

解释器

机器执行速度

,因为源代码只需被转换一次

,因为每行代码都需要被解释执行

开发效率

,因为需要耗费大量时间编译

,无需花费时间生成目标代码,更快的开发和测试

调试

难以调试编译器生成的目标代码

容易调试源代码,因为解释器一行一行地执行

可移植性(跨平台)

不同平台需要重新编译目标平台代码

同一份源码可以跨平台执行,因为每个平台会开发对应的解释器

学习难度

相对较高,需要了解源代码、编译器以及目标机器的知识

相对较低,无需了解机器的细节

错误检查

编译器可以在编译代码时检查错误

解释器只能在执行代码时检查错误

运行时增强

可以动态增强

AOT与JIT对比

JIT

AOT

1.具备实时调整能力
2.生成最优机器指令
3.根据代码运行情况优化内存占用

1.速度快,优化了运行时编译时间和内存消耗
2.程序初期就能达最高性能
3.加快程序启动速度

1.运行期边编译速度慢
2.初始编译不能达到最高性能

1.程序第一次编译占用时间长
2.牺牲高级语言一些特性

在 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有两种:C2Graal
  • 在Hotspot VM中,默认的Server Compiler是C2编译器。

分层编译

Java 7开始引入了分层编译(Tiered Compiler)的概念,它结合了C1C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将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

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

GraalVM提供了两种运行Java应用程序的方式:

  • 1. 在HotSpot JVM上使用Graal即时(JIT)编译器
  • 2. 作为预先编译(AOT)的本机可执行文件运行(本地镜像)。

GraalVM的多语言能力使得在单个应用程序中混合多种编程语言成为可能,同时消除了外部语言调用的成本。

架构

安装

跨平台提供原生镜像原理:

VisualStudio

免费的开发人员软件和服务 - Visual Studio

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'
  • 提示各种其他找不到

需要修改三个环境变量PathINCLUDElib

  • 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值