-
Spring Boot 异步处理全面详解:从基础到高级应用作者:北辰alk关键词:Spring Boot、异步处理、@Async、线程池、性能优化1. 引言在现代Web应用开发中,高并发和快速响应是系统设计的重要目标。传统的同步处理方式在面对大量耗时操作时,会导致请求线程阻塞,降低系统的吞吐量。Spring Boot提供了强大的异步处理能力,能够有效提升系统性能和用户体验。本文将全面深入地介绍Spring Boot中的异步处理机制,涵盖从基础使用到高级应用的各个方面,包括异步配置、线程池优化、异常处理、事务管理等,并通过实际代码示例和流程图帮助读者彻底掌握这一重要技术。2. 异步处理基础概念2.1 同步 vs 异步同步处理:请求发起后,必须等待任务完成才能继续执行线程处于阻塞状态,资源利用率低编程模型简单,易于理解和调试异步处理:请求发起后,立即返回,任务在后台执行线程不会阻塞,可以继续处理其他请求提高系统吞吐量和资源利用率2.2 异步处理适用场景耗时IO操作:文件上传、邮件发送、短信通知批量数据处理:数据导入导出、报表生成第三方API调用:支付接口、外部服务集成日志记录:操作日志、系统监控数据收集3. Spring Boot异步处理核心注解3.1 启用异步支持在Spring Boot应用中,首先需要启用异步处理功能:@Configuration@EnableAsyncpublic class AsyncConfig { // 异步配置类}或者直接在主应用类上添加注解:@SpringBootApplication@EnableAsyncpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}3.2 @Async注解详解@Async注解用于标记异步执行的方法:@Servicepublic class AsyncService { @Async public void processTask(String taskName) { // 异步执行的任务逻辑 System.out.println("处理任务: " + taskName + ",线程: " + Thread.currentThread().getName()); }}4. 异步方法返回值处理4.1 无返回值异步方法@Servicepublic class NotificationService { @Async public void sendEmail(String to, String subject, String content) { try { // 模拟邮件发送耗时 Thread.sleep(3000); System.out.println("邮件发送成功至: " + to); System.out.println("主题: " + subject); System.out.println("内容: " + content); System.out.println("执行线程: " + Thread.currentThread().getName()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }}4.2 有返回值异步方法@Servicepublic class CalculationService { @Async public CompletableFuture<Integer> calculateSum(int start, int end) { System.out.println("开始计算从 " + start + " 到 " + end + " 的和"); System.out.println("计算线程: " + Thread.currentThread().getName()); int sum = 0; for (int i = start; i <= end; i++) { sum += i; try { Thread.sleep(10); // 模拟计算耗时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } return CompletableFuture.completedFuture(sum); } @Async public CompletableFuture<String> processData(String data) { return CompletableFuture.supplyAsync(() -> { try { // 模拟数据处理 Thread.sleep(2000); return "处理结果: " + data.toUpperCase(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); }}5. 自定义线程池配置5.1 基础线程池配置@Configuration@EnableAsyncpublic class AsyncConfig { @Bean("taskExecutor") public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 核心线程数 executor.setCorePoolSize(10); // 最大线程数 executor.setMaxPoolSize(20); // 队列容量 executor.setQueueCapacity(200); // 线程活跃时间(秒) executor.setKeepAliveSeconds(60); // 线程名前缀 executor.setThreadNamePrefix("async-task-"); // 拒绝策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待所有任务结束后再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 等待时间 executor.setAwaitTerminationSeconds(60); executor.initialize(); return executor; }}5.2 多个线程池配置@Configuration@EnableAsyncpublic class MultipleAsyncConfig { // CPU密集型任务线程池 @Bean("cpuIntensiveTaskExecutor") public ThreadPoolTaskExecutor cpuIntensiveTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); int corePoolSize = Runtime.getRuntime().availableProcessors(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(corePoolSize * 2); executor.setQueueCapacity(100); executor.setThreadNamePrefix("cpu-intensive-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); executor.initialize(); return executor; } // IO密集型任务线程池 @Bean("ioIntensiveTaskExecutor") public ThreadPoolTaskExecutor ioIntensiveTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); executor.setMaxPoolSize(50); executor.setQueueCapacity(500); executor.setThreadNamePrefix("io-intensive-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } // 定时任务线程池 @Bean("scheduledTaskExecutor") public ThreadPoolTaskExecutor scheduledTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(50); executor.setThreadNamePrefix("scheduled-task-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); executor.initialize(); return executor; }}5.3 使用指定线程池@Servicepublic class AdvancedAsyncService { // 使用CPU密集型线程池 @Async("cpuIntensiveTaskExecutor") public CompletableFuture<String> cpuIntensiveTask() { System.out.println("CPU密集型任务,线程: " + Thread.currentThread().getName()); // 模拟CPU密集型计算 return CompletableFuture.completedFuture("CPU任务完成"); } // 使用IO密集型线程池 @Async("ioIntensiveTaskExecutor") public CompletableFuture<String> ioIntensiveTask() { System.out.println("IO密集型任务,线程: " + Thread.currentThread().getName()); // 模拟IO操作 try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return CompletableFuture.completedFuture("IO任务完成"); }}6. 异步处理流程详解6.1 异步执行流程图graph TD A[客户端请求] --> B[Controller接收请求] B --> C[调用@Async方法] C --> D[立即返回响应] D --> E[客户端收到响应] C --> F[提交任务到线程池] F --> G{线程池状态检查} G -->|队列未满| H[任务进入队列] G -->|队列已满| I[创建新线程] I --> J{是否超过最大线程数} J -->|是| K[执行拒绝策略] J -->|否| L[创建线程执行任务] H --> M[线程从队列获取任务] M --> N[执行异步任务] N --> O[任务完成] K --> P[任务被拒绝处理]6.2 异步处理时序图ClientControllerAsync ServiceThread PoolWorker Thread发送请求调用异步方法提交任务到线程池立即返回CompletableFuture返回Future对象立即返回响应异步处理开始分配任务给工作线程执行耗时任务任务完成,设置结果客户端可以继续其他操作ClientControllerAsync ServiceThread PoolWorker Thread7. 完整示例代码7.1 控制器类@RestController@RequestMapping("/api/async")public class AsyncController { private final NotificationService notificationService; private final CalculationService calculationService; private final AdvancedAsyncService advancedAsyncService; public AsyncController(NotificationService notificationService, CalculationService calculationService, AdvancedAsyncService advancedAsyncService) { this.notificationService = notificationService; this.calculationService = calculationService; this.advancedAsyncService = advancedAsyncService; } @PostMapping("/send-email") public ResponseEntity<String> sendEmail() { String to = "user@example.com"; String subject = "测试邮件"; String content = "这是一封测试异步处理的邮件"; notificationService.sendEmail(to, subject, content); return ResponseEntity.accepted().body("邮件发送任务已提交"); } @GetMapping("/calculate") public CompletableFuture<ResponseEntity<Map<String, Object>>> calculateSum() { CompletableFuture<Integer> sum1 = calculationService.calculateSum(1, 100); CompletableFuture<Integer> sum2 = calculationService.calculateSum(101, 200); return CompletableFuture.allOf(sum1, sum2) .thenApply(v -> { Map<String, Object> result = new HashMap<>(); try { result.put("sum1", sum1.get()); result.put("sum2", sum2.get()); result.put("total", sum1.get() + sum2.get()); } catch (Exception e) { throw new RuntimeException(e); } return ResponseEntity.ok(result); }); } @GetMapping("/parallel-tasks") public CompletableFuture<ResponseEntity<Map<String, String>>> executeParallelTasks() { CompletableFuture<String> cpuTask = advancedAsyncService.cpuIntensiveTask(); CompletableFuture<String> ioTask = advancedAsyncService.ioIntensiveTask(); return CompletableFuture.allOf(cpuTask, ioTask) .thenApply(v -> { Map<String, String> results = new HashMap<>(); try { results.put("cpuTask", cpuTask.get()); results.put("ioTask", ioTask.get()); } catch (Exception e) { throw new RuntimeException(e); } return ResponseEntity.ok(results); }); }}7.2 服务类@Servicepublic class ComprehensiveAsyncService { private static final Logger logger = LoggerFactory.getLogger(ComprehensiveAsyncService.class); @Async("taskExecutor") public CompletableFuture<String> processWithRetry(String data, int maxRetries) { return CompletableFuture.supplyAsync(() -> { int attempt = 0; while (attempt < maxRetries) { try { logger.info("处理数据尝试: {}, 数据: {}", attempt + 1, data); // 模拟处理逻辑 if (Math.random() > 0.3) { // 70%成功率 return "处理成功: " + data; } else { throw new RuntimeException("处理失败"); } } catch (Exception e) { attempt++; if (attempt >= maxRetries) { logger.error("处理数据失败,已达到最大重试次数"); throw new RuntimeException("处理失败,重试次数耗尽"); } try { Thread.sleep(1000 * attempt); // 指数退避 } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException("任务被中断"); } } } return "处理完成"; }); } @Async public CompletableFuture<List<String>> batchProcess(List<String> items) { logger.info("开始批量处理,项目数量: {}", items.size()); List<CompletableFuture<String>> futures = items.stream() .map(item -> processItem(item)) .collect(Collectors.toList()); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> futures.stream() .map(future -> { try { return future.get(); } catch (Exception e) { return "处理失败: " + e.getMessage(); } }) .collect(Collectors.toList())); } @Async private CompletableFuture<String> processItem(String item) { return CompletableFuture.supplyAsync(() -> { try { // 模拟处理时间 Thread.sleep(100 + (long)(Math.random() * 400)); return "处理完成: " + item; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } }); }}8. 异常处理机制8.1 异步方法异常处理@Servicepublic class AsyncExceptionHandlingService { @Async public CompletableFuture<String> processWithExceptionHandling(String input) { return CompletableFuture.supplyAsync(() -> { if (input == null || input.trim().isEmpty()) { throw new IllegalArgumentException("输入不能为空"); } try { // 模拟业务处理 Thread.sleep(1000); return "处理结果: " + input.toUpperCase(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("处理被中断", e); } }).exceptionally(throwable -> { // 异常处理逻辑 logger.error("处理过程中发生异常", throwable); return "错误: " + throwable.getMessage(); }); }}8.2 全局异步异常处理器@Componentpublic class GlobalAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalAsyncExceptionHandler.class); @Override public void handleUncaughtException(Throwable ex, Method method, Object... params) { logger.error("异步方法执行异常 - 方法: {}, 参数: {}", method.getName(), Arrays.toString(params), ex); // 可以在这里添加额外的异常处理逻辑,如发送告警、记录监控等 sendAlert(method, ex); } private void sendAlert(Method method, Throwable ex) { // 模拟发送告警 logger.warn("发送异步任务异常告警 - 方法: {}, 异常: {}", method.getName(), ex.getMessage()); }}8.3 配置异常处理器@Configuration@EnableAsyncpublic class AsyncExceptionConfig implements AsyncConfigurer { private final GlobalAsyncExceptionHandler exceptionHandler; public AsyncExceptionConfig(GlobalAsyncExceptionHandler exceptionHandler) { this.exceptionHandler = exceptionHandler; } @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-exception-"); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return exceptionHandler; }}9. 异步与事务管理9.1 异步事务处理@Servicepublic class AsyncTransactionService { private final UserRepository userRepository; private final AuditLogRepository auditLogRepository; public AsyncTransactionService(UserRepository userRepository, AuditLogRepository auditLogRepository) { this.userRepository = userRepository; this.auditLogRepository = auditLogRepository; } @Async @Transactional(propagation = Propagation.REQUIRES_NEW) public CompletableFuture<Void> asyncOperationWithTransaction(Long userId, String action) { return CompletableFuture.runAsync(() -> { try { // 在独立事务中执行 User user = userRepository.findById(userId).orElseThrow(); AuditLog auditLog = new AuditLog(); auditLog.setUserId(userId); auditLog.setAction(action); auditLog.setTimestamp(LocalDateTime.now()); auditLogRepository.save(auditLog); // 模拟业务处理 Thread.sleep(1000); logger.info("异步事务操作完成 - 用户: {}, 操作: {}", user.getUsername(), action); } catch (Exception e) { logger.error("异步事务操作失败", e); throw new RuntimeException(e); } }); }}10. 监控和调试10.1 线程池监控@Componentpublic class ThreadPoolMonitor { private static final Logger logger = LoggerFactory.getLogger(ThreadPoolMonitor.class); private final ThreadPoolTaskExecutor taskExecutor; public ThreadPoolMonitor(@Qualifier("taskExecutor") ThreadPoolTaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } @Scheduled(fixedRate = 30000) // 每30秒执行一次 public void monitorThreadPool() { ThreadPoolExecutor threadPoolExecutor = taskExecutor.getThreadPoolExecutor(); logger.info("线程池监控信息:"); logger.info("活跃线程数: {}", threadPoolExecutor.getActiveCount()); logger.info("核心线程数: {}", threadPoolExecutor.getCorePoolSize()); logger.info("最大线程数: {}", threadPoolExecutor.getMaximumPoolSize()); logger.info("池中当前线程数: {}", threadPoolExecutor.getPoolSize()); logger.info("任务总数: {}", threadPoolExecutor.getTaskCount()); logger.info("已完成任务数: {}", threadPoolExecutor.getCompletedTaskCount()); logger.info("队列大小: {}", threadPoolExecutor.getQueue().size()); logger.info("队列剩余容量: {}", threadPoolExecutor.getQueue().remainingCapacity()); }}10.2 异步任务跟踪@Aspect@Componentpublic class AsyncExecutionAspect { private static final Logger logger = LoggerFactory.getLogger(AsyncExecutionAspect.class); @Around("@annotation(org.springframework.scheduling.annotation.Async)") public Object logAsyncExecution(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); String className = joinPoint.getTarget().getClass().getSimpleName(); long startTime = System.currentTimeMillis(); logger.info("开始执行异步方法: {}.{}", className, methodName); try { Object result = joinPoint.proceed(); long executionTime = System.currentTimeMillis() - startTime; logger.info("异步方法执行完成: {}.{}, 耗时: {}ms", className, methodName, executionTime); return result; } catch (Exception e) { long executionTime = System.currentTimeMillis() - startTime; logger.error("异步方法执行异常: {}.{}, 耗时: {}ms, 异常: {}", className, methodName, executionTime, e.getMessage()); throw e; } }}11. 性能优化建议11.1 线程池参数调优@Configuration@EnableAsyncpublic class OptimizedAsyncConfig { @Bean("optimizedTaskExecutor") public ThreadPoolTaskExecutor optimizedTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 根据服务器CPU核心数动态设置 int cpuCores = Runtime.getRuntime().availableProcessors(); // CPU密集型任务 executor.setCorePoolSize(cpuCores); executor.setMaxPoolSize(cpuCores * 2); // IO密集型任务(注释掉的配置) // executor.setCorePoolSize(cpuCores * 2); // executor.setMaxPoolSize(cpuCores * 4); executor.setQueueCapacity(1000); executor.setKeepAliveSeconds(300); executor.setThreadNamePrefix("optimized-async-"); // 自定义拒绝策略 executor.setRejectedExecutionHandler((r, executor1) -> { logger.warn("线程池队列已满,任务被拒绝,当前活跃线程: {}", executor1.getActiveCount()); // 可以在这里添加降级逻辑 if (!executor1.isShutdown()) { r.run(); } }); executor.initialize(); return executor; }}11.2 最佳实践总结合理配置线程池参数:根据任务类型(CPU密集型/IO密集型)调整参数使用合适的拒绝策略:根据业务需求选择适当的拒绝策略异常处理:确保所有异步方法都有完善的异常处理机制资源清理:应用关闭时确保线程池正确关闭监控告警:建立线程池监控和告警机制避免过度使用:不是所有场景都适合异步处理12. 总结Spring Boot的异步处理功能为构建高性能、高并发的应用提供了强大的支持。通过合理使用@Async注解、配置线程池、处理异常和监控性能,可以显著提升系统的吞吐量和响应速度。本文详细介绍了Spring Boot异步处理的各个方面,从基础使用到高级特性,提供了完整的代码示例和流程图。在实际项目中,应根据具体业务需求和系统特点,合理设计和配置异步处理方案,以达到最佳的性能效果。附录:完整项目结构src/main/java/├── com/example/async/│ ├── config/│ │ ├── AsyncConfig.java│ │ ├── MultipleAsyncConfig.java│ │ └── AsyncExceptionConfig.java│ ├── controller/│ │ └── AsyncController.java│ ├── service/│ │ ├── AsyncService.java│ │ ├── NotificationService.java│ │ ├── CalculationService.java│ │ ├── AdvancedAsyncService.java│ │ └── AsyncTransactionService.java│ ├── aspect/│ │ └── AsyncExecutionAspect.java│ ├── monitor/│ │ └── ThreadPoolMonitor.java│ └── exception/│ └── GlobalAsyncExceptionHandler.java└── resources/ └── application.yml希望本文能够帮助您全面掌握Spring Boot中的异步处理技术,在实际项目中灵活运用,构建高性能的应用程序。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/qq_16242613/article/details/152948442
-
1. 自动配置概述1.1 什么是自动配置?Spring Boot 自动配置是其最重要的特性之一,它尝试根据添加的 jar 依赖自动配置 Spring 应用程序。简单来说,就是约定优于配置理念的具体实现。 传统 Spring 配置: @Configurationpublic class ManualDataSourceConfig { @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/test"); dataSource.setUsername("root"); dataSource.setPassword("password"); return dataSource; } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }} Spring Boot 自动配置: // 只需在 application.properties 中配置spring.datasource.url=jdbc:mysql://localhost:3306/testspring.datasource.username=rootspring.datasource.password=password// Spring Boot 自动创建 DataSource 和 JdbcTemplate 1.2 自动配置的优势快速启动:减少样板代码配置智能默认值:提供合理的默认配置灵活覆盖:可轻松自定义配置条件化装配:根据条件智能启用配置2. 自动配置核心原理2.1 核心注解 @SpringBootApplication让我们从启动类开始分析: @SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }} @SpringBootApplication 源码分析: @Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration // 关键注解@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication { // ...} 2.2 @EnableAutoConfiguration 注解这是自动配置的入口点: @Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class) // 核心导入public @interface EnableAutoConfiguration { // ...}AI写代码 2.3 AutoConfigurationImportSelector 类这是自动配置的核心处理器: public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } // 获取自动配置入口 AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { // 获取所有配置类 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); configurations = removeDuplicates(configurations); Set<String> exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = getConfigurationClassFilter().filter(configurations); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); } protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 从 META-INF/spring.factories 加载配置 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); return configurations; } protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; }} 3. 自动配置加载机制3.1 spring.factories 文件Spring Boot 在 spring-boot-autoconfigure jar 包的 META-INF/spring.factories 文件中定义了大量的自动配置类: # Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration,\org.springframework.boot.autoconfigure.netty.NettyAutoConfiguration,\org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration,\org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration 3.2 自动配置加载流程graph TD A[Spring Boot 启动] --> B[@SpringBootApplication] B --> C[@EnableAutoConfiguration] C --> D[AutoConfigurationImportSelector] D --> E[加载 spring.factories] E --> F[获取自动配置类列表] F --> G[条件注解过滤] G --> H[排除用户配置的类] H --> I[注册有效的配置类] I --> J[创建 Bean 定义] J --> K[完成自动配置] subgraph 条件过滤 G1[@ConditionalOnClass] G2[@ConditionalOnBean] G3[@ConditionalOnProperty] G4[@ConditionalOnMissingBean] end 4. 条件化配置详解4.1 常用条件注解Spring Boot 提供了一系列条件注解来控制配置类的加载: 注解 说明@ConditionalOnClass 类路径下存在指定类时生效@ConditionalOnMissingClass 类路径下不存在指定类时生效@ConditionalOnBean 容器中存在指定 Bean 时生效@ConditionalOnMissingBean 容器中不存在指定 Bean 时生效@ConditionalOnProperty 配置属性满足条件时生效@ConditionalOnResource 资源文件存在时生效@ConditionalOnWebApplication 是 Web 应用时生效@ConditionalOnNotWebApplication 不是 Web 应用时生效@ConditionalOnExpression SpEL 表达式为 true 时生效4.2 DataSource 自动配置示例让我们分析 DataSourceAutoConfiguration 的实现: @Configuration(proxyBeanMethods = false)@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")@EnableConfigurationProperties(DataSourceProperties.class)@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class })public class DataSourceAutoConfiguration { @Configuration(proxyBeanMethods = false) @Conditional(EmbeddedDatabaseCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import(EmbeddedDataSourceConfiguration.class) static class EmbeddedDatabaseConfiguration { } @Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) protected static class PooledDataSourceConfiguration { } // 内部条件类 static class PooledDataSourceCondition extends SpringBootCondition { // 条件判断逻辑 }} 4.3 自定义条件注解我们也可以创建自定义条件注解: // 自定义条件注解@Target({ ElementType.TYPE, ElementType.METHOD })@Retention(RetentionPolicy.RUNTIME)@Documented@Conditional(OnProductionCondition.class)public @interface ConditionalOnProduction {} // 条件判断逻辑public class OnProductionCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment env = context.getEnvironment(); String profile = env.getProperty("spring.profiles.active"); return "prod".equals(profile); }} // 使用自定义条件注解@Configuration@ConditionalOnProductionpublic class ProductionConfiguration { // 生产环境特有的配置} 5. 自动配置实现实战5.1 创建自定义 Starter让我们创建一个简单的邮件服务自动配置: 项目结构: email-spring-boot-starter/├── src/│ └── main/│ ├── java/│ │ └── com/example/email/│ │ ├── EmailService.java│ │ ├── EmailProperties.java│ │ └── autoconfigure/│ │ └── EmailAutoConfiguration.java│ └── resources/│ └── META-INF/│ └── spring.factories EmailService.java: public class EmailService { private final String host; private final int port; private final String username; private final String password; public EmailService(String host, int port, String username, String password) { this.host = host; this.port = port; this.username = username; this.password = password; } public void sendEmail(String to, String subject, String content) { // 模拟发送邮件 System.out.printf("发送邮件到: %s, 主题: %s, 内容: %s%n", to, subject, content); System.out.printf("使用服务器: %s:%d, 用户: %s%n", host, port, username); }} EmailProperties.java: @ConfigurationProperties(prefix = "email")public class EmailProperties { private String host = "smtp.example.com"; private int port = 25; private String username; private String password; private boolean enabled = true; // getters and setters public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; }} EmailAutoConfiguration.java: @Configuration@ConditionalOnClass(EmailService.class)@EnableConfigurationProperties(EmailProperties.class)public class EmailAutoConfiguration { private final EmailProperties properties; public EmailAutoConfiguration(EmailProperties properties) { this.properties = properties; } @Bean @ConditionalOnMissingBean @ConditionalOnProperty(prefix = "email", name = "enabled", havingValue = "true", matchIfMissing = true) public EmailService emailService() { return new EmailService( properties.getHost(), properties.getPort(), properties.getUsername(), properties.getPassword() ); } @Bean @ConditionalOnMissingBean public EmailController emailController(EmailService emailService) { return new EmailController(emailService); }} EmailController.java: public class EmailController { private final EmailService emailService; public EmailController(EmailService emailService) { this.emailService = emailService; } public void sendWelcomeEmail(String email) { emailService.sendEmail(email, "欢迎", "欢迎使用我们的服务!"); }} spring.factories: # Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.example.email.autoconfigure.EmailAutoConfiguration 5.2 使用自定义 Starter在应用项目中引入 starter 依赖: application.yml: email: host: smtp.163.com port: 465 username: your-email@163.com password: your-password enabled: true 使用示例: @RestControllerpublic class UserController { private final EmailController emailController; public UserController(EmailController emailController) { this.emailController = emailController; } @PostMapping("/register") public String register(@RequestParam String email) { // 用户注册逻辑 emailController.sendWelcomeEmail(email); return "注册成功"; }} 6. 自动配置高级特性6.1 配置顺序控制使用 @AutoConfigureAfter 和 @AutoConfigureBefore 控制配置顺序: @Configuration@AutoConfigureAfter(DataSourceAutoConfiguration.class)@AutoConfigureBefore(TransactionAutoConfiguration.class)public class MyCustomAutoConfiguration { // 这个配置会在 DataSource 配置之后,事务配置之前执行} 6.2 条件配置的复杂逻辑@Configuration@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })@ConditionalOnSingleCandidate(DataSource.class)@ConditionalOnProperty(prefix = "app.feature", name = "enabled", havingValue = "true")public class ComplexConditionConfiguration { @Bean @ConditionalOnMissingBean public MyRepository myRepository(JdbcTemplate jdbcTemplate) { return new MyRepository(jdbcTemplate); } static class MyRepository { private final JdbcTemplate jdbcTemplate; public MyRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } // 数据访问方法 }} 6.3 使用 @ConditionalOnExpression@Configuration@ConditionalOnExpression( "${app.feature.enabled:false} && " + "T(org.springframework.util.StringUtils).hasText('${app.feature.api-key:}')")public class ExpressionConditionConfiguration { // 复杂的条件判断} 7. 自动配置调试与优化7.1 调试自动配置启用调试日志: # application.properties 查看自动配置报告:启动应用后,控制台会输出自动配置报告: =========================AUTO-CONFIGURATION REPORT========================= Positive matches:----------------- AopAutoConfiguration matched: - @ConditionalOnClass found required classes 'org.springframework.context.annotation.EnableAspectJAutoProxy', ... (OnClassCondition) - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition) DataSourceAutoConfiguration matched: - @ConditionalOnClass found required classes 'javax.sql.DataSource', ... (OnClassCondition) Negative matches:----------------- ActiveMQAutoConfiguration: Did not match: - @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition) Exclusions:----------- None 7.2 排除自动配置方式1:使用注解排除 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, MailSenderAutoConfiguration.class})public class Application { // ...} 方式2:使用配置属性排除 spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfigurationAI写代码properties17.3 性能优化建议合理使用条件注解:避免不必要的条件检查延迟初始化:使用 @Lazy 注解配置扫描路径:精确指定 @ComponentScan 路径排除不必要的自动配置———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/qq_16242613/article/details/152824353
-
什么是JWTJson web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。JWT请求流程1. 用户使用账号和面发出post请求;2. 服务器使用私钥创建一个jwt;3. 服务器返回这个jwt给浏览器;4. 浏览器将该jwt串在请求头中像服务器发送请求;5. 服务器验证该jwt;6. 返回响应的资源给浏览器。JWT的主要应用场景身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。优点1.简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。4.不需要在服务端保存会话信息,特别适用于分布式微服务。`JWT的结构JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。就像这样:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQJWT包含了三部分:Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)Payload 负载 (类似于飞机上承载的物品)Signature 签名/签证HeaderJWT的头部承载两部分信息:token类型和采用的加密算法。{ "alg": "HS256", "typ": "JWT"} 声明类型:这里是jwt声明加密的算法:通常直接使用 HMAC SHA256加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证Payload载荷就是存放有效信息的地方。有效信息包含三个部分1.标准中注册的声明2.公共的声明3.私有的声明标准中注册的声明 (建议但不强制使用) :iss: jwt签发者sub: 面向的用户(jwt所面向的用户)aud: 接收jwt的一方exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)nbf: 定义在什么时间之前,该jwt都是不可用的.iat: jwt的签发时间jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。公共的声明 :公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.私有的声明 :私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。Signaturejwt的第三部分是一个签证信息,这个签证信息由三部分组成:header (base64后的)payload (base64后的)secret这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。下面来进行SpringBoot和JWT的集成引入JWT依赖,由于是基于Java,所以需要的是java-jwt<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version></dependency>需要自定义两个注解用来跳过验证的PassToken@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface PassToken { boolean required() default true;}需要登录才能进行操作的注解UserLoginToken@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface UserLoginToken { boolean required() default true;}@Target:注解的作用目标@Target(ElementType.TYPE)——接口、类、枚举、注解@Target(ElementType.FIELD)——字段、枚举的常量@Target(ElementType.METHOD)——方法@Target(ElementType.PARAMETER)——方法参数@Target(ElementType.CONSTRUCTOR) ——构造函数@Target(ElementType.LOCAL_VARIABLE)——局部变量@Target(ElementType.ANNOTATION_TYPE)——注解@Target(ElementType.PACKAGE)——包@Retention:注解的保留位置RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。@Document:说明该注解将被包含在javadoc中@Inherited:说明子类可以继承父类中的该注解简单自定义一个实体类User,使用lombok简化实体类的编写@Data@AllArgsConstructor@NoArgsConstructorpublic class User { String Id; String username; String password;}需要写token的生成方法public String getToken(User user) { String token=""; token= JWT.create().withAudience(user.getId()) .sign(Algorithm.HMAC256(user.getPassword())); return token; }Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。withAudience()存入需要保存在token的信息,这里我把用户ID存入token中接下来需要写一个拦截器去获取token并验证tokenpublic class AuthenticationInterceptor implements HandlerInterceptor { @Autowired UserService userService; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token // 如果不是映射到方法直接通过 if(!(object instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod=(HandlerMethod)object; Method method=handlerMethod.getMethod(); //检查是否有passtoken注释,有则跳过认证 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //检查有没有需要用户权限的注解 if (method.isAnnotationPresent(UserLoginToken.class)) { UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class); if (userLoginToken.required()) { // 执行认证 if (token == null) { throw new RuntimeException("无token,请重新登录"); } // 获取 token 中的 user id String userId; try { userId = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { throw new RuntimeException("401"); } User user = userService.findUserById(userId); if (user == null) { throw new RuntimeException("用户不存在,请重新登录"); } // 验证 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); try { jwtVerifier.verify(token); } catch (JWTVerificationException e) { throw new RuntimeException("401"); } return true; } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { }实现一个拦截器就需要实现HandlerInterceptor接口HandlerInterceptor接口主要定义了三个方法1.boolean preHandle ():预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。2.void postHandle():后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。3.void afterCompletion():整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中主要流程:1.从 http 请求头中取出 token,2.判断是否映射到方法3.检查是否有passtoken注释,有则跳过认证4.检查有没有需要用户登录的注解,有则需要取出并验证5.认证通过则可以访问,不通过会报相关错误信息配置拦截器在配置类上添加了注解@Configuration,标明了该类是一个配置类并且会将该类作为一个SpringBean添加到IOC容器内@Configurationpublic class InterceptorConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录 } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); }}WebMvcConfigurerAdapter该抽象类其实里面没有任何的方法实现,只是空实现了接口WebMvcConfigurer内的全部方法,并没有给出任何的业务逻辑处理,这一点设计恰到好处的让我们不必去实现那些我们不用的方法,都交由WebMvcConfigurerAdapter抽象类空实现,如果我们需要针对具体的某一个方法做出逻辑处理,仅仅需要在WebMvcConfigurerAdapter子类中@Override对应方法就可以了。注:在SpringBoot2.0及Spring 5.0中WebMvcConfigurerAdapter已被废弃网上有说改为继承WebMvcConfigurationSupport(),不过试了下,还是过期的解决方法:直接实现WebMvcConfigurer (官方推荐)@Configurationpublic class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); }}InterceptorRegistry内的addInterceptor需要一个实现HandlerInterceptor接口的拦截器实例,addPathPatterns方法用于设置拦截器的过滤路径规则。这里我拦截所有请求,通过判断是否有@LoginRequired注解 决定是否需要登录在数据访问接口中加入登录操作注解@RestController@RequestMapping("api")public class UserApi { @Autowired UserService userService; @Autowired TokenService tokenService; //登录 @PostMapping("/login") public Object login(@RequestBody User user){ JSONObject jsonObject=new JSONObject(); User userForBase=userService.findByUsername(user); if(userForBase==null){ jsonObject.put("message","登录失败,用户不存在"); return jsonObject; }else { if (!userForBase.getPassword().equals(user.getPassword())){ jsonObject.put("message","登录失败,密码错误"); return jsonObject; }else { String token = tokenService.getToken(userForBase); jsonObject.put("token", token); jsonObject.put("user", userForBase); return jsonObject; } } } @UserLoginToken @GetMapping("/getMessage") public String getMessage(){ return "你已通过验证"; }}不加注解的话默认不验证,登录接口一般是不验证的。在getMessage()中我加上了登录注解,说明该接口必须登录获取token后,在请求头中加上token并通过验证才可以访问下面进行测试,启动项目,使用postman测试接口在没token的情况下访问api/getMessage接口我这里使用了统一异常处理,所以只看到错误message 下面进行登录,从而获取token登录操作我没加验证注解,所以可以直接访问 把token加在请求头中,再次访问api/getMessage接口注意:这里的key一定不能错,因为在拦截器中是取关键字token的值String token = httpServletRequest.getHeader("token");加上token之后就可以顺利通过验证和进行接口访问了
-
MQTT配置 1. 前言公司的IOT平台主要采用MQTT(消息队列遥测传输)对底层的驱动做命令下发和数据采集。也用到了redis、zeroMQ、nats等消息中间件。今天先整理SpringBoot集成MQTT笔记和工作中遇到的问题。2. MQTT介绍MQTT is a machine-to-machine (M2M)/"Internet of Things" connectivity protocol. It was designed as an extremely lightweight publish/subscribe messaging transport. It is useful for connections with remote locations where a small code footprint is required and/or network bandwidth is at a premium.官网地址:http://mqtt.org/ 、 https://www.mqtt.com/MQTT除了具备大部分消息中间件拥有的功能外,其最大的特点就是小型传输。以减少开销,减低网络流量的方式去满足低带宽、不稳定的网络远程传输。MQTT服务器有很多,比如Apache-Apollo和EMQX,ITDragon龙 目前使用的时EMQX作为MQTT的服务器。使用也很简单,下载解压后,进入bin目录执行emqx console 启动服务。MQTT调试工具可以用MQTTBox3. SpringBoot 集成MQTT3.1 导入mqtt库第一步:导入面向企业应用集成库和对应mqtt集成库compile('org.springframework.boot:spring-boot-starter-integration')compile('org.springframework.integration:spring-integration-mqtt')这里要注意spring-integration-mqtt的版本。因为会存在org.eclipse.paho.client.mqttv3修复了一些bug,并迭代了新版本。但spring-integration-mqtt并没有及时更新的情况。修改方法如下compile("org.springframework.integration:spring-integration-mqtt") { exclude group: "org.eclipse.paho" , module: "org.eclipse.paho.client.mqttv3"}compile("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.2")第二步:MQTT连接配置文件# MQTT Configmqtt.server=tcp://x.x.x.x:1883mqtt.username=xxxmqtt.password=xxxmqtt.client-id=clientIDmqtt.cache-number=100mqtt.message.topic=itDragon/tags/cov3.2 配置MQTT订阅者Inbound 入站消息适配器第一步:配置MQTT客户端工厂类DefaultMqttPahoClientFactory第二步:配置MQTT入站消息适配器MqttPahoMessageDrivenChannelAdapter第三步:定义MQTT入站消息通道MessageChannel第四步:声明MQTT入站消息处理器MessageHandler以下有些配置是冲突或者重复的,主要是体现一些重要配置。package com.itdragon.server.configimport com.itdragon.server.message.ITDragonMQTTMessageHandlerimport org.eclipse.paho.client.mqttv3.MqttConnectOptionsimport org.springframework.beans.factory.annotation.Valueimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.integration.annotation.ServiceActivatorimport org.springframework.integration.channel.DirectChannelimport org.springframework.integration.core.MessageProducerimport org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactoryimport org.springframework.integration.mqtt.core.MqttPahoClientFactoryimport org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapterimport org.springframework.integration.mqtt.support.DefaultPahoMessageConverterimport org.springframework.messaging.MessageChannelimport org.springframework.messaging.MessageHandlerimport java.time.Instant@Configurationclass MQTTConfig { @Value("\${mqtt.server}") lateinit var mqttServer: String @Value("\${mqtt.user-name}") lateinit var mqttUserName: String @Value("\${mqtt.password}") lateinit var mqttUserPassword: String @Value("\${mqtt.client-id}") lateinit var clientID: String @Value("\${mqtt.cache-number}") lateinit var maxMessageInFlight: String @Value("\${mqtt.message.topic}") lateinit var messageTopic: String /** * 配置DefaultMqttPahoClientFactory * 1. 配置基本的链接信息 * 2. 配置maxInflight,在mqtt消息量比较大的情况下将值设大 */ fun mqttClientFactory(): MqttPahoClientFactory { val mqttConnectOptions = MqttConnectOptions() // 配置mqtt服务端地址,登录账号和密码 mqttConnectOptions.serverURIs = arrayOf(mqttServer) mqttConnectOptions.userName = mqttUserName mqttConnectOptions.password = mqttUserPassword.toCharArray() // 配置最大不确定接收消息数量,默认值10,qos!=0 时生效 mqttConnectOptions.maxInflight = maxMessageInFlight.toInt() val factory = DefaultMqttPahoClientFactory() factory.connectionOptions = mqttConnectOptions return factory } /** * 配置Inbound入站,消费者基本连接配置 * 1. 通过DefaultMqttPahoClientFactory 初始化入站通道适配器 * 2. 配置超时时长,默认30000毫秒 * 3. 配置Paho消息转换器 * 4. 配置发送数据的服务质量 0~2 * 5. 配置订阅通道 */ @Bean fun itDragonMqttInbound(): MessageProducer { // 初始化入站通道适配器,使用的是Eclipse Paho MQTT客户端库 val adapter = MqttPahoMessageDrivenChannelAdapter(clientID + Instant.now().toEpochMilli(), mqttClientFactory(), messageTopic) // 设置连接超时时长(默认30000毫秒) adapter.setCompletionTimeout(30000) // 配置默认Paho消息转换器(qos=0, retain=false, charset=UTF-8) adapter.setConverter(DefaultPahoMessageConverter()) // 设置服务质量 // 0 最多一次,数据可能丢失; // 1 至少一次,数据可能重复; // 2 只有一次,有且只有一次;最耗性能 adapter.setQos(0) // 设置订阅通道 adapter.outputChannel = itDragonMqttInputChannel() return adapter } /** * 配置Inbound入站,消费者订阅的消息通道 */ @Bean fun itDragonMqttInputChannel(): MessageChannel { return DirectChannel() } /** * 配置Inbound入站,消费者的消息处理器 * 1. 使用@ServiceActivator注解,表明所修饰的方法用于消息处理 * 2. 使用inputChannel值,表明从指定通道中取值 * 3. 利用函数式编程的思路,解耦MessageHandler的业务逻辑 */ @Bean @ServiceActivator(inputChannel = "itDragonMqttInputChannel") fun commandDataHandler(): MessageHandler { /*return MessageHandler { message -> println(message.payload) }*/ return ITDragonMQTTMessageHandler() }}注意:1)MQTT的客户端ID要唯一。2)MQTT在消息量大的情况下会出现消息丢失的情况。3)MessageHandler注意解耦问题。3.3 配置MQTT发布者Outbound 出站消息适配器第一步:配置Outbound出站,出站通道适配器第二步:配置Outbound出站,发布者发送的消息通道第三步:对外提供推送消息的接口在原有的MQTTConfig配置类的集成上补充以下内容 /** * 配置Outbound出站,出站通道适配器 * 1. 通过MqttPahoMessageHandler 初始化出站通道适配器 * 2. 配置异步发送 * 3. 配置默认的服务质量 */ @Bean @ServiceActivator(inputChannel = "itDragonMqttOutputChannel") fun itDragonMqttOutbound(): MqttPahoMessageHandler { // 初始化出站通道适配器,使用的是Eclipse Paho MQTT客户端库 val messageHandler = MqttPahoMessageHandler(clientID + Instant.now().toEpochMilli() + "_set", mqttClientFactory()) // 设置异步发送,默认是false(发送时阻塞) messageHandler.setAsync(true) // 设置默认的服务质量 messageHandler.setDefaultQos(0) return messageHandler } /** * 配置Outbound出站,发布者发送的消息通道 */ @Bean fun itDragonMqttOutputChannel(): MessageChannel { return DirectChannel() } /** * 对外提供推送消息的接口 * 1. 使用@MessagingGateway注解,配置MQTTMessageGateway消息推送接口 * 2. 使用defaultRequestChannel值,调用时将向其发送消息的默认通道 * 3. 配置灵活的topic主题 */ @MessagingGateway(defaultRequestChannel = "itDragonMqttOutputChannel") interface MQTTMessageGateway { fun sendToMqtt(data: String, @Header(MqttHeaders.TOPIC) topic: String) fun sendToMqtt(data: String, @Header(MqttHeaders.QOS) qos: Int, @Header(MqttHeaders.TOPIC) topic: String) } 注意:1)发布者和订阅者的客户端ID不能相同。2)消息的推送建议采用异步的方式。3)消息的推送方法可以只传payload消息体,但需要配置setDefaultTopic。3.4 MQTT消息处理和发送3.4.1 消息处理为了让消息处理函数和MQTT配置解耦,这里提供MessageHandler 注册类,将消息处理的业务逻辑以函数式编程的思维注册到Handler中。package com.itdragon.server.messageimport org.springframework.messaging.Messageimport org.springframework.messaging.MessageHandlerclass ITDragonMQTTMessageHandler : MessageHandler { private var handler: ((String) -> Unit)? = null fun registerHandler(handler: (String) -> Unit) { this.handler = handler } override fun handleMessage(message: Message<*>) { handler?.run { this.invoke(message.payload.toString()) } }}注册MessageHandlerpackage com.itdragon.server.messageimport org.slf4j.LoggerFactoryimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.stereotype.Serviceimport javax.annotation.PostConstruct@Serviceclass ITDragonMessageDispatcher { private val logger = LoggerFactory.getLogger(ITDragonMessageDispatcher::class.java) @Autowired lateinit var itDragonMQTTMessageHandler: ITDragonMQTTMessageHandler @PostConstruct fun init() { itDragonMQTTMessageHandler.registerHandler { itDragonMsgHandler(it) } } fun itDragonMsgHandler(message: String) { logger.info("itdragon mqtt receive message: $message") try { // todo }catch (ex: Exception) { ex.printStackTrace() } }}3.4.1 消息发送注入MQTT的MessageGateway,然后推送消息。@Autowiredlateinit var mqttGateway: MQTTConfig.MQTTMessageGateway@Scheduled(fixedDelay = 10*1000)fun sendMessage() { mqttGateway.sendToMqtt("Hello ITDragon ${Instant.now()}", "itDragon/tags/cov/set")}4. 开发常见问题4.1 MQTT每次重连失败都会增长线程数项目上线一段时间后,客户的服务器严重卡顿。原因是客户服务断网后,MQTT在每次尝试重连的过程中一直在创建新的线程,导致一个Java服务创建了上万个线程。解决方案是更新了org.eclipse.paho.client.mqttv3的版本,也是 "3.1 导入mqtt库" 中提到的。后续就没有出现这个问题了。4.2 MQTT消息量大存在消息丢失的情况MQTT的消息量大的情况下,既要保障数据的完整,又要保障性能的稳定。光从MQTT本身上来说,很难做到鱼和熊掌不可兼得。 先要理清需求:1)数据的完整性,主要用于能耗的统计、报警的分析2)性能的稳定性,服务器不挂🤣🤣🤣🤣在消息量大的情况下, 可以将服务质量设置成0(最多一次)以减少消息确认的开销,用来保证系统的稳定性。将消息的服务质量设置成0后,会让消息的丢失可能性变得更大,如何保证数据的完整性?其实可以在往MQTT通道推送消息之前,先将底层驱动采集的数据先异步保存到Inflxudb数据库中。还有就是每次发送消息量不能太大,太大也会导致消息丢失。最直接的就是后端报错,比如:java.io.EOFException 和 too large message: xxx bytes 。但是有的场景后端没有报错,前端订阅的mqtt也没收到消息。最麻烦的是mqttbox工具因为数据量太大直接卡死。一时间真不知道把锅甩给谁。其实我们 可以将消息拆包一批批发送。可以缓解这个问题🤣🤣🤣🤣。其实采集的数据消息,若在这一批推送过程中丢失。也会在下一批推送过程中补上。命令下发也是一样,如果下发失败,再重写下发一次。毕竟消息的丢失并不是必现的情况。也是小概率事件,系统的稳定性才是最重要的。转载自https://www.cnblogs.com/itdragon/p/12463050.html
-
前言在当今数字化时代,随着互联网应用的飞速发展,数据传输的效率和性能成为了至关重要的问题。GeoJSON 是一种基于 JSON 格式的地理空间数据交换格式,它广泛应用于地理信息系统(GIS)领域,用于描述地理空间数据的几何形状、属性等信息。GeoJSON 数据通常包含大量的地理坐标点、几何形状等信息,数据量往往较大。在 Web 地理信息系统应用中,如地图展示、地理数据可视化等场景,GeoJSON 数据的传输效率直接关系到地图加载的速度和用户体验。因此,对 GeoJSON 数据进行有效的压缩,以减少数据传输的体积,显得尤为重要。对于基于SpringBoot 框架构建的 WebGIS 应用来说,如何高效地传输数据、减少网络带宽的占用,是提升用户体验和系统性能的关键所在。而 Gzip 压缩技术,作为一种被广泛采用的解决方案,无疑为这一问题的解决提供了强大的助力。图片以 GeoJSON 数据为例,通过在 SpringBoot 应用中开启 Gzip 压缩,对 GeoJSON 数据进行瘦身,不仅可以显著减少数据传输的体积,提高地图加载的速度,还可以提升用户的交互体验。在实际的 Web 地理信息系统开发中,这种优化手段是非常实用和有效的。通过对 GeoJSON 数据的压缩处理,我们可以更好地满足用户对于地图快速加载和流畅交互的需求,同时也为整个应用的性能优化提供了有力的支持。在接下来的内容中,我们将详细介绍在 SpringBoot 中开启 Gzip 压缩的两种方式的具体实现步骤,并通过实际的 GeoJSON 数据压缩案例,展示这两种方式的应用效果和优缺点。希望通过本文的介绍,能够帮助读者更好地理解和掌握在 SpringBoot 应用中使用 Gzip 压缩技术的方法,从而提升自己开发的 WebGIS 应用的性能和用户体验。一、GZIP压缩知识简介GZIP 是一种数据压缩格式,只能用于压缩单个文件。它可用于网络文件传输时的压缩,例如 nginx 中的 ngx_http_gzip_module,启用压缩功能后可以节约带宽;也可用于本地文件存储时的压缩。本节将重点对Gzip进行一个简单的介绍,让大家对Gzip的相关知识有一个简单的了解。1、什么是GzipGzip 的压缩算法基于 LZ77 算法 和 Huffman 编码 的结合。具体过程如下:• LZ77 算法: LZ77 算法通过查找和替换重复的字节序列来压缩数据。它维护一个滑动窗口,在窗口内查找匹配的字符串,然后使用指针来替代这些重复的字符串。例如,对于字符串 "http://jiurl.yeah.nethttp://jiurl.nease.net",LZ77 算法会将其压缩为 "http://jiurl.yeah.net(22,13)nease(23,4)",其中 (22,13) 表示距离当前位置 22 个字符处的 13 个字符与当前位置的字符相同。• Huffman 编码: Huffman 编码是一种基于字符频率的编码方法。它为出现频率高的字符分配较短的编码,为出现频率低的字符分配较长的编码,从而达到压缩的目的。在 Gzip 中,LZ77 算法的输出结果会进一步通过 Huffman 编码进行压缩。• Gzip 文件结构: Gzip 文件包含文件头、压缩数据块和文件尾。文件头存储文件的元数据,如压缩方法、时间戳等;压缩数据块是使用 DEFLATE 算法压缩后的数据;文件尾存储校验和(CRC32)和原始文件大小,以确保文件的完整性2、Gzip特点• 无损压缩: Gzip 是一种无损压缩算法,数据在解压缩后可以完全还原,没有任何损失。• 高效的压缩率: 对于文本文件(如 HTML、JSON、XML),Gzip 的压缩率通常在 50%-90% 之间。它通过查找重复的字符串并用较短的指针替代,以及根据字符频率进行编码,从而实现高效压缩。• 广泛支持: Gzip 被几乎所有现代浏览器、服务器和编程语言支持。在 HTTP 传输中,服务器可以根据浏览器的请求头(如 Accept-Encoding: gzip)来判断是否使用 Gzip 压缩响应内容。• 压缩和解压速度较快: Gzip 的压缩和解压速度相对较快,尤其是解压过程,因为解压时只需根据指针和编码还原数据,计算量相对较小。• 适用于特定文件类型: Gzip 对文本文件的压缩效果较好,但对于已经压缩过的文件(如图片、音乐、视频)效果不明显,甚至可能导致文件变大3、Gzip在GIS方面的应用• 地理数据传输: 在 GIS 应用中,地理数据(如 GeoJSON 文件)通常包含大量的坐标点和几何形状信息,数据量较大。使用 Gzip 压缩可以显著减少数据传输的体积,加快地图加载速度,提升用户体验。• 服务器端优化: 在服务器端,可以配置 Web 服务器(如 Nginx)开启 Gzip 压缩功能。当客户端请求地理数据时,服务器会自动对响应内容进行压缩,减少网络带宽的占用。• 前端性能优化: 在前端开发中,可以通过工具(如 Webpack 的 Compression-webpack-plugin 插件)在构建过程中对地理数据文件进行 Gzip 压缩,然后在服务器上直接提供压缩后的文件,减少服务器的实时压缩负载。• 数据存储优化: 对于存储在服务器上的地理数据文件,使用 Gzip 压缩可以节省存储空间。在需要读取数据时,再进行解压处理。总之,Gzip 压缩技术在 GIS 领域的应用,不仅可以提高数据传输效率,还可以优化服务器性能和存储空间,是提升 GIS 应用性能的重要手段之一。二、SpringBoot中开启Gzip的方式SpringBoot 是一个非常流行的 Java 基于 Spring 框架的快速开发框架,它极大地简化了 Spring 应用的开发过程。在 SpringBoot 应用中,通过合理的配置和编程,可以很方便地集成 Gzip 压缩功能,从而实现对响应数据的自动压缩。这不仅可以提高数据传输的效率,还可以减轻服务器的负载,提升整个应用的性能。本节将重点介绍在SpringBoot中关于Gzip的相关知识以及在SpringBoot中GeoJSON的一些实践案例。1、在SpringBoot中开启Gzip的知识简介在 SpringBoot 中,开启 Gzip 压缩主要有两种方式:一种是通过配置文件进行全局配置,另一种是通过编程的方式在特定的控制器或方法上进行局部配置。这两种方式各有优缺点,适用于不同的应用场景。• 全局配置方式 : 通过在 SpringBoot 的配置文件(如 application.yml 或 application.properties)中添加相关的 Gzip 压缩配置,可以实现对整个应用的 HTTP 响应进行统一的压缩处理。这种方式简单方便,适用于大多数需要压缩的场景,但缺乏对特定数据类型的针对性处理。• 局部配置方式 : 通过在控制器或方法上添加自定义的注解或逻辑,可以实现对特定数据类型的压缩处理。这种方式更加灵活,可以根据不同的数据类型和业务需求,定制不同的压缩策略,但相对来说实现起来较为复杂,需要更多的编程工作。这里首先简单介绍了两种在SpringBoot中开启Gzip压缩的方式,为下文全面讲解这两种方式做准备,先让大家了解相关知识。2、SpringBoot中GeoJSON的实例GeoJSON在WebGIS中的用处很多,很多矢量数据的边界,范围点等数据,我们都是直接以GeoJSON的格式返回给前端,并直接进行展示的。比如之前很多的行政区划展示,省市县等不同的行政区划范围展示等,我们的实现过程都是在后台的Controller层中返回一个包含GeoJSON的对象,方法如下:package com.yelang.project.meteorology.domain;import java.io.Serializable;import com.baomidou.mybatisplus.annotation.TableField;import lombok.Data;import lombok.EqualsAndHashCode;import lombok.ToString;@Data@ToString(callSuper=true)//callSuper=true表示输出父类属性@EqualsAndHashCode(callSuper=false)public class AreaWeatherVO extends WeatherNow implements Serializable{ private static final long serialVersionUID = -7559774548761847068L; @TableField(exist = false,value= "province_code") private String provinceCode; @TableField(exist = false,value= "province_name") private String provinceName; @TableField(exist = false,value= "city_code") private String cityCode; @TableField(exist = false,value= "city_name") private String cityName; @TableField(exist = false,value= "area_name") private String areaName; @TableField(exist = false) private String geomJson; private String lat; private String lon;}上面是一个视图对象的具体代码,在Controller的方法中我们调用如下:@RequiresPermissions("met:province:weather:list")@GetMapping("/list/{pcode}")@ResponseBodypublic AjaxResult ewsnProvinceList(@PathVariable("pcode") String pcode){ String day = "2025-08-17"; List<AreaWeatherVO> dataList = weatherNowService.getWeatherByProvinceAndday(pcode,day); return AjaxResult.success().put("data", dataList); }经过以上的代码输出接口中就包含GeoJOSN数据,如下图所示:图片其具体的geoJSON值如下图:图片在网络窗口中可以看到整个接口返回的数据大小大约为5MB,如果遇到更大范围的行政区划,返回的数据肯定会大,比如西藏的行政区划大约有14MB,如图所示:图片那么如何通过开启Gzip来减少这些数据的输出呢?下面两个部分来重点讲解。三、全局开启Gzip实现本节将详细介绍如何在SpringBoot中开启Gzip压缩的配置。1、实现原理在 application.yml 或 application.properties 中添加以下配置:示例:application.ymlserver: compression: enabled: true mime-types: application/json min-response-size: 1KB # 小于1KB的响应不压缩或者application.properties中:server.compression.enabled=trueserver.compression.mime-types=application/jsonserver.compression.min-response-size=1024请注意:该配置会对所有返回 application/json 的接口启用 GZIP 压缩。2、实现效果在我们的工程中配置文件是以yml的形式配置的,按照上面的步骤进行设置后,重新启动应用程序后来看一下同样的接口,其返回的数据量大小是多少:图片通过以上图片可以直观的看到,开启全局压缩后,我们的接口返回大小,从14.4M下降了5MB,几乎是原来的1/3,这个压缩比例还是可以的。四、局部约定配置上面的这种实现方式全局的开启,也就是所有的接口都会开启,虽然可以设置mime-types来进行一定的过滤,但是依然会有很大的覆盖面。如果只想对某个接口生效或者指定一些接口生效又应该怎么实现呢?本节来讲讲针对这种情况的实现。1、实现原理基于局部约定配置的方式的实现原理其实是通过自定义 Filter 来精确控制哪些接口启用压缩,因此通过过滤器就可以将我们需要设定的请求路径进行针对性过滤,从而开启针对这些接口的Gzip过滤压缩。下面我们来看看在SpringBoot中如何实现呢?2、具体代码实现首先在SpringBoot中直接创建一个过滤器Filter,关键代码如下:package com.yelang.framework.interceptor.gzip;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;import java.io.IOException;import java.util.Arrays;import java.util.List;import java.util.zip.GZIPOutputStream;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.ServletOutputStream;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.WriteListener;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpServletResponseWrapper;@Componentpublic class SelectiveGzipFilter implements Filter { private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final List<String> gzipPatterns = Arrays.asList( "/eq/province/geojson/**", "/eq/province/detourcoefficient/list/**", "/eq/info/home/earthinfo", "/eq/province/abbreviations/list" ); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String requestUri = req.getRequestURI(); String contextPath = req.getContextPath(); boolean match = gzipPatterns.stream() .anyMatch(pattern -> pathMatcher.match(pattern, requestUri)); if (match) { System.out.println("成功匹配"); // 启用 GZIP 压缩逻辑(同上) HttpServletResponse res = (HttpServletResponse) response; res.setHeader("Content-Encoding", "gzip"); res.setHeader("Content-Type", "application/json"); GZIPResponseWrapper gzipResponse = new GZIPResponseWrapper(res); chain.doFilter(request, gzipResponse); gzipResponse.finish(); } else { System.out.println("未匹配上..."); chain.doFilter(request, response); } } static class GZIPResponseWrapper extends HttpServletResponseWrapper { private final GZIPOutputStream gzipOutputStream; public GZIPResponseWrapper(HttpServletResponse response) throws IOException { super(response); gzipOutputStream = new GZIPOutputStream(response.getOutputStream()); } @Override public ServletOutputStream getOutputStream() { return new ServletOutputStream() { @Override public void write(int b) throws IOException { gzipOutputStream.write(b); } @Override public boolean isReady() { returntrue; } @Override public void setWriteListener(WriteListener writeListener) { throw new UnsupportedOperationException(); } }; } public void finish() throws IOException { gzipOutputStream.finish(); } }}这里演示了如何设置多个目标URL地址的配置方式,使用AntPathMatcher 来进行匹配实现。需要注意的是,这里我们没有区分请求的头地址,以若依为例,可能会在匹配时无法正确对应,导致无法正常的开启Gzip压缩,因此为了保证正确的启用,我们在进行地址匹配时,需要自动过滤项目的服务名称,详细代码如下:String contextPath = req.getContextPath();// 去掉 context-path 部分,只匹配相对路径String relativePath = requestUri.substring(contextPath.length());System.out.println("relativePath==>" + relativePath);/*boolean match = gzipPatterns.stream() .anyMatch(pattern -> pathMatcher.match(pattern, requestUri));*/boolean match = gzipPatterns.stream() .anyMatch(pattern -> pathMatcher.match(pattern, relativePath));其实这里的contenxPath对应的就是配置文件中定义的参数:# 开发环境配置server: # 服务器的HTTP端口,默认为80 port: 8080 servlet: # 应用的访问路径 context-path: /earthqadminGeoJSON 数据通常体积较大,压缩后可显著减少传输时间。以下是实战建议:图片只有加上以上代码后才能实现正确匹配,在访问地址被请求是输出如下信息:图片五、总结以上就是本文的主要内容,我们将详细介绍在 SpringBoot 中开启 Gzip 压缩的两种方式的具体实现步骤,并通过实际的 GeoJSON 数据压缩案例,展示这两种方式的应用效果和优缺点。希望通过本文的介绍,能够帮助读者更好地理解和掌握在 SpringBoot 应用中使用 Gzip 压缩技术的方法,从而提升自己开发的 WebGIS 应用的性能和用户体验。博文首先简单介绍了Gzip的相关知识,然后介绍了在SpringBoot中开启Gzip的方式,最后以代码加案例的形式详细的介绍全局开启Gzip和局部开启Gzip的两种不同模式实现原理及具体代码实现。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。
-
无需重启服务,实时更新配置! 本文将深入探索Spring Boot中@RefreshScope的神奇力量,让你的应用配置在运行时动态刷新,彻底告别服务重启的烦恼。 一、为什么需要动态刷新配置?在传统Java应用中,修改配置文件后必须重启服务才能生效,这会导致:• 服务中断: 重启期间服务不可用• 状态丢失: 内存中的临时数据被清空• 运维复杂: 需要复杂的发布流程Spring Boot的@RefreshScope完美解决了这些问题,实现配置热更新,让应用像乐高积木一样灵活重组! 二、@RefreshScope核心原理 1. 工作原理图解graph TD A[修改配置文件] --> B[发送POST刷新请求] B --> C[/actuator/refresh 端点] C --> D[RefreshScope 刷新机制] D --> E[销毁旧Bean并创建新Bean] E --> F[新配置立即生效] 2. 关键技术解析• 作用域代理: 为Bean创建动态代理,拦截方法调用• 配置绑定: 当配置更新时,重新绑定@Value注解的值• Bean生命周期管理: 销毁并重新初始化被@RefreshScope标记的Bean 三、完整实现步骤 步骤1:添加必要依赖<!-- pom.xml --><dependencies> <!-- Spring Boot基础依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 配置刷新核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- 配置中心支持 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> <version>3.1.3</version> </dependency></dependencies> 步骤2:启用刷新机制// 主应用类@SpringBootApplication@EnableRefreshScope // 关键注解:开启配置刷新能力publicclassDynamicConfigApp { publicstaticvoidmain(String[] args) { SpringApplication.run(DynamicConfigApp.class, args); }}步骤3:配置application.yml# 应用基础配置app:feature: enabled:true timeout:5000 retry-count:3 welcome-msg:"Hello, Dynamic Config!"# 暴露刷新端点(关键!)management:endpoints: web: exposure: include:refresh,health,info 步骤4:创建动态配置Bean@Service@RefreshScope// 标记此Bean支持动态刷新publicclassFeatureService { // 注入可刷新的配置项 @Value("${app.feature.enabled}") privateboolean featureEnabled; @Value("${app.feature.timeout}") privateint timeout; @Value("${app.feature.retry-count}") privateint retryCount; @Value("${app.feature.welcome-msg}") private String welcomeMessage; public String getFeatureConfig() { return String.format(""" Feature Enabled: %s Timeout: %d ms Retry Count: %d Message: %s """, featureEnabled, timeout, retryCount, welcomeMessage); }} 步骤5:创建测试控制器@RestController@RequestMapping("/config")publicclassConfigController { privatefinal FeatureService featureService; // 构造函数注入 publicConfigController(FeatureService featureService) { this.featureService = featureService; } @GetMapping public String getConfig() { return featureService.getFeatureConfig(); }} 步骤6:触发配置刷新修改application.yml后,发送刷新请求:curl -X POST http://localhost:8080/actuator/refresh响应示例(返回被修改的配置项):["app.feature.timeout", "app.feature.welcome-msg"] 四、深入理解@RefreshScope 1. 作用域代理原理// 伪代码:Spring如何实现动态刷新publicclassRefreshScopeProxyimplementsApplicationContextAware { private Object targetBean; @Override public Object invoke(Method method) { if (configChanged) { // 1. 销毁旧Bean context.destroyBean(targetBean); // 2. 重新创建Bean targetBean = context.getBean(beanName); } return method.invoke(targetBean, args); }} 2. 刷新范围控制技巧场景1:只刷新特定Bean的部分属性@Component@RefreshScopepublicclassPaymentService { // 只有带@Value的属性会刷新 @Value("${payment.timeout}") privateint timeout; // 不会被刷新的属性 privatefinalStringapiVersion="v1.0"; }场景2:组合配置类刷新@Configuration@RefreshScope// 整个配置类可刷新publicclassAppConfig { @Bean @RefreshScope public FeatureService featureService() { returnnewFeatureService(); } @Value("${app.theme}") private String theme;} 五、生产环境最佳实践 1. 安全加固配置management: endpoint: refresh: enabled:trueendpoints: web: exposure: include:refresh base-path:/internal# 修改默认路径 path-mapping: refresh:secure-refresh# 端点重命名# 添加安全认证spring:security: user: name:admin password:$2a$10$NVM0n8ElaRgg7zWO1CxUdei7vWoQP91oGycgVNCY8GQEx.TGx.AaC 2. 自动刷新方案方案1:Git Webhook自动刷新 方案2:配置中心联动(Nacos示例)//bootstrap.ymlspring: cloud: nacos: config: server-addr:localhost:8848 auto-refresh:true # 开启自动刷新 六、常见问题排查 问题1:刷新后配置未生效解决方案:• 检查是否添加@RefreshScope• 确认刷新端点返回了修改的配置项• 查看日志:logging.level.org.springframework.cloud=DEBUG 问题2:多实例刷新不同步解决方案:# 使用Spring Cloud Bus同步刷新curl-XPOSThttp://host:port/actuator/bus-refresh 问题3:配置更新导致内存泄漏预防措施:@PreDestroypublicvoidcleanUp() { // 清理资源} 七、扩展应用场景动态功能开关:实时开启/关闭功能模块# 修改后立即生效feature.new-checkout.enabled=true运行时日志级别调整@RefreshScopepublicclassLogConfig { @Value("${logging.level.root}") private String logLevel; // 动态应用新日志级别}数据库连接池调优# 动态修改连接池配置spring.datasource.hikari.maximum-pool-size=20 结语:拥抱动态配置新时代通过@RefreshScope,我们实现了:• ✅ 零停机配置更新• ✅ 即时生效的应用参数• ✅ 更灵活的运维体验• ✅ 资源利用最大化最佳实践建议:• 敏感配置(如密码)避免使用动态刷新• 配合配置中心(Nacos/Config Server)使用• 生产环境务必保护刷新端点
-
本文将介绍如何利用Java Agent技术实现对SpringBoot应用的无侵入式监控,帮助开发人员在不修改源码的情况下获取应用运行时的关键指标。Java Agent简介Java Agent是JDK 1.5引入的特性,它允许我们在JVM启动时或运行时动态地修改已加载的类字节码,从而实现对应用行为的增强或监控。Java Agent的核心优势在于能够在不修改源代码的情况下,对应用进行功能扩展。Java Agent主要有两种使用方式:启动时加载(premain)运行时加载(agentmain)本文将主要关注启动时加载的方式。技术原理Java Agent的工作原理基于字节码增强技术,通过在类加载过程中修改字节码来实现功能增强。在SpringBoot应用监控场景中,我们可以利用Java Agent拦截关键方法的调用,收集执行时间、资源使用情况等指标。主要技术栈:• Java Agent:提供字节码修改的入口• Byte Buddy/ASM/Javassist:字节码操作库• SpringBoot:目标应用框架• Micrometer:指标收集与暴露实现步骤1. 创建Agent项目首先,我们需要创建一个独立的Maven项目用于开发Java Agent:<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>demo</groupId> <artifactId>springboot-agent</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <artifactId>agent</artifactId> <dependencies> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.14.5</version> </dependency> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy-agent</artifactId> <version>1.14.5</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>1.10.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>21</source> <target>21</target> <encoding>utf-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <archive> <manifestEntries> <Premain-Class>com.example.agent.MonitorAgent</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> </plugins> </build></project>2. 实现Agent主类创建MonitorAgent类,实现premain方法:package com.example.agent;import net.bytebuddy.agent.builder.AgentBuilder;import net.bytebuddy.implementation.MethodDelegation;import net.bytebuddy.matcher.ElementMatchers;import java.lang.instrument.Instrumentation;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;public class MonitorAgent { private static final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); public static void premain(String arguments, Instrumentation instrumentation) { System.out.println("SpringBoot监控Agent已启动..."); log(); // 使用ByteBuddy拦截SpringBoot的Controller方法 new AgentBuilder.Default() .type(ElementMatchers.nameEndsWith("Controller")) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> builder.method(ElementMatchers.isAnnotatedWith( ElementMatchers.named("org.springframework.web.bind.annotation.RequestMapping") .or(ElementMatchers.named("org.springframework.web.bind.annotation.GetMapping")) .or(ElementMatchers.named("org.springframework.web.bind.annotation.PostMapping")) .or(ElementMatchers.named("org.springframework.web.bind.annotation.PutMapping")) .or(ElementMatchers.named("org.springframework.web.bind.annotation.DeleteMapping")) )) .intercept(MethodDelegation.to(ControllerInterceptor.class)) ) .installOn(instrumentation); } private static void log(){ executorService.scheduleAtFixedRate(() -> { // 收集并打印性能指标 String text = MetricsCollector.scrape(); System.out.println("==============="); System.out.println(text); }, 0, 5, TimeUnit.SECONDS); }}3. 实现拦截器创建Controller拦截器:package com.example.agent;import net.bytebuddy.implementation.bind.annotation.*;import java.lang.reflect.Method;import java.util.concurrent.Callable;public class ControllerInterceptor { @RuntimeType public static Object intercept( @Origin Method method, @SuperCall Callable<?> callable, @AllArguments Object[] args) throws Exception { long startTime = System.currentTimeMillis(); String className = method.getDeclaringClass().getName(); String methodName = method.getName(); try { // 调用原方法 return callable.call(); } catch (Exception e) { // 记录异常信息 MetricsCollector.recordException(className, methodName, e); throw e; } finally { long executionTime = System.currentTimeMillis() - startTime; // 收集性能指标 MetricsCollector.recordExecutionTime(className, methodName, executionTime); } }}4. 实现指标收集创建MetricsCollector类用于收集和暴露监控指标:package com.example.agent;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.atomic.AtomicLong;public class MetricsCollector { private static final Map<String, AtomicLong> executionTimeMap = new ConcurrentHashMap<>(); private static final Map<String, AtomicLong> invocationCountMap = new ConcurrentHashMap<>(); private static final Map<String, AtomicLong> exceptionCountMap = new ConcurrentHashMap<>(); public static void recordExecutionTime(String className, String methodName, long executionTime) { String key = className + "." + methodName; executionTimeMap.computeIfAbsent(key, k -> new AtomicLong(0)).addAndGet(executionTime); invocationCountMap.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet(); // 输出日志,实际项目中可能会发送到监控系统 System.out.printf("Controller执行: %s, 耗时: %d ms%n", key, executionTime); } public static void recordException(String className, String methodName, Exception e) { String key = className + "." + methodName; exceptionCountMap.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet(); System.out.printf("Controller异常: %s, 异常类型: %s, 消息: %s%n", key, e.getClass().getName(), e.getMessage()); } public static void recordSqlExecutionTime(String className, String methodName, long executionTime) { String key = className + "." + methodName; executionTimeMap.computeIfAbsent(key, k -> new AtomicLong(0)).addAndGet(executionTime); invocationCountMap.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet(); System.out.printf("SQL执行: %s, 耗时: %d ms%n", key, executionTime); } public static void recordSqlException(String className, String methodName, Exception e) { String key = className + "." + methodName; exceptionCountMap.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet(); System.out.printf("SQL异常: %s, 异常类型: %s, 消息: %s%n", key, e.getClass().getName(), e.getMessage()); } // 获取各种指标的方法,可以被监控系统调用 public static Map<String, AtomicLong> getExecutionTimeMap() { return executionTimeMap; } public static Map<String, AtomicLong> getInvocationCountMap() { return invocationCountMap; } public static Map<String, AtomicLong> getExceptionCountMap() { return exceptionCountMap; }}5. 集成Prometheus与Grafana(可选)为了更好地可视化监控数据,我们可以将收集到的指标暴露给Prometheus,并使用Grafana进行展示。首先,添加Micrometer相关依赖:<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>1.10.0</version></dependency>然后,修改MetricsCollector类,将收集到的指标注册到Micrometer:package com.example.agent;import io.micrometer.core.instrument.Counter;import io.micrometer.core.instrument.MeterRegistry;import io.micrometer.core.instrument.Timer;import io.micrometer.prometheus.PrometheusConfig;import io.micrometer.prometheus.PrometheusMeterRegistry;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.TimeUnit;public class MetricsCollector { private static final PrometheusMeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); private static final Map<String, Timer> timers = new ConcurrentHashMap<>(); private static final Map<String, Counter> exceptionCounters = new ConcurrentHashMap<>(); public static void recordExecutionTime(String className, String methodName, long executionTime) { String key = className + "." + methodName; getOrCreateTimer(key, "controller").record(executionTime, TimeUnit.MILLISECONDS); System.out.printf("Controller执行: %s, 耗时: %d ms%n", key, executionTime); } public static void recordException(String className, String methodName, Exception e) { String key = className + "." + methodName; getOrCreateExceptionCounter(key, "controller", e.getClass().getSimpleName()).increment(); System.out.printf("Controller异常: %s, 异常类型: %s, 消息: %s%n", key, e.getClass().getName(), e.getMessage()); } public static void recordSqlExecutionTime(String className, String methodName, long executionTime) { String key = className + "." + methodName; getOrCreateTimer(key, "sql").record(executionTime, TimeUnit.MILLISECONDS); System.out.printf("SQL执行: %s, 耗时: %d ms%n", key, executionTime); } public static void recordSqlException(String className, String methodName, Exception e) { String key = className + "." + methodName; getOrCreateExceptionCounter(key, "sql", e.getClass().getSimpleName()).increment(); System.out.printf("SQL异常: %s, 异常类型: %s, 消息: %s%n", key, e.getClass().getName(), e.getMessage()); } private static Timer getOrCreateTimer(String name, String type) { return timers.computeIfAbsent(name, k -> Timer.builder("app.execution.time") .tag("name", name) .tag("type", type) .register(registry) ); } private static Counter getOrCreateExceptionCounter(String name, String type, String exceptionType) { String key = name + "." + exceptionType; return exceptionCounters.computeIfAbsent(key, k -> Counter.builder("app.exception.count") .tag("name", name) .tag("type", type) .tag("exception", exceptionType) .register(registry) ); } // 获取Prometheus格式的指标数据 public static String scrape() { return registry.scrape(); } // 获取注册表,可以被其他组件使用 public static MeterRegistry getRegistry() { return registry; }}6. 启动Agent并应用到SpringBoot应用编译并打包Agent项目后,可以通过JVM参数将Agent添加到SpringBoot应用中:java -javaagent:/path/to/springboot-monitor-agent.jar -jar your-springboot-app.jar进阶扩展除了基本的监控功能外,我们还可以对Agent进行以下扩展:1. JVM指标监控监控JVM的内存使用、GC情况、线程数等指标:private static void monitorJvmMetrics(MeterRegistry registry) { // 注册JVM内存指标 new JvmMemoryMetrics().bindTo(registry); // 注册GC指标 new JvmGcMetrics().bindTo(registry); // 注册线程指标 new JvmThreadMetrics().bindTo(registry);}2. HTTP客户端监控监控应用发起的HTTP请求:new AgentBuilder.Default() .type(ElementMatchers.nameContains("RestTemplate") .or(ElementMatchers.nameContains("HttpClient"))) .transform((builder, typeDescription, classLoader, module, protectionDomain) -> builder.method(ElementMatchers.named("execute") .or(ElementMatchers.named("doExecute")) .or(ElementMatchers.named("exchange"))) .intercept(MethodDelegation.to(HttpClientInterceptor.class)) ) .installOn(instrumentation);3. 分布式追踪集成与Zipkin或Jaeger等分布式追踪系统集成,实现全链路追踪:public static void recordTraceInfo(String className, String methodName, String traceId, String spanId) { // 记录追踪信息 MDC.put("traceId", traceId); MDC.put("spanId", spanId); // 处理逻辑...}优势与注意事项优势无侵入性:不需要修改应用源代码灵活性:可以动态决定要监控的类和方法通用性:适用于任何基于SpringBoot的应用运行时监控:可以实时收集应用运行数据注意事项性能影响:字节码增强会带来一定的性能开销,需要合理选择监控点兼容性:需要确保Agent与应用的JDK版本兼容稳定性:Agent本身的异常不应影响应用主流程安全性:收集的数据可能包含敏感信息,需要注意数据安全总结在实际使用中,我们可以根据具体需求,对Agent进行定制化开发,实现更加精细化的监控。同时,可以将Agent与现有的监控系统集成,构建完整的应用性能监控体系。
-
单点登录(Single Sign-On,简称 SSO) 是一种身份认证机制,允许用户仅通过一次登录,即可访问多个相互信任的应用系统或服务,而无需在每个系统中重复输入用户名和密码。其核心目标是简化用户认证流程,提升用户体验,同时降低企业IT管理的复杂度。1. 单点登录的核心原理核心思想“一次认证,全网通行”:用户登录一次后,由身份提供方(Identity Provider, IdP)生成一个认证凭证(如 Token 或 Ticket),其他服务提供方(Service Provider, SP)通过验证该凭证即可授予访问权限。基于信任关系:参与SSO的系统需预先建立信任(如通过共享密钥、OAuth2.0协议、SAML协议等)。关键组件用户(User):需要访问多个系统的终端用户。身份提供方(IdP):负责用户认证的中心化系统(如企业AD、OAuth2.0服务器、第三方身份平台如Auth0)。服务提供方(SP):用户需要访问的应用或服务(如Web应用、移动App、内部系统)。协议:定义IdP和SP之间通信的规则(如SAML、OAuth2.0、OpenID Connect、CAS)。2. 单点登录的工作流程以 OAuth2.0 + OpenID Connect 为例(常见于现代Web应用):步骤 1:用户访问应用用户打开应用A(SP),尝试登录。应用A检测到用户未登录,重定向到身份提供方(IdP,如Google账号登录页面)。步骤 2:用户身份认证用户在IdP页面输入用户名和密码(或通过二维码、短信等二次验证)。IdP验证用户身份后,生成一个**授权码(Authorization Code)**并返回给应用A。步骤 3:获取访问令牌应用A用授权码向IdP请求访问令牌(Access Token)和身份令牌(ID Token)。IdP验证授权码后,返回令牌(通常为JWT格式,包含用户信息如邮箱、ID等)。步骤 4:访问其他应用用户访问应用B(另一个SP)时,应用B检测到用户已有有效令牌,直接放行。若令牌过期,应用B可引导用户通过IdP重新认证(无需再次输入密码)。简化流程图用户 → 应用A → IdP(认证) → 应用A(获取令牌) → 应用A/B(访问资源)3. 单点登录的常见协议协议全称特点应用场景SAMLSecurity Assertion Markup Language基于XML,企业级,支持复杂场景(如多因素认证)。企业内部系统、云服务集成OAuth2.0Open Authorization 2.0授权框架(非认证协议),允许第三方应用访问用户资源(如用微信登录其他App)。移动App、API授权OpenID ConnectOAuth2.0的扩展在OAuth2.0基础上增加身份认证功能,返回用户信息(JWT格式)。现代Web应用、社交登录CASCentral Authentication Service轻量级,基于HTTP/HTTPS,适合高校或中小型企业。教育机构、内部系统4. 单点登录的优缺点优点用户体验提升:用户无需记住多个密码,减少登录摩擦。管理效率提高:IT部门可统一管理用户账号(如禁用、修改密码、权限控制)。安全性增强:减少密码泄露风险(用户更倾向于使用强密码或启用MFA)。合规性支持:满足审计要求(如记录所有系统的登录日志)。缺点单点故障风险:若IdP宕机,所有依赖SSO的系统均无法登录。实施复杂度:需协调多个系统的开发和配置(尤其是跨域或跨组织场景)。安全边界扩展:一旦IdP被攻破,攻击者可访问所有关联系统(需结合MFA、零信任等加固)。5. 单点登录的典型应用场景企业办公系统员工通过企业AD账号一次登录,访问邮箱、OA、CRM、ERP等系统。云服务集成用Google/Microsoft账号登录Slack、Trello、Zoom等第三方服务。高校/教育机构学生用学号登录图书馆、选课系统、在线学习平台。政府/公共服务公民用身份证号或支付宝/微信登录政务服务平台(如“一网通办”)。6. 单点登录的安全实践多因素认证(MFA):在SSO基础上增加短信、令牌或生物识别验证。令牌加密:使用HTTPS传输令牌,避免中间人攻击。短期令牌:设置Access Token的较短有效期(如1小时),减少泄露风险。审计日志:记录所有SSO登录事件,便于追踪异常行为。零信任架构:结合持续身份验证(如设备状态、行为分析),而非仅依赖初始登录。7. 常见问题Q1:SSO 和 OAuth2.0 有什么区别?SSO是概念,指“一次登录访问多个系统”。OAuth2.0是协议,用于授权第三方应用访问用户资源(如用微信登录拼多多)。OpenID Connect是OAuth2.0的扩展,专门用于SSO场景(如用Google账号登录其他网站)。Q2:如果IdP被攻破,所有系统都会沦陷吗?是潜在风险,但可通过以下措施缓解:启用MFA(如短信+密码)。限制令牌的权限范围(如只读访问)。使用短期令牌并定期刷新。结合零信任策略(如持续验证设备安全性)。Q3:SSO 适合小型团队吗?适合。小型团队可用免费SSO解决方案(如Google Workspace、Okta Free Tier)统一管理账号,避免密码混乱。总结单点登录(SSO)通过中心化认证机制,显著提升了用户体验和IT管理效率,是现代数字化系统的标配。其实现需结合具体场景选择协议(如SAML用于企业、OAuth2.0用于移动App),并注重安全性设计(如MFA、零信任)。无论是个人用户(如用微信登录多个App)还是企业(如统一管理内部系统),SSO都能大幅简化认证流程,同时降低安全风险。
-
1. 为什么需要静态资源映射?在开发Web应用时,我们经常需要处理文件上传与访问,例如:用户头像存储后,如何通过URL直接访问?博客文章中的图片如何托管到服务器并提供外链?如何避免将文件放到 resources/static 目录下,导致项目臃肿?传统做法可能是使用Nginx反向代理,但对于小型项目或快速开发场景,我们可以直接用 Spring MVC 的静态资源映射 功能,将本地磁盘的某个目录映射为Web可访问路径。本文将基于 WebMvcConfigurer 手写配置,实现 本地文件目录映射为Web URL,并解决常见问题。2. 核心代码解析我们先来看完整的配置类:123456789101112131415161718package com.qcby.AICommunity.config; import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configurationpublic class WebMVCConfiguration implements WebMvcConfigurer { @Value("${upload-path.face}") // 从配置文件读取路径 private String face; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/community/upload/face/**") .addResourceLocations("file:" + face); }}2.1 关键点解析**@Configuration**声明这是一个Spring配置类,会被自动加载。**@Value("${upload-path.face}")**从 application.yml 或 application.properties 注入文件存储路径,例如:123upload-path: face: D:/upload/face/ # Windows # face: /var/upload/face/ # Linux**addResourceHandlers 方法**addResourceHandler("/community/upload/face/**")定义Web访问的URL模式,** 表示匹配任意子路径。addResourceLocations("file:" + face)指定本地文件系统路径,file: 前缀表示磁盘目录(而非classpath资源)。3. 实际应用示例3.1 文件上传Controller假设我们有一个头像上传接口:123456789101112131415@RestController@RequestMapping("/user")public class UserController { @Value("${upload-path.face}") private String uploadPath; @PostMapping("/upload-avatar") public String uploadAvatar(MultipartFile file) throws IOException { String fileName = UUID.randomUUID() + ".jpg"; File dest = new File(uploadPath + fileName); file.transferTo(dest); // 保存到本地 return "/community/upload/face/" + fileName; // 返回访问路径 }}文件会被保存到 D:/upload/face/xxx.jpg(取决于配置)。前端可通过 http://your-domain.com/community/upload/face/xxx.jpg 直接访问。3.2 前端调用示例1234567891011121314// 上传头像const fileInput = document.querySelector('input[type="file"]');const formData = new FormData();formData.append('file', fileInput.files[0]); fetch('/user/upload-avatar', { method: 'POST', body: formData}).then(response => response.text()).then(url => { console.log('访问地址:', url); // 示例结果: "/community/upload/face/a1b2c3d4.jpg"});4. 常见问题与优化4.1 路径分隔符兼容性问题Windows 使用反斜杠 \,而 Linux 使用正斜杠 /。建议在配置中统一:123private String face = "D:/upload/face/"; // 推荐// 或private String face = "D:\\upload\\face\\"; // 需转义4.2 权限问题确保应用有权限读写目标目录(生产环境注意 chmod 或 Windows 权限设置)。4.3 动态路径映射如果需要按用户ID分目录存储:12345678@Value("${upload-path.base}")private String basePath; @Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/user/**") .addResourceLocations("file:" + basePath + "user/");}然后在保存文件时:12String userDir = basePath + "user/" + userId + "/";Files.createDirectories(Paths.get(userDir)); // 创建用户目录4.4 结合Nginx优化(生产环境推荐)虽然Spring Boot能直接映射本地文件,但生产环境建议:用Nginx处理静态资源,减轻应用服务器压力。配置缓存、CDN加速访问。5. 总结本文通过 WebMvcConfigurer 实现了:本地磁盘目录映射为Web可访问URL。动态配置文件存储路径(通过 @Value)。解决跨平台路径问题。适用场景:开发环境快速搭建文件访问服务。中小型项目避免引入Nginx的简化方案。进一步优化:结合Spring Security控制访问权限。使用OSS(如阿里云OSS)替代本地存储。
-
01CharacterEncodingFilter —— 乱码终结者关键词:UTF-8、forceEncoding、Ordered.HIGHEST_PRECEDENCE只要出现中文、emoji、阿拉伯文,就用它!12345678910@Beanpublic FilterRegistrationBean<CharacterEncodingFilter> characterEncodingFilter() { CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("UTF-8"); // ① 指定编码 filter.setForceEncoding(true); // ② 强制覆盖已有编码 FilterRegistrationBean<CharacterEncodingFilter> bean = new FilterRegistrationBean<>(filter); bean.addUrlPatterns("/*"); // ③ 拦截所有请求 bean.setOrder(Ordered.HIGHEST_PRECEDENCE);// ④ 最先执行,防止其他过滤器捣蛋 return bean;}注解:setEncoding 解决请求/响应乱码;setForceEncoding(true) 覆盖 Tomcat 默认 ISO-8859-1;addUrlPatterns(“/*”) 全局生效;最高优先级,保证后续过滤器拿到的就是 UTF-8。02HiddenHttpMethodFilter —— 把 POST 伪装成 PUT/DELETE关键词:RESTful、_method、HTML 表单1234<form action="/books/7" method="post"> <input type="hidden" name="_method" value="DELETE"/> <button type="submit">删除图书</button></form>spring.mvc.hiddenmethod.filter.enabled=true # application.yml 一行搞定注解:表单必须是 POST;隐藏字段 _method 的值就是真实 HTTP 方法;Spring MVC 会自动路由到 @DeleteMapping。03FormContentFilter —— PUT/PATCH 也能玩表单关键词:application/x-www-form-urlencoded、非 POST 表单spring.mvc.formcontent.filter.enabled: true # 同样一行配置场景:老项目前端不会发 application/json,但接口想用 PUT 更新。123456@PutMapping("/users/{id}")public String updateUser(@PathVariable Long id, UserForm form) { // 表单字段自动封装进 UserForm userService.update(id, form); return "redirect:/users";}注解:过滤器把 PUT 请求体解析成 Map<String,String[]>;Spring 数据绑定无缝衔接。04RequestContextFilter —— 随时随地拿 Request关键词:RequestContextHolder、非 Controller 取 IP12345678910@Servicepublic class ClientInfoService { public String whoami() { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest req = attrs.getRequest(); return String.format("IP: %s, UA: %s", req.getRemoteAddr(), req.getHeader("User-Agent")); }}注解:任何地方都能拿到当前线程的 HttpServletRequest;适用于日志、审计、灰度路由。05CorsFilter —— 跨域通行证关键词:Access-Control-Allow-Origin、Credentials、预检1234567891011@Beanpublic CorsFilter corsFilter() { CorsConfiguration cfg = new CorsConfiguration(); cfg.setAllowCredentials(true); // ① 允许携带 Cookie cfg.addAllowedOrigin("https://spa.xxx.com"); // ② 白名单域名 cfg.addAllowedHeader("*"); // ③ 任意请求头 cfg.addAllowedMethod("*"); // ④ 任意方法 UrlBasedCorsConfigurationSource src = new UrlBasedCorsConfigurationSource(); src.registerCorsConfiguration("/api/**", cfg); return new CorsFilter(src);}注解:精准控制哪些接口、哪些域名可以跨域;支持 Authorization 头与 Cookie;比 @CrossOrigin 粒度更细,可集中管理。06ShallowEtagHeaderFilter —— 让 304 飞起来关键词:ETag、缓存、节省带宽1234567@Beanpublic FilterRegistrationBean<ShallowEtagHeaderFilter> etag() { FilterRegistrationBean<ShallowEtagHeaderFilter> bean = new FilterRegistrationBean<>(); bean.setFilter(new ShallowEtagHeaderFilter()); bean.addUrlPatterns("/static/*", "/api/report/*"); return bean;}第一次 200 + ETag,第二次 304,直接省 80% 流量!注解:计算响应体 MD5 作为 ETag;客户端带 If-None-Match 对比即可;静态资源、报表接口效果最佳。07ForwardedHeaderFilter —— 反向代理小棉袄关键词:X-Forwarded-Proto、Nginx、ELB、HTTPS1234567@Beanpublic FilterRegistrationBean<ForwardedHeaderFilter> forwarded() { FilterRegistrationBean<ForwardedHeaderFilter> bean = new FilterRegistrationBean<>(); bean.setFilter(new ForwardedHeaderFilter()); bean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 越早越好 return bean;}注解:自动重写 request.getScheme()、getServerName();解决 HTTPS 终止在 Nginx 时,重定向变成 http 的尴尬。08OrderedRequestContextFilter —— 顺序可控的 RequestContext如果你写了 10+ 个自定义 Filter,谁先谁后就是生命线。默认优先级:REQUEST_WRAPPER_FILTER_MAX_ORDER - 10000想插队?直接实现 Ordered 接口09ResourceUrlEncodingFilter —— 静态资源“带版本号”关键词:缓存破坏、内容哈希、Thymeleaf 自动替换1234567891011121314151617@Configurationpublic class WebCfg implements WebMvcConfigurer { @Bean public ResourceUrlEncodingFilter resourceUrlEncodingFilter() { return new ResourceUrlEncodingFilter(); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/") .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)) .resourceChain(true) .addResolver(new VersionResourceResolver() .addContentVersionStrategy("/**")); // ① 哈希指纹 }}Thymeleaf 模板:12<link rel="stylesheet" th:href="@{/static/css/app.css}" rel="external nofollow" /><!-- 实际输出:/static/css/app-8a9b2c3.css -->注解:文件内容变动 → 指纹变化 → 浏览器重新下载;365 天强缓存,更新即发版,用户无感知。
-
演示环境说明:开发工具:IDEA 2021.3JDK版本: JDK 1.8Spring Boot版本:2.3.1 RELEASEMaven版本:3.8.2操作系统:Windows 11📝 前言:定时任务和调度的应用场景 定时任务和调度是现代应用中常见的功能,特别是在需要执行周期性任务的情况下,例如定时备份数据、发送通知、清理临时文件等。在微服务架构、分布式系统以及大规模应用中,定时任务的管理变得更加重要。 Spring Boot提供了两种常见的定时任务实现方式:一种是基于@Scheduled注解的简单定时任务,另一种是功能更强大的Quartz框架。本文将介绍如何在Spring Boot中使用这两种方式实现定时任务和调度功能。🚀 定时任务概述 定时任务是指按照预定的时间间隔或固定的时间点执行的任务。它可以是简单的周期性执行任务,也可以是复杂的定时调度任务。常见的定时任务应用包括:定期清理缓存或临时文件定时报告生成与发送定期同步外部数据定时检查系统健康状态定时任务调度的分类固定间隔任务:每隔固定时间执行一次任务。固定时间点任务:在特定的时间点执行任务(例如每天午夜12点)。复杂调度任务:需要支持更灵活调度的场景,比如Quartz调度框架。🛠️ 方法一:使用@Scheduled注解 Spring Boot内置的@Scheduled注解非常适合用于简单的定时任务,支持定时任务的执行和控制。你只需要在方法上添加@Scheduled注解,并配置执行的时间策略。1. 启用定时任务 要使用@Scheduled注解,首先需要在Spring Boot的主类或配置类上启用定时任务功能:12345678910import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.scheduling.annotation.EnableScheduling;@SpringBootApplication@EnableScheduling // 启用定时任务功能public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}@EnableScheduling:该注解启用Spring的定时任务调度功能。2. 使用@Scheduled注解@Scheduled注解可以接受多种参数来定义定时任务的执行策略:fixedRate:每隔固定时间执行任务(单位:毫秒),无论上一个任务是否完成。fixedDelay:任务结束后延迟一定时间再执行下一次任务。initialDelay:任务开始执行前的延迟时间。cron:使用Cron表达式指定任务的执行时间。示例:简单的定时任务1234567891011121314151617181920import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;@Componentpublic class SimpleScheduledTask { // 每5秒执行一次 @Scheduled(fixedRate = 5000) public void runTask() { System.out.println("任务执行了! " + System.currentTimeMillis()); } // 延迟1秒后开始,每次执行完后间隔3秒再执行 @Scheduled(initialDelay = 1000, fixedDelay = 3000) public void runDelayedTask() { System.out.println("延迟任务执行! " + System.currentTimeMillis()); } // 使用Cron表达式,每天午夜12点执行任务 @Scheduled(cron = "0 0 0 * * ?") public void runCronTask() { System.out.println("Cron任务执行! " + System.currentTimeMillis()); }}fixedRate:每5秒执行一次任务。fixedDelay:延迟1秒后开始,每次执行完后延迟3秒再执行。cron:根据Cron表达式每天午夜12点执行任务。3. 配置@Scheduled的参数@Scheduled提供了灵活的配置方式,可以根据不同需求选择合适的方式:fixedRate:表示任务之间的时间间隔,单位是毫秒。fixedDelay:表示任务执行完之后,再延迟多少时间进行下次执行。cron:通过Cron表达式定义任务的具体执行时间。4. 定时任务的配置 定时任务的调度机制也可以通过application.properties或application.yml来进行配置,以控制执行的间隔时间、超时等。123# application.properties配置spring.task.scheduling.pool.size=5 # 配置线程池大小spring.task.scheduling.shutdown.await-termination=true # 任务完成后是否等待线程池的关闭🚀 方法二:使用Quartz框架 Quartz是一个功能强大的定时任务调度框架,支持更复杂的调度需求,如任务的持久化、集群调度和触发器管理等。在Spring Boot中,可以通过spring-boot-starter-quartz来集成Quartz。1. 添加依赖在pom.xml中加入Quartz依赖:1234567<dependencies> <!-- Quartz Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency></dependencies>2. Quartz任务配置首先,我们需要配置Quartz调度器,可以在application.properties中配置Quartz相关参数:1234# application.properties配置spring.quartz.job-store-type=memory # 使用内存存储任务(也可以使用jdbc)spring.quartz.scheduler.instance-name=MySchedulerspring.quartz.thread-pool.size=103. 创建Quartz任务和调度器Quartz的任务通常需要实现Job接口,然后通过Scheduler进行调度。示例:创建一个简单的Quartz任务1234567891011import org.quartz.Job;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;import org.springframework.stereotype.Component;@Componentpublic class MyQuartzJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("Quartz任务执行中... " + System.currentTimeMillis()); }}示例:调度任务1234567891011121314151617181920212223242526272829import org.quartz.*;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class QuartzConfig { @Autowired private MyQuartzJob myQuartzJob; @Bean public JobDetail jobDetail() { return JobBuilder.newJob(MyQuartzJob.class) .withIdentity("myJob") .storeDurably() .build(); } @Bean public Trigger trigger() { return TriggerBuilder.newTrigger() .forJob(jobDetail()) .withSchedule(CronScheduleBuilder.cronSchedule("0 0/1 * * * ?")) // 每分钟执行一次 .build(); } @Bean public Scheduler scheduler() throws SchedulerException { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.scheduleJob(jobDetail(), trigger()); return scheduler; }}JobDetail:定义了任务的具体内容。Trigger:定义任务的触发器,在此配置了一个Cron触发器,使任务每分钟执行一次。4. Quartz的高级功能Quartz还支持一些高级功能,包括:任务持久化:通过数据库存储任务状态,支持任务的恢复。集群调度:在多个Quartz实例间分配任务。复杂的Cron表达式:支持灵活的调度规则。5. 使用Quartz的高级调度功能 Quartz能够处理更复杂的任务调度需求。例如,可以在不同的时间点触发不同的任务,或者在某些条件满足时自动启动任务。12345// 例如,创建一个复杂的Cron表达式任务,控制任务的执行时间@Scheduled(cron = "0 0 8 * * ?") // 每天8点执行public void runComplexTask() { System.out.println("执行复杂调度任务!");}✅ 总结:Spring Boot中的定时任务与调度 在Spring Boot中,定时任务和调度非常容易实现。通过@Scheduled注解,我们可以快速实现简单的定时任务,满足常见的周期性任务需求。而Quartz则是一个功能更强大的调度框架,适用于更复杂的任务调度需求,支持任务持久化、集群调度等功能。关键点:@Scheduled:适合简单的定时任务,支持fixedRate、fixedDelay和cron等多种方式。Quartz:适用于复杂的调度任务,支持任务的持久化、集群调度和灵活的Cron表达式。调度配置:通过application.properties来配置调度相关的参数,如线程池大小、任务超时等。通过Spring Boot的定时任务和Quartz集成,我们能够有效地管理任务调度,确保系统的稳定性和可维护性。
-
如果你想从 Mono<String>(JSON 字符串)中提取例如 data[0].name 并返回给前端,可以使用 Reactor 的 map 操作符 结合 JSON 解析库(如 Jackson 或 Gson)来处理。以下是修改后的代码:方案 1:使用 Jackson 解析 JSON(推荐)Spring Boot 默认集成 Jackson,可以直接解析 JSON:import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import reactor.core.publisher.Mono; @ApiOperation(value = "获取当前用户的最近会话名称") @GetMapping(value = "/api/v1/ai/chat-stream/conversations/last/name") public Mono<String> getLastConversationName() { return copilotApiService .getConversations(null, 1, "-updated_at") // 返回 Mono<String>(JSON) .map(json -> { try { ObjectMapper mapper = new ObjectMapper(); JsonNode rootNode = mapper.readTree(json); // 解析 JSON String name = rootNode.path("data").get(0).path("name").asText(); // 提取 name return name; // 返回单个字符串 } catch (Exception e) { throw new RuntimeException("Failed to parse JSON", e); } }); } 访问 /api/v1/ai/chat-stream/conversations/last/name 会返回:"战国灭亡顺序表格 📊" 方案 2:使用 Gson 解析 JSON如果项目使用 Gson,可以这样改:import com.google.gson.JsonObject; import com.google.gson.JsonParser; import reactor.core.publisher.Mono; @ApiOperation(value = "获取当前用户的最近会话名称") @GetMapping(value = "/api/v1/ai/chat-stream/conversations/last/name") public Mono<String> getLastConversationName() { return copilotApiService .getConversations(null, 1, "-updated_at") .map(json -> { JsonObject rootObject = JsonParser.parseString(json).getAsJsonObject(); String name = rootObject.getAsJsonArray("data") .get(0) .getAsJsonObject() .get("name") .getAsString(); return name; }); } 方案 3:定义 DTO 类(更规范)如果 JSON 结构复杂,建议定义 DTO 类,用 ObjectMapper 自动映射:import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; // 定义 DTO 类 public class ConversationResponse { private int limit; private boolean has_more; private List<ConversationData> data; // getters & setters public static class ConversationData { private String id; private String name; // 其他字段... // getters & setters } } // 控制器代码 @ApiOperation(value = "获取当前用户的最近会话名称") @GetMapping(value = "/api/v1/ai/chat-stream/conversations/last/name") public Mono<String> getLastConversationName() { return copilotApiService .getConversations(null, 1, "-updated_at") .map(json -> { ObjectMapper mapper = new ObjectMapper(); ConversationResponse response = mapper.readValue(json, ConversationResponse.class); return response.getData().get(0).getName(); // 直接返回 name }); } 关键点总结map 操作符:用于处理 Mono 中的数据。JSON 解析:简单场景:用 Jackson 或 Gson 手动解析。复杂场景:定义 DTO 类,自动映射。异常处理:如果 JSON 解析失败,可以抛出 RuntimeException 或自定义异常。最终推荐代码(Jackson + DTO)@ApiOperation(value = "获取当前用户的最近会话名称") @GetMapping(value = "/api/v1/ai/chat-stream/conversations/last/name") public Mono<String> getLastConversationName() { return copilotApiService .getConversations(null, 1, "-updated_at") .map(json -> { try { ObjectMapper mapper = new ObjectMapper(); ConversationResponse response = mapper.readValue(json, ConversationResponse.class); return response.getData().get(0).getName(); } catch (Exception e) { return "默认名称"; // 或者抛出异常 } }); } 这样就能 只返回 name 字段 给前端,代码也更健壮。
-
在 Spring Boot 中,如果你有一个 Mono<String>(来自 WebFlux 或 Reactor),并且需要从中提取字符串并返回,可以使用以下几种方式:1. 直接返回 Mono<String>(推荐)Spring WebFlux 支持直接返回 Mono 或 Flux,框架会自动订阅并处理响应:import reactor.core.publisher.Mono; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MyController { @GetMapping("/example") public Mono<String> getExample() { Mono<String> monoString = Mono.just("Hello, WebFlux!"); return monoString; // 直接返回 Mono,Spring 会自动处理 } } 访问 /example 会直接返回 Hello, WebFlux!。2. 阻塞等待(不推荐,仅用于测试)如果 必须 获取 Mono 中的值(例如在非 Reactor 上下文中),可以使用 block(),但 会阻塞线程,违反 Reactor 的非阻塞原则:@GetMapping("/block-example") public String getBlockExample() { Mono<String> monoString = Mono.just("Hello, Blocking!"); return monoString.block(); // 阻塞等待 Mono 完成(不推荐在生产代码中使用) } ⚠️ 警告:block() 会强制同步等待 Mono 完成,可能影响性能,仅适用于测试或命令行工具。3. 使用 subscribe()(不推荐)如果只是想触发 Mono 的执行(例如记录日志),可以 subscribe(),但 不会返回结果:@GetMapping("/subscribe-example") public void getSubscribeExample() { Mono<String> monoString = Mono.just("Hello, Subscribe!"); monoString.subscribe(result -> System.out.println("Received: " + result)); // 注意:这里没有返回值,适用于异步处理 } 适用场景:日志记录、异步通知等,但 不能用于 HTTP 响应。4. 结合 flatMap 或 map 处理如果需要对 Mono 中的字符串进行处理后再返回:@GetMapping("/process-example") public Mono<String> getProcessExample() { Mono<String> monoString = Mono.just("Hello, WebFlux!"); return monoString.map(s -> s.toUpperCase()); // 转换为大写后返回 } 访问 /process-example 会返回 HELLO, WEBFLUX!。5. 从 Mono<String> 提取值并返回给前端如果 Mono<String> 是某个异步操作的结果(例如从数据库或远程服务获取),可以直接返回 Mono,Spring 会自动处理:@GetMapping("/async-example") public Mono<String> getAsyncExample() { return someAsyncService.fetchData(); // 返回 Mono<String> } 总结方法适用场景是否阻塞推荐程度直接返回 Mono<String>Spring WebFlux 控制器❌ 非阻塞✅✅✅ 推荐block()测试、命令行工具✅ 阻塞❌ 不推荐subscribe()异步处理(如日志)❌ 非阻塞⚠️ 仅特殊场景map/flatMap处理 Mono 数据❌ 非阻塞✅ 推荐最佳实践在 WebFlux 控制器中,直接返回 Mono<String>,让 Spring 处理订阅和响应。避免 block(),除非在测试或非响应式上下文中。使用 map/flatMap 处理 Mono 中的数据。这样既能保持响应式编程的优势,又能正确返回数据给前端。
-
Java服务使用了读写分离地址,代码中使用事务注解,会强制走主库吗?
-
炎热的天气不能影响学习技术的热情,7月份干货合集来了 ,希望可以帮到大家。1.详解MySQL中乐观锁与悲观锁的实现机制及应用场景https://bbs.huaweicloud.com/forum/thread-0232188895402649026-1-1.html2.C++寻位映射的究极密码:哈希扩展-转载https://bbs.huaweicloud.com/forum/thread-0288188731723925024-1-1.html3.【初阶数据结构】双向链表-转载https://bbs.huaweicloud.com/forum/thread-0278188731637851025-1-1.html4. 通俗易懂->哈希表详解-转载https://bbs.huaweicloud.com/forum/thread-02127188731515830029-1-1.html5.数组去重性能优化:为什么Set和Object哈希表的效率最高 -转载https://bbs.huaweicloud.com/forum/thread-0228188731443765029-1-1.html6.【数据结构】时间复杂度和空间复杂度-转载https://bbs.huaweicloud.com/forum/thread-0232188731388663024-1-1.html7.【HarmonyOS Next之旅】DevEco Studio使用指南(三十五) -> 配置构建(二)-转载https://bbs.huaweicloud.com/forum/thread-0278188730852978024-1-1.html8.在【k8s】中部署Jenkins的实践指南-转载https://bbs.huaweicloud.com/forum/thread-0255188730734591030-1-1.html9.Linux下CUDA安装全攻略 -转载https://bbs.huaweicloud.com/forum/thread-0228188730644737028-1-1.html10.Spring Boot 中的默认异常处理机制及执行流程【转载】https://bbs.huaweicloud.com/forum/thread-0228188730601126027-1-1.html11.MyBatis-Plus 自动赋值实体字段最佳实践指南【转载】https://bbs.huaweicloud.com/forum/thread-0278188730544024023-1-1.html12.【Linux】网络基础-转载https://bbs.huaweicloud.com/forum/thread-02127188730499410027-1-1.html13.一文详解php、jsp、asp和aspx的区别(小科普)【转载】https://bbs.huaweicloud.com/forum/thread-0278188730481700022-1-1.html14.如何使用Ajax完成与后台服务器的数据交互详解【转载】https://bbs.huaweicloud.com/forum/thread-0201188730427989026-1-1.html15.【Linux指南】Linux系统 -权限全面解析 -转载https://bbs.huaweicloud.com/forum/thread-0223188730394113017-1-1.html
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签