• [行业前沿] 【分享交流】现在为啥还是有很多公司使用spring boot 而没用spring cloud
    现在为啥还是有很多公司使用spring boot 而没用spring cloud
  • [技术干货] spring boot处理定时任务
    Spring Boot 提供了多种方式来处理定时任务,以下是主要的实现方法:1. 使用 @Scheduled 注解这是最简单的方式,适用于简单的定时任务。基本配置启用定时任务:@SpringBootApplication @EnableScheduling // 启用定时任务 public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } 创建定时任务:import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class ScheduledTasks { // 每5秒执行一次 @Scheduled(fixedRate = 5000) public void taskWithFixedRate() { System.out.println("Fixed rate task - " + System.currentTimeMillis() / 1000); } // 固定延迟执行(上一次执行完成后5秒再执行) @Scheduled(fixedDelay = 5000) public void taskWithFixedDelay() throws InterruptedException { Thread.sleep(1000); // 模拟任务执行时间 System.out.println("Fixed delay task - " + System.currentTimeMillis() / 1000); } // 使用Cron表达式(每分钟的第30秒执行) @Scheduled(cron = "30 * * * * ?") public void taskWithCronExpression() { System.out.println("Cron task - " + System.currentTimeMillis() / 1000); } } @Scheduled 参数说明fixedRate:固定速率执行,从任务开始时间计算间隔fixedDelay:固定延迟执行,从任务结束时间计算间隔initialDelay:初始延迟(与fixedRate或fixedDelay配合使用)cron:使用Cron表达式定义执行时间2. 使用 TaskScheduler 接口提供更灵活的编程方式:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.time.Instant; @Component public class DynamicScheduledTasks { @Autowired private TaskScheduler taskScheduler; @PostConstruct public void scheduleTasks() { // 动态添加定时任务 taskScheduler.schedule( () -> System.out.println("Dynamic task - " + Instant.now()), new CronTrigger("0/10 * * * * ?") // 每10秒执行一次 ); } } 3. 使用 Quartz 框架对于更复杂的调度需求,可以集成 Quartz:添加依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> 配置 Quartzimport org.quartz.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class QuartzConfig { @Bean public JobDetail sampleJobDetail() { return JobBuilder.newJob(SampleJob.class) .withIdentity("sampleJob") .storeDurably() .build(); } @Bean public Trigger sampleJobTrigger() { SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(10) .repeatForever(); return TriggerBuilder.newTrigger() .forJob(sampleJobDetail()) .withIdentity("sampleTrigger") .withSchedule(scheduleBuilder) .build(); } } 创建 Job 类import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; public class SampleJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("Quartz job executed - " + System.currentTimeMillis() / 1000); } } 4. 动态管理定时任务如果需要运行时动态管理定时任务:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicBoolean; @Component public class DynamicScheduledTask implements SchedulingConfigurer { @Autowired private SomeService someService; private final AtomicBoolean enabled = new AtomicBoolean(true); @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.addTriggerTask( // 任务内容 () -> { if (enabled.get()) { someService.doSomething(); } }, // 触发器 triggerContext -> { String cron = enabled.get() ? "0/5 * * * * ?" : "0 0 0 1 1 ?"; // 禁用时设为无效表达式 return new CronTrigger(cron).nextExecutionTime(triggerContext); } ); } public void setEnabled(boolean enabled) { this.enabled.set(enabled); } } 5. 分布式定时任务在集群环境中,可以使用以下方案避免重复执行:ShedLock:轻量级分布式锁<dependency> <groupId>net.javacrumbs.shedlock</groupId> <artifactId>shedlock-spring</artifactId> <version>4.42.0</version> </dependency> <dependency> <groupId>net.javacrumbs.shedlock</groupId> <artifactId>shedlock-provider-jdbc-template</artifactId> <version>4.42.0</version> </dependency> @Scheduled(cron = "0 0/15 * * * *") @SchedulerLock(name = "myScheduledTask", lockAtMostFor = "14m", lockAtLeastFor = "14m") public void myScheduledTask() { // 任务内容 } Quartz + JDBC:使用数据库存储调度信息最佳实践对于简单任务,优先使用 @Scheduled需要动态调度时使用 TaskScheduler复杂调度需求使用 Quartz集群环境考虑分布式锁或Quartz集群模式避免长时间运行的任务阻塞调度线程(使用 @Async 异步执行)合理设置异常处理,避免任务因异常终止常见问题解决定时任务不执行:检查是否添加了 @EnableScheduling确保定时任务类被Spring管理(有 @Component 等注解)检查是否有未捕获的异常任务执行时间过长影响后续执行:使用 @Async 异步执行考虑调整任务执行频率优化任务代码性能集群环境下重复执行:实现分布式锁使用Quartz集群模式或通过业务逻辑去重
  • [技术干货] 解决spring boot日志太多导致硬盘爆满的问题
    当Spring Boot应用产生过多日志导致服务器硬盘空间不足时,可以采取以下几种解决方案:1. 配置日志轮转(Log Rotation)使用Logback或Log4j2的日志轮转功能,自动清理旧日志文件:Logback配置示例(logback-spring.xml)<configuration> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>/var/log/myapp/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 每日轮转 --> <fileNamePattern>/var/log/myapp/app.%d{yyyy-MM-dd}.log</fileNamePattern> <!-- 保留30天的日志 --> <maxHistory>30</maxHistory> <!-- 超过1GB时触发轮转(可选) --> <totalSizeCap>1GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="FILE"/> </root> </configuration> Log4j2配置示例(log4j2.xml)<Configuration status="WARN"> <Appenders> <RollingFile name="RollingFile" fileName="/var/log/myapp/app.log" filePattern="/var/log/myapp/app-%d{yyyy-MM-dd}-%i.log"> <PatternLayout> <Pattern>%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</Pattern> </PatternLayout> <Policies> <!-- 每日轮转 --> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> <!-- 每100MB轮转一次 --> <SizeBasedTriggeringPolicy size="100 MB"/> </Policies> <!-- 保留30天的日志 --> <DefaultRolloverStrategy max="30"/> </RollingFile> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="RollingFile"/> </Root> </Loggers> </Configuration> 2. 调整日志级别减少不必要的日志输出:# application.properties logging.level.root=WARN logging.level.com.yourpackage=INFO 3. 使用异步日志记录减少日志记录对应用性能的影响:Logback异步配置<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <queueSize>512</queueSize> <appender-ref ref="FILE" /> </appender> Log4j2异步配置<Configuration status="WARN"> <Appenders> <RollingFile name="RollingFile" ...> <!-- 同上 --> </RollingFile> <Async name="Async" bufferSize="1024"> <AppenderRef ref="RollingFile"/> </Async> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="Async"/> </Root> </Loggers> </Configuration> 4. 监控和清理旧日志设置cron任务定期清理旧日志:# 每天清理30天前的日志 0 0 * * * find /var/log/myapp/ -name "*.log" -mtime +30 -exec rm {} \; 使用logrotate工具(Linux系统自带):/var/log/myapp/*.log { daily missingok rotate 30 compress delaycompress notifempty create 644 root root }5. 其他优化建议分离日志目录:将日志存储在单独的磁盘分区使用日志收集系统:如ELK(Elasticsearch+Logstash+Kibana)或Fluentd限制单个日志文件大小:在轮转配置中设置maxFileSize监控磁盘空间:设置告警,在空间不足时及时处理考虑日志重要性:评估是否需要记录所有DEBUG级别日志最佳实践组合推荐组合使用以下方案:配置日志轮转(保留7-30天日志)调整日志级别为INFO或WARN设置定期清理旧日志的cron任务监控应用日志输出量,优化不必要的日志这样可以有效控制日志增长,同时保留足够的日志用于问题排查。
  • [技术干货] xxl-job报错 a different jdbcTypeForNull confiquration property
    这个错误是 XXL-Job 在执行 MyBatis SQL 操作时出现的参数绑定问题,具体原因是 MyBatis 无法处理 null 参数的 JDBC 类型,导致 SQLException: invalid column type: 1111(1111 是 JdbcType.OTHER 的内部编码)。错误分析核心问题:MyBatis 在执行 SQL 时,某个参数(glueSource)被设置为 null,但没有明确指定 jdbcType。默认情况下,MyBatis 会尝试推断 null 的 JDBC 类型,但某些数据库驱动(如 PostgreSQL、Oracle)对 null 的类型要求严格,导致 invalid column type 错误。关键错误信息:Could not set parameters for mapping: ParameterMapping{property='glueSource', ...} Error setting null for parameter #15 with JdbcType OTHER SQLException: invalid column type: 1111 glueSource 参数是 String 类型,但传入的是 null。MyBatis 默认使用 JdbcType.OTHER 处理 null,但数据库驱动不支持。解决方案方法 1:修改 MyBatis 配置,全局设置 jdbcTypeForNull在 application.properties 或 application.yml 中配置:# application.properties mybatis.configuration.jdbc-type-for-null=NULL 或# application.yml mybatis: configuration: jdbc-type-for-null: NULL 这样 MyBatis 会将 null 显式设置为 JdbcType.NULL,而不是 OTHER。方法 2:在 Mapper XML 中显式指定 jdbcType如果 glueSource 是 XXL-Job 的某个 SQL 参数(比如 XxlJobInfo 或 XxlJobGlue 表的字段),在对应的 Mapper XML 文件中修改:<!-- 示例:修改 glueSource 参数的 jdbcType --> <insert id="save" parameterType="XxlJobInfo"> INSERT INTO xxl_job_info (..., glue_source, ...) VALUES (..., #{glueSource,jdbcType=VARCHAR}, ...) </insert> 关键点:显式指定 jdbcType=VARCHAR(或其他合适的类型,如 CLOB)。方法 3:检查数据库表结构确认 xxl_job_info 或 xxl_job_glue 表中 glue_source 字段的类型:如果是 TEXT、CLOB 或 VARCHAR,确保 MyBatis 映射的 javaType 和 jdbcType 匹配。如果字段允许 NULL,但代码传入了 null,需按 方法 1 或 2 处理。示例表结构(MySQL):CREATE TABLE xxl_job_info ( ... glue_source VARCHAR(2000) NULL COMMENT 'GLUE脚本内容', ... ); 方法 4:检查 XXL-Job 版本如果是旧版 XXL-Job,可能存在 MyBatis 映射配置问题,尝试升级到最新稳定版。根本原因MyBatis 默认对 null 参数使用 JdbcType.OTHER,但某些数据库驱动(如 PostgreSQL)不支持该类型。XXL-Job 的某些 SQL 操作(如保存或更新 Job 信息时)未正确处理 glueSource 参数的 null 值。总结推荐优先使用方法 1(全局配置 jdbcTypeForNull),简单高效。如果问题仅出现在特定 SQL,使用方法 2(显式指定 jdbcType)。检查数据库表结构和 XXL-Job 版本,确保兼容性。修改后重启服务,问题应该能解决。如果仍有异常,可以检查 XXL-Job 的日志,定位具体的 SQL 语句。
  • [技术干货] xxl-job 启动报错 javax.net.ssl.SSLHandshakeException
    xxl-job报错如下2025-07-25 09:21:06 INFO 13764 --- [ c.x.j.a.core.thread.JobScheduleHelper:52 ] : >>>>>>>>> init xxl-job admin scheduler success. 2025-07-25 09:21:17 INFO 13764 --- [ com.alibaba.druid.pool.DruidDataSource:998 ] : {dataSource-1} inited 2025-07-25 09:40:14 ERROR 13764 --- [ c.x.j.a.core.alarm.impl.EmailJobAlarm:75 ] : >>>>>>>>>>> xxl-job, job fail alarm email send error, JobLogId:27668541 org.springframework.mail.MailSendException: Mail server connection failed; nested exception is javax.mail.MessagingException: Could not convert socket to TLS; nested exception is: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target. Failed messages: javax.mail.MessagingException: Could not convert socket to TLS; nested exception is: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:448) at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:361) at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:356) at com.xxl.job.admin.core.alarm.impl.EmailJobAlarm.doAlarm(EmailJobAlarm.java:73) at com.xxl.job.admin.core.alarm.JobAlarmer.alarm(JobAlarmer.java:52) at com.xxl.job.admin.core.thread.JobFailMonitorHelper$1.run(JobFailMonitorHelper.java:64) at java.lang.Thread.run(Thread.java:745) Caused by: javax.mail.MessagingException: Could not convert socket to TLS at com.sun.mail.smtp.SMTPTransport.startTLS(SMTPTransport.java:2140) at com.sun.mail.smtp.SMTPTransport.protocolConnect(SMTPTransport.java:734) at javax.mail.Service.connect(Service.java:342) at org.springframework.mail.javamail.JavaMailSenderImpl.connectTransport(JavaMailSenderImpl.java:518) at org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:437) ... 6 common frames omitted Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1949) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:302) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:296) at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1509) at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:216) at sun.security.ssl.Handshaker.processLoop(Handshaker.java:979) at sun.security.ssl.Handshaker.process_record(Handshaker.java:914) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1062) at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387) at com.sun.mail.util.SocketFetcher.configureSSLSocket(SocketFetcher.java:602) at com.sun.mail.util.SocketFetcher.startTLS(SocketFetcher.java:529) at com.sun.mail.smtp.SMTPTransport.startTLS(SMTPTransport.java:2135) ... 10 common frames omitted Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:387) at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292) at sun.security.validator.Validator.validate(Validator.java:260) at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324) at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:229) at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:124) at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1491) ... 20 common frames omitted Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:146) at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:131) at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:280) at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:382) ... 26 common frames omitted 2025-07-25 09:46:17 ERROR 13764 --- [ c.x.j.a.c.resolver.WebExceptionResolver:32 ] : WebExceptionResolver:{} org.apache.catalina.connector.ClientAbortException: java.io.IOException: 你的主机中的软件中止了一个已建立的连接。 at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:309) at org.apache.catalina.connector.OutputBuffer.flush(OutputBuffer.java:272) at org.apache.catalina.connector.CoyoteOutputStream.flush(CoyoteOutputStream.java:118) at java.io.FilterOutputStream.flush(FilterOutputStream.java:140) at com.fasterxml.jackson.core.json.UTF8JsonGenerator.flush(UTF8JsonGenerator.java:1178) at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:1008) at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:345) at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:104) at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:277) at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:181) at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:78) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:124) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1060) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:962) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:888) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:745) Caused by: java.io.IOException: 你的主机中的软件中止了一个已建立的连接。 at sun.nio.ch.SocketDispatcher.write0(Native Method) at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:51) at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93) at sun.nio.ch.IOUtil.write(IOUtil.java:65) at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:471) at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:138) at org.apache.tomcat.util.net.NioBlockingSelector.write(NioBlockingSelector.java:101) at org.apache.tomcat.util.net.NioSelectorPool.write(NioSelectorPool.java:152) at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1253) at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:764) at org.apache.tomcat.util.net.SocketWrapperBase.flushBlocking(SocketWrapperBase.java:717) at org.apache.tomcat.util.net.SocketWrapperBase.flush(SocketWrapperBase.java:707) at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.flush(Http11OutputBuffer.java:572) at org.apache.coyote.http11.filters.ChunkedOutputFilter.flush(ChunkedOutputFilter.java:157) at org.apache.coyote.http11.Http11OutputBuffer.flush(Http11OutputBuffer.java:220) at org.apache.coyote.http11.Http11Processor.flush(Http11Processor.java:1195) at org.apache.coyote.AbstractProcessor.action(AbstractProcessor.java:402) at org.apache.coyote.Response.action(Response.java:209) at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:305) ... 58 common frames omitted 2025-07-25 09:46:17 ERROR 13764 --- [ o.a.c.c.C.[.[.[.[dispatcherServlet]:175 ] : Servlet.service() for servlet [dispatcherServlet] in context with path [/xxl-job-admin] threw exception [Request processing failed; nested exception is java.lang.IllegalStateException: getOutputStream() has already been called for this response] with root cause java.lang.IllegalStateException: getOutputStream() has already been called for this response at org.apache.catalina.connector.Response.getWriter(Response.java:584) at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:227) at com.xxl.job.admin.controller.resolver.WebExceptionResolver.resolveException(WebExceptionResolver.java:53) at org.springframework.web.servlet.DispatcherServlet.processHandlerException(DispatcherServlet.java:1320) at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1131) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1077) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:962) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:888) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1597) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:745) 从日志来看,XXL-JOB 启动后遇到了两个主要问题:1. 邮件告警失败(SSL证书问题)javax.net.ssl.SSLHandshakeException: PKIX path building failed: unable to find valid certification path to requested target原因:系统无法验证邮件服务器(SMTP)的SSL证书,通常是因为:邮件服务器使用自签名证书缺少中间证书Java的cacerts信任库未包含该证书解决方案:临时方案(不推荐生产环境):// 在邮件配置中禁用证书验证(仅测试用) props.put("mail.smtp.ssl.trust", "*"); props.put("mail.smtp.ssl.checkserveridentity", "false"); 推荐方案:导出邮件服务器的证书:openssl s_client -connect smtp.yourserver.com:465 -showcerts </dev/null | openssl x509 -outform PEM > smtp.pem导入到Java信任库:keytool -import -alias smtp -keystore $JAVA_HOME/lib/security/cacerts -file smtp.pem(默认密码:changeit)检查邮件配置:确保application.properties/application.yml中的SMTP配置正确:spring.mail.host=smtp.yourserver.com spring.mail.port=465 spring.mail.username=your@email.com spring.mail.password=yourpassword spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true spring.mail.properties.mail.smtp.ssl.enable=true 2. 客户端连接中断异常ClientAbortException: java.io.IOException: 你的主机中的软件中止了一个已建立的连接可能原因:客户端(如浏览器/调用方)主动断开连接网络不稳定响应处理冲突(见下文的IllegalStateException)3. 响应流冲突(次要问题)IllegalStateException: getOutputStream() has already been called for this response原因:在同一个请求中混合使用了getWriter()和getOutputStream()可能是异常处理逻辑中重复输出了响应解决方案:检查WebExceptionResolver.java第53行附近的代码,确保异常处理时没有重复设置响应输出流。完整修复建议:优先解决邮件证书问题(这是导致告警失败的根本原因)检查网络稳定性,特别是客户端连接中断的情况修复响应流冲突问题(检查自定义异常处理器)如果不需要邮件告警,可以暂时关闭相关功能:xxl.job.alarm.email=false 补充说明:日志显示Druid数据源初始化成功,说明数据库连接正常调度器初始化成功(init xxl-job admin scheduler success)这些问题不影响核心调度功能,但会影响告警通知和部分HTTP响应建议先解决SSL证书问题,这是当前最关键的阻塞性问题。
  • [技术干货] Spring Boot 中的默认异常处理机制及执行流程【转载】
    Spring Boot 异常处理机制详解Spring Boot 提供了一套完善的默认异常处理机制,通过内置的 BasicErrorController 自动处理应用中的各类异常情况。这套机制基于 Spring MVC 的异常处理框架构建,具有以下特点:多格式响应支持:对于传统Web应用:当发生异常时,会自动返回错误页面(如 404.html、5xx.html)对于REST API:会自动生成结构化的JSON错误响应,包含timestamp、status、error、path等字段默认错误路径:提供了/error映射路径作为统一的错误处理入口可以通过实现ErrorController接口来自定义错误处理逻辑错误页面配置:静态错误页:支持在src/main/resources/static/error/目录下放置静态错误页模板错误页:支持在模板引擎目录(如templates/error/)下放置动态错误页支持根据状态码命名文件(如404.html、500.html)内置异常转换:自动将常见异常转换为合适的HTTP状态码例如:MethodArgumentNotValidException -> 400 Bad Request例如:NoHandlerFoundException -> 404 Not Found配置选项:通过server.error.*配置项可以自定义错误处理行为例如:server.error.include-message=always(控制错误信息是否包含详细异常信息)实际应用示例:1234567891011121314151617// 自定义错误页@Controllerpublic class MyErrorController implements ErrorController {    @RequestMapping("/error")    public String handleError(HttpServletRequest request) {        Object status = request.getAttribute("javax.servlet.error.status_code");        if (status != null) {            Integer statusCode = Integer.valueOf(status.toString());            if(statusCode == 404) {                return "error/404";            } else if(statusCode == 500) {                return "error/500";            }        }        return "error/generic";    }}这套机制既适用于传统Web应用的页面错误展示,也适配RESTful API的JSON错误响应,为开发者提供了开箱即用的异常处理解决方案。默认错误页面功能当应用出现异常时,Spring Boot 会自动展示一个"Whitelabel Error Page"(白色标签错误页),这个页面包含以下关键信息:HTTP状态码(如404、500等)错误发生的时间戳(精确到毫秒)具体的错误信息(异常消息)请求的URL路径错误跟踪ID(便于日志关联)例如:访问不存在的URL时,会返回一个包含"404 Not Found"状态的错误页面服务器内部错误时会显示500错误页面,并附带相关错误信息参数验证失败时会返回400错误页面自动异常转换机制Spring Boot 会自动将常见的异常类型转换为合适的HTTP状态码:404 Not Found触发条件:NoHandlerFoundException使用场景:当请求的URL没有对应的控制器方法时示例:访问 /api/non-existent-endpoint400 Bad Request触发条件:MethodArgumentNotValidException(方法参数验证失败)使用场景:表单验证失败、REST API参数校验不通过示例:提交的JSON数据缺少必填字段500 Internal Server Error触发条件:其他所有未捕获的异常使用场景:业务逻辑中的运行时异常示例:数据库连接失败、空指针异常错误属性配置选项开发者可以通过 application.properties 或 application.yml 文件自定义错误处理行为:12345678# 控制错误信息中是否包含异常消息server.error.include-message=always # 可选值:always, on_param, never# 控制是否包含堆栈跟踪信息server.error.include-stacktrace=on_param # 可选值:always, on_param, never# 自定义错误处理路径(默认为/error)server.error.path=/custom-error# 是否包含错误详情(绑定异常的具体字段错误)server.error.include-binding-errors=always默认错误处理流程详解异常触发阶段当应用代码中抛出异常且未被捕获时异常被Spring MVC的 DispatcherServlet 捕获请求转发阶段DispatcherServlet 将异常转发到配置的错误路径(默认是/error)根据请求的Accept头决定响应格式(HTML或JSON)错误处理阶段BasicErrorController 处理该请求收集错误信息(状态码、错误消息、时间戳等)对于浏览器请求(Accept包含text/html),返回HTML错误页对于API请求(Accept包含application/json),返回JSON格式的错误信息响应生成阶段 JSON响应示例:123456{  "timestamp": "2023-05-15T08:12:34.567+00:00",  "status": 404,  "error": "Not Found",  "path": "/api/non-existent"}扩展机制虽然Spring Boot提供了默认处理,但开发者可以通过以下方式扩展:自定义ErrorController实现ErrorController接口重写getErrorPath()和error()方法示例:记录错误日志或发送告警通知全局异常处理器使用@ControllerAdvice注解定义全局异常处理类配合@ExceptionHandler处理特定异常示例:12345678@ControllerAdvicepublic class GlobalExceptionHandler {    @ExceptionHandler(value = {UserNotFoundException.class})    protected ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {        ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", ex.getMessage());        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);    }}继承ResponseEntityExceptionHandler继承Spring提供的基类进行更细粒度的控制可以覆盖处理特定MVC异常的默认行为示例:自定义验证错误的响应格式
  • [技术干货] Spring Boot 全面解析:简化Java企业级开发的利器
    一、Spring Boot 概述Spring Boot是由Pivotal团队在2013年开始研发的一个开源框架,旨在简化Spring应用的初始搭建和开发过程。它通过"约定优于配置"的理念,让开发者能够快速创建独立运行、生产级别的基于Spring的应用程序。Spring Boot的核心优势:快速启动:简化项目初始化,减少配置工作自动配置:智能判断并自动配置所需的Spring功能内嵌服务器:默认集成Tomcat、Jetty或Undertow生产就绪:提供健康检查、指标监控等生产级特性丰富的starter:简化依赖管理,实现"开箱即用"二、Spring Boot 核心特性详解1. 自动配置(Auto-Configuration)Spring Boot通过分析项目的classpath和已定义的bean,自动配置Spring应用程序。例如:当检测到H2数据库在classpath中时,自动配置内存数据库当存在Spring MVC依赖时,自动配置DispatcherServlet根据application.properties/yaml中的配置自动调整行为2. Starter依赖Spring Boot提供了一系列"starter"模块,将相关依赖组合在一起:Starter功能描述spring-boot-starter-web构建Web应用,包含Spring MVC和Tomcatspring-boot-starter-data-jpaSpring Data JPA与Hibernatespring-boot-starter-securitySpring Security支持spring-boot-starter-test测试支持(JUnit, Mockito等)spring-boot-starter-actuator生产监控和管理端点3. 外部化配置Spring Boot支持多种配置方式,优先级从高到低:命令行参数JNDI属性Java系统属性(System.getProperties())操作系统环境变量应用配置文件(application-{profile}.properties/yaml)application.yml示例:yaml server: port: 8081 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/mydb username: root password: secret jpa: hibernate: ddl-auto: update show-sql: true三、快速构建Spring Boot应用1. 项目初始化使用Spring Initializr:通过IDE(如IntelliJ IDEA)创建项目,选择所需依赖。或使用命令行:bash curl https://start.spring.io/starter.tgz -d dependencies=web,jpa \-d type=maven-project -d baseDir=myapp | tar -xzvf -2. 基础项目结构text myapp/├── src/│ ├── main/│ │ ├── java/│ │ │ └── com/example/myapp/│ │ │ ├── MyappApplication.java # 主启动类│ │ │ ├── controller/ # 控制器层│ │ │ ├── service/ # 业务逻辑层│ │ │ ├── repository/ # 数据访问层│ │ │ └── model/ # 实体类│ │ └── resources/│ │ ├── static/ # 静态资源│ │ ├── templates/ # 模板文件│ │ ├── application.yml # 配置文件│ │ └── application-dev.yml # 开发环境配置│ └── test/ # 测试代码└── pom.xml # Maven配置3. 编写第一个REST接口java @RestController@RequestMapping("/api/hello")public class HelloController { @GetMapping public String sayHello(@RequestParam(defaultValue = "World") String name) { return "Hello, " + name + "!"; } @GetMapping("/{id}") public ResponseEntity<HelloResponse> getHello(@PathVariable Long id) { HelloResponse response = new HelloResponse(id, "Hello Message"); return ResponseEntity.ok(response); }}// DTO类public record HelloResponse(Long id, String message) {}四、Spring Boot高级特性1. 数据访问Spring Data JPA示例:java @Entitypublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String email; // getters/setters...}public interface UserRepository extends JpaRepository<User, Long> { List<User> findByUsernameContaining(String username);}@Servicepublic class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public List<User> searchUsers(String keyword) { return userRepository.findByUsernameContaining(keyword); }}2. 事务管理java @Service@Transactionalpublic class OrderService { private final OrderRepository orderRepository; private final PaymentService paymentService; public OrderService(OrderRepository orderRepository, PaymentService paymentService) { this.orderRepository = orderRepository; this.paymentService = paymentService; } public Order createOrder(OrderRequest request) { Order order = new Order(request); order = orderRepository.save(order); paymentService.processPayment(order); return order; }}3. 异常处理java @ControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponse> handleNotFound( ResourceNotFoundException ex) { ErrorResponse response = new ErrorResponse( "NOT_FOUND", ex.getMessage(), LocalDateTime.now() ); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationErrors( MethodArgumentNotValidException ex) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(FieldError::getDefaultMessage) .collect(Collectors.toList()); ErrorResponse response = new ErrorResponse( "VALIDATION_ERROR", "Validation failed", LocalDateTime.now(), errors ); return ResponseEntity.badRequest().body(response); }}五、Spring Boot生产就绪特性1. Actuator端点yaml management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always metrics: enabled: true常用端点:/actuator/health - 应用健康状态/actuator/info - 应用信息/actuator/metrics - 性能指标/actuator/env - 环境变量/actuator/loggers - 日志配置2. 监控集成与Prometheus集成:xml <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId></dependency>配置:yaml management: metrics: export: prometheus: enabled: true3. 性能优化建议JVM调优:bash java -Xms512m -Xmx1024m -jar myapp.jar连接池配置:yaml spring: datasource: hikari: maximum-pool-size: 20 connection-timeout: 30000缓存配置:java @Configuration@EnableCachingpublic class CacheConfig { @Bean public CacheManager cacheManager() { return new ConcurrentMapCacheManager("products"); }}@Servicepublic class ProductService { @Cacheable("products") public Product getProductById(Long id) { // 数据库查询... }}六、Spring Boot 3.0新特性基于Java 17+:要求最低Java 17版本GraalVM原生镜像支持:bash ./mvnw spring-boot:build-image改进的观察能力:与Micrometer深度集成记录API改进:使用新的Log4j2 API更强大的自动配置:更智能的条件配置
  • [技术干货] Spring Boot 3.5 正式发布,王炸级更新!!
    就在前几天,Spring Boot 3.5.0 正式发布了:最新的支持版本如下:3.2.x 在前几天也停止维护了,很神奇的是,3.1.x 及以下的停更版本居然也发布 bug 更新包了。从路线图可以看到每个版本的终止时间,每个版本的生命周期只有一年,3.3.x 以下的版本停止免费维护了,3.3.x 还有一个月也要停止维护了,商业支持的 3.x 最高版本版本 3.1.x、2.x 为 2.7.x。Spring Boot 进入了全新的 3.4+ 时代了,2.6.x 以下的版本彻底退出历史舞台,4.0 都快要来了,技术变革太快了。。Spring Boot 3.5.0 新特性1、最低环境要求Spring Boot 3.0.0 开始支持并最低要求 Java 17,目前的新版本也陆续开始支持 Java 21 ~ 24 了。对 Java 开发环境的要求对比表:Spring BootJDKSpringMavenGradle3.5.017 ~ 246.2.7+3.6.3+7.6.4+,8.4+3.4.016 ~ 236.2.0+3.6.3+7.6.4+,8.4+3.3.017 ~ 226.1.8+3.6.3+7.5+,8.x3.2.017 ~ 216.1.1+3.6.3+7.5+,8.x3.1.017 ~ 206.0.9+3.6.3+7.5+,8.x3.0.017 ~ 196.0.2+3.5+7.5+2.7.188 ~ 215.3.31+3.5+6.8.x, 6.9.x, 7.x, 8.x支持 Java 8 的最后一个 Spring Boot 2.x 系列版本早已已经退伍啦,Java 17 的新时代彻底到来。如果你还停留在 Java 8 就 OUT 了,过去一两年,Java 8 采用率腰斩,Java 17 暴涨 430%!!。2、结构化日志记录改进在《Spring Boot 3.4 版本》中出的新功能结构化日志,Spring Boot 3.5 又对它进行改进了:支持自定义结构化日志堆栈跟踪,可以自定义写入结构化日志的堆栈跟踪,以限制其大小或以不同的格式进行打印。可以使用 logging.structured.json.stacktrace.* 属性配置堆栈跟踪输出。ECS 结构日志的 JSON 输出已更新为使用嵌套格式,这将提高与使用 JSON 的后端之间的兼容性。ECS 结构化日志格式,现在在 tags 字段中添加了 Logback 和 Log4j 的标记。3、服务连接支持 SSLSpring Boot 3.5 已经支持为选定的服务连接添加客户端 SSL 支持,以下服务连接支持此功能:CassandraCouchbaseElasticsearchKafkaMongoDBRabbitMQRedis另外,Testcontainers 和 Docker Compose 集成也进行了更新,也允许进行 SSL 配置:对于 Testcontainers,可以使用新的注解;对于 Docker Compose,可以使用标签。4、从环境变量加载属性虽然之前的版本已经可以从环境变量加载单个属性,但现在,Spring Boot 3.5 开始,可以从单个环境变量加载多个属性。比如,现在可以定义一个多行环境变量 USER_CONFIGURATION ,可以包含以下内容:user.name=Johnuser.age=18user.sex=1...然后可以通过使用 env: 前缀导入:spring.config.import=env:USER_CONFIGURATION在 Environment 中,就可以找到 user.name 、user.age 等其他多个属性了,也就是说,当有多个属性值的时候,可以不用配置多个环境变量了,一个环境变量搞定。5、AsyncTaskExecutor 与自定义 ExecutorAsyncTaskExecutor 是 Spring 框架提供的异步任务执行器接口,用于执行异步方法或任务。它是 Spring 对 Java 原生 Executor 的增强,提供了更高层次的封装,常用于异步方法调用(如 @Async)或手动提交任务。如果自定义的 Executor 的 Bean 存在,Spring Boot 3.5 现在可以自动配置 AsyncTaskExecutor,只需要将 spring.task. execute.mode 属性的值设置为 force。在这种模式下运行时,它确保所有集成(包括常规的 @Async 处理)都使用自动配置的执行器,除非定义了 AsyncConfigurer 相关的 Bean。6、Bean 后台初始化支持自动配置Spring Boot 3.5 现在支持自动配置一个名为 bootstrapExecutor 的 Bean,在还没有的相关 Bean 情况下才自动配置,以支持 Bean 在后台完成初始化。要实现这一点,需要在上下文中有一个名为 applicationTaskExecutor 的 Bean,当然也可以定义自定义 Executor bean,这样就能让 Bean 后台初始化可以开箱即用。7、通过注解注册过滤器和 Servlet之前注册过滤器和 Servlet 普通使用的是 ServletRegistrationBean 和 FilterRegistrationBean 方式,Spring Boot 3.5 祭出了两个注解替代方案:使用 @ServletRegistration 注册 Servlet ;使用 @FilterRegistration 注册 Filter;如下例所示:@Configuration(proxyBeanMethods = false)public class FilterConfiguration { @Bean @FilterRegistration(name = "encoding-filter", urlPatterns = "/*", order = 0) public EncodingFilter encodingFilter() { return new EncodingFilter(); }}此外,对 FilterRegistrationBean 的行为也进行了调整,以前,你可以将一个空的 DispatcherType 集传递给 setDispatcherTypes 方法,然后再将它传递给服务器。现在,它的行为与调用 setDispatcherTypes(null) 作用相同,将其与 FilterRegistration 注解的功能保持一致。8、自动配置的 TaskExecutor 名称以前,Spring Boot 自动配置了一个 TaskExecutor ,其中包含 taskExecutor 和 applicationTaskExecutor Bean 名称。在 Spring Boot 3.5 这个版本中,只提供了 applicationTaskExecutor bean 名称,请求按名称自动配置的 Executor 的代码应该适应使用 applicationTaskExecutor 。如果你的系统依赖这种方式,还想和过去保持兼容,你也可以使用 BeanFactoryPostProcessor 来添加别名,如下面的例子所示:@Configurationpublic class MyConfiguration { @Bean static BeanFactoryPostProcessor taskExecutorAliasBeanFactoryPostProcessor() { return (beanFactory) -> beanFactory.registerAlias("applicationTaskExecutor", "taskExecutor"); }}9、Redis 配置变更当配置 spring.data.redis.url 时,使用的 Redis 数据库由 URL 决定。如果 URL 没有指定数据库,则使用默认值 0 。当配置 spring.data.redis.url 时,现在忽略 spring.data.redis.database 属性,使其行为与主机、端口、用户名和密码属性保持一致。另外,增加使用 spring.data.redis.lettuce.read-from 属性配置 ReadFrom 的支持。10、Prometheus Pushgateway 配置变更将指标推送到 Prometheus Pushgateway 现在需要 io.prometheus:prometheus-metrics-exporter-pushgateway 而不是 io.prometheus:simpleclient_pushgateway 了。另外,新客户端的 Pushgateway 支持也可能需要更改配置,如果使用 management.prometheus.metrics.export.pushgateway.base-url ,需要替换为 management.prometheus.metrics.export.pushgateway.address ,并将值调整为 host:port 的形式。为了支持新的 Pushgateway 客户端,添加了三个新属性:management.prometheus.metrics.export.pushgateway.format:将 format 属性设置为 text 以文本形式推送指标,而不是使用 protobuf。management.prometheus.metrics.export.pushgateway.scheme:将 scheme 属性设置为 https ,支持在推送指标时使用 SSL。management.prometheus.metrics.export.pushgateway.token:设置 token 属性,而不是现有的 username 和 password 属性,以使用基于令牌的身份验证。11、更多1)执行器端点 heapdump 现在默认为 access=NONE,其目的是帮助减少错误配置应用程序泄露敏感信息的可能性。2)属性 .enabled 的支持的值已经收紧了,现在必须是 true 或 false 。3)配置文件命名规则在此版本中得到了加强,配置文件现在只能包含 - (破折号)、 _ (下划线)、字母和数字。此外,配置文件不允许以 - 或 _ 开头或结尾。4)TestRestTemplate 现在和常规的 RestTemplate 一样使用相同的重定向设置,HttpOption.ENABLE_REDIRECTS 选项也已弃用。5)大量 Spring 库和第三方类库都得到了更新,还有一些废除项。总结Spring Boot 3.5 这个版本的变化还挺大的,增加了不少实用功能,很多功能也都得到了增强,做技术的真要时刻保持对新技术的渴望啊,不然都跟不上时代的步伐了。Spring Boot 这波操作,既是顺应大环境对现代 Java 的需求,又在细节上做了许多微创新。从环境支持的 Java 版本不断提升,到结构化日志、服务连接 SSL、注解式 Servlet 和 Filter 注册、Redis 配置调整、Pushgateway 的更新……每一项都体现出了官方对开发体验和生产效率的深度思考。
  • [技术干货] Spring Boot 使用 Tomcat 作为容器时访问根 context-path 302分析
    起因是安全团队反馈了一个漏洞,说通过公网域名访问内网中的一个SpringBoot服务的根路径,原本是域名的url变成了服务的内网的ip。简略版的网络拓扑如下SpringBoot版本:2.2.5.RELEASEserver.servlet.context-path=/demo通过域名访问的url如下:https://domain.cn/demo访问之后url转变为:http://10.x.1/demo/因为网关后面的SpringBoot服务在多个机器部署,转变后的url会变成其中的一台服务器的ip。在nginx机器上看了下日志,发现请求有一个302重定向的过程,然后变成了SpringBoot的404页面,因为根路径没有对应的handler处理。Whitelabel Error PageThis application has no explicit mapping for /error, so you are seeing this as a fallback.Fri Apr 10 23:59:59 CST 2025There was an unexpected error (type=Not Found, status=404).No message available测试环境,在Chrome中打开F12调试模式,在Network下开启Preserve log,通过SpringBoot服务所在机器的ip和端口直接访问服务的根路径,同样有一个302重定向的过程http://10.x.1/demo会转变为http://10.x.1/demo/url在302重定向后,url后面拼接了一个 /怀疑是不是SpringBoot有什么特殊处理,SpringBoot中是通过org.springframework.web.servlet.DispatcherServlet:doDispatch方法来分发处理请求的,本地调试项目发现doDispatch方法会进入两次,DispatcherServlet上面是tomcat容器了,看样子是容器里了,往上回溯调用链发现org.apache.catalina.mapper.Mapper:internalMapWrapper方法里有一段代码有关于重定向的逻辑,代码如下:if(mappingData.wrapper == null && noServletPath && contextVersion.object.getMapperContextRootRedirectEnabled()) { // The path is empty, redirect to "/" path.append('/'); pathEnd = path.getEnd(); mappingData.redirectPath.setChars (path.getBuffer(), pathOffset, pathEnd - pathOffset); path.setEnd(pathEnd - 1); return;}意思就是如果请求的路径是 "" ,那么将重定向到 / ,项目的 server.servlet.context-path=/demo,那么访问根路径,后面不加其它的url,正好就走到了这一个逻辑里面,看代码逻辑是有一个参数可以控制是否重定向的:mapperContextRootRedirectEnabled,这个是org.apache.catalina.core.StandardContext类中的一个配置参数,可以通过配置来修改server.tomcat.redirect-context-root贴一下它的注释:Determines if requests for a web application context root will beredirected (adding a trailing slash) by the Mapper. This is moreefficient but has the side effect of confirming that the context path isvalid.修改参数的值为false,重启项目再次访问,不再出现302,而是直接到了404页面server.tomcat.redirect-context-root=false如果项目的context-path是 / 没有302重定向的过程。有项目使用了undertow作为容器,但是看了下undertow的代码,发现没有提供类似的参数,后面写一篇文章来记录使用undertow作为容器时出现这样情况的解决方案。tomcat作为老牌的容器,相较undertow还是有更多灵活的配置选项。转载自https://www.cnblogs.com/imadc/p/18820909
  • [技术干货] springboot项目打包:你的项目还是打成一个xx.jar包吗?看看如何打成分层包-转载
    1.整合打包 1.pom.xml引入依赖<!-- SpringBoot 打包插件,把 maven-jar-plugin 打成的jar包重新打成可运行jar包 --><plugin>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-maven-plugin</artifactId></plugin>AI写代码java运行 2.执行mvn命令打包或者工具直接打包1.maven常用打包命令:1、mvn clean 将旧的class字节码文件删除。2、mvn pakage 打包,动态web工程打war包,Java工程打jar 包。3、mvn install 将项目生成jar包放在仓库中,然后打包。4、mvn clean install 删除target文件夹 ,然后打包到target。2.maven工具打包maven窗口执行 在项目target文件中找到jar包这样的jar包,包含了依赖包,配置文件在里面,对于项目的部署发布很不友好,如果网络传输慢,需要很长时间发布,更新替换配置文件也需要重新打包,很麻烦。下面是分层打包,对依赖和配置文件和运行jar包分开,部署起来就非常方便快捷啦👇👇👇2.分层打包1.pom.xml 引入依赖1.在 build中引入<build>        <finalName>${output.software.name}-${output.software.version}</finalName>        <resources>            <resource>                <directory>${project.basedir}/lib</directory>                <targetPath>BOOT-INF/lib/</targetPath>                <includes>                    <include>**/*.jar</include>                </includes>            </resource>            <resource>                <directory>src/main/resources</directory>                <filtering>true</filtering>            </resource>        </resources>        <plugins>            <!--支持yaml读取pom的参数-->            <plugin>                <artifactId>maven-resources-plugin</artifactId>                <configuration>                    <encoding>utf-8</encoding>                    <useDefaultDelimiters>true</useDefaultDelimiters>                </configuration>            </plugin>            <!-- 打JAR包 -->            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-jar-plugin</artifactId>                <configuration>                    <!-- 不打包资源文件(配置文件和依赖包分开) -->                    <excludes>                        <exclude>application*.yml</exclude>                    </excludes>                    <archive>                        <manifest>                            <addClasspath>true</addClasspath>                            <!-- MANIFEST.MF 中 Class-Path 加入前缀 -->                            <classpathPrefix>${output.software.libs}/</classpathPrefix>                            <!-- jar包不包含唯一版本标识 -->                            <useUniqueVersions>false</useUniqueVersions>                            <!--指定入口类 -->                            <mainClass>${output.software.mainclass}</mainClass>                        </manifest>                        <manifestEntries>                            <!--MANIFEST.MF 中 Class-Path 加入资源文件目录 -->                            <Class-Path>./${output.software.config}/</Class-Path>                        </manifestEntries>                    </archive>                    <outputDirectory>${project.build.directory}/${output.software.name}</outputDirectory>                </configuration>            </plugin>            <!-- 这个插件是用来复制项目依赖的jar包 -->            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-dependency-plugin</artifactId>                <executions>                    <execution>                        <!-- 自定义 -->                        <id>copy-dependencies</id>                        <phase>package</phase>                        <goals>                            <!-- 复制依赖的jar包 -->                            <goal>copy-dependencies</goal>                        </goals>                        <configuration>                            <!-- 将依赖的jar包复制到该路径下 -->                            <outputDirectory>                                ${project.build.directory}/${output.software.name}/${output.software.libs}                            </outputDirectory>                        </configuration>                    </execution>                </executions>            </plugin>             <!-- 这个插件是用来复制项目的静态资源-->            <plugin>                <artifactId>maven-resources-plugin</artifactId>                <executions>                    <execution>                        <!-- 自定义 -->                        <id>copy-resources</id>                        <phase>package</phase>                        <goals>                            <!-- 复制静态资源 -->                            <goal>copy-resources</goal>                        </goals>                        <configuration>                            <resources>                                <resource>                                    <!-- 指定静态资源的路径 -->                                    <directory>target/classes</directory>                                    <!-- 指定需要复制的文件 -->                                    <includes>                                        <include>application.properties</include>                                        <include>application.yml</include>                                        <include>application-${spring.auto.active}.yml</include>                                    </includes>                                </resource>                            </resources>                            <!-- 指定复制到该目录下 -->                            <outputDirectory>                                ${project.build.directory}/${output.software.name}/${output.software.config}                            </outputDirectory>                        </configuration>                    </execution>                </executions>            </plugin>             <!-- 以上配置后你的文件打包后的文件目录如下                -lib                -config                -项目名.jar             -->            <!-- 这个插件使用来将分离出来的静态资源和依赖的jar包(就是上面说到的文件目录),        压缩成一个zip文件。个人感觉这个蛮方便的 -->            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-assembly-plugin</artifactId>                <configuration>                    <!-- 这个插件需要指定一个配置文件 【重点:这里的assembly.xml 就是讲到分层打包】 -->                    <descriptors>                        <descriptor>src/assembly/assembly.xml</descriptor>                    </descriptors>                </configuration>                <executions>                    <execution>                        <!-- 自定义 -->                        <id>make-assembly</id>                        <phase>package</phase>                        <goals>                            <!-- 只执行一次 -->                            <goal>single</goal>                        </goals>                    </execution>                </executions>            </plugin>        </plugins>    </build>AI写代码XML 2.properties需要引入配置参数<properties>        <!-- 打包输出软件名称 -->        <output.software.name>pet</output.software.name>        <!-- 打包输出依赖包路径 -->        <output.software.libs>lib</output.software.libs>        <!-- 打包输出配置文件路径 -->        <output.software.config>config</output.software.config>        <!-- 打包输出软件版本号 -->        <output.software.version>1.0.0</output.software.version>        <!-- 启动类完成名称 -->        <output.software.mainclass>cn.ly.PetHomeApp</output.software.mainclass>        <!-- 激活配置 -->        <spring.auto.active>dev</spring.auto.active>        <!-- os.platform 配置保持为空,不填写任何内容 -->        <os.platform></os.platform>        <platform>linux-x86_64</platform>        <platform.format>tar.gz</platform.format>    </properties>AI写代码XML参数说明:1.output.software.name 打包输出软件名称2.output.software.libs     jar包目录名称3.output.software.config 配置文件目录名称4.output.software.version 打包软件版本号5.output.software.mainclass 启动类路径6.spring.auto.active 多环境下配置的激活配置这里启用的dev(没有就不管,用到了参考下面配置)7.platform 配置的输出软件后缀(可以不用配置)8.platform.format 打包格式,这里配置的 tar.gz上面第6点使用多环境 在application.yml中需要配置spring:  profiles:    active: ${spring.auto.active}AI写代码java运行如果报错,加入配置,支持yml读取pom参数。这里已经引入            <!--支持yaml读取pom的参数-->            <plugin>                <artifactId>maven-resources-plugin</artifactId>                <configuration>                    <encoding>utf-8</encoding>                    <useDefaultDelimiters>true</useDefaultDelimiters>                </configuration>            </plugin>AI写代码XML 3.需要在src/assembly中新建配置文件 assembly.xml 这个文件是把打出来的包进行压缩处理1.文件目录  2.这是assembly.xml文件<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd"><id>release-${platform}</id><formats><format>${platform.format}</format></formats><includeBaseDirectory>true</includeBaseDirectory><files><file><source>./target/${output.software.name}/${output.software.name}-${output.software.version}.jar</source><outputDirectory></outputDirectory><fileMode>755</fileMode></file></files> <fileSets><fileSet><directory>./target/${output.software.name}/${output.software.config}</directory><outputDirectory>${output.software.config}</outputDirectory></fileSet> <fileSet><directory>./target/${output.software.name}/${output.software.libs}</directory><outputDirectory>${output.software.libs}</outputDirectory></fileSet></fileSets> </assembly> AI写代码XML 4.然后直接打包 这是打包之后的结果5.直接进入jar包目录,就可以执行jar包了3.运行jar包 进入jar包目录,进入控制台,输入命令 java -jar 包名————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/QWERTYuuid/article/details/125980426
  • [技术干货] springboot项目打包:你的项目还是打成一个xx.jar包吗?看看如何打成分层包-转载
    1.整合打包 1.pom.xml引入依赖<!-- SpringBoot 打包插件,把 maven-jar-plugin 打成的jar包重新打成可运行jar包 --><plugin>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-maven-plugin</artifactId></plugin>AI写代码java运行 2.执行mvn命令打包或者工具直接打包1.maven常用打包命令:1、mvn clean 将旧的class字节码文件删除。2、mvn pakage 打包,动态web工程打war包,Java工程打jar 包。3、mvn install 将项目生成jar包放在仓库中,然后打包。4、mvn clean install 删除target文件夹 ,然后打包到target。2.maven工具打包maven窗口执行 在项目target文件中找到jar包这样的jar包,包含了依赖包,配置文件在里面,对于项目的部署发布很不友好,如果网络传输慢,需要很长时间发布,更新替换配置文件也需要重新打包,很麻烦。下面是分层打包,对依赖和配置文件和运行jar包分开,部署起来就非常方便快捷啦👇👇👇2.分层打包1.pom.xml 引入依赖1.在 build中引入<build>        <finalName>${output.software.name}-${output.software.version}</finalName>        <resources>            <resource>                <directory>${project.basedir}/lib</directory>                <targetPath>BOOT-INF/lib/</targetPath>                <includes>                    <include>**/*.jar</include>                </includes>            </resource>            <resource>                <directory>src/main/resources</directory>                <filtering>true</filtering>            </resource>        </resources>        <plugins>            <!--支持yaml读取pom的参数-->            <plugin>                <artifactId>maven-resources-plugin</artifactId>                <configuration>                    <encoding>utf-8</encoding>                    <useDefaultDelimiters>true</useDefaultDelimiters>                </configuration>            </plugin>            <!-- 打JAR包 -->            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-jar-plugin</artifactId>                <configuration>                    <!-- 不打包资源文件(配置文件和依赖包分开) -->                    <excludes>                        <exclude>application*.yml</exclude>                    </excludes>                    <archive>                        <manifest>                            <addClasspath>true</addClasspath>                            <!-- MANIFEST.MF 中 Class-Path 加入前缀 -->                            <classpathPrefix>${output.software.libs}/</classpathPrefix>                            <!-- jar包不包含唯一版本标识 -->                            <useUniqueVersions>false</useUniqueVersions>                            <!--指定入口类 -->                            <mainClass>${output.software.mainclass}</mainClass>                        </manifest>                        <manifestEntries>                            <!--MANIFEST.MF 中 Class-Path 加入资源文件目录 -->                            <Class-Path>./${output.software.config}/</Class-Path>                        </manifestEntries>                    </archive>                    <outputDirectory>${project.build.directory}/${output.software.name}</outputDirectory>                </configuration>            </plugin>            <!-- 这个插件是用来复制项目依赖的jar包 -->            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-dependency-plugin</artifactId>                <executions>                    <execution>                        <!-- 自定义 -->                        <id>copy-dependencies</id>                        <phase>package</phase>                        <goals>                            <!-- 复制依赖的jar包 -->                            <goal>copy-dependencies</goal>                        </goals>                        <configuration>                            <!-- 将依赖的jar包复制到该路径下 -->                            <outputDirectory>                                ${project.build.directory}/${output.software.name}/${output.software.libs}                            </outputDirectory>                        </configuration>                    </execution>                </executions>            </plugin>             <!-- 这个插件是用来复制项目的静态资源-->            <plugin>                <artifactId>maven-resources-plugin</artifactId>                <executions>                    <execution>                        <!-- 自定义 -->                        <id>copy-resources</id>                        <phase>package</phase>                        <goals>                            <!-- 复制静态资源 -->                            <goal>copy-resources</goal>                        </goals>                        <configuration>                            <resources>                                <resource>                                    <!-- 指定静态资源的路径 -->                                    <directory>target/classes</directory>                                    <!-- 指定需要复制的文件 -->                                    <includes>                                        <include>application.properties</include>                                        <include>application.yml</include>                                        <include>application-${spring.auto.active}.yml</include>                                    </includes>                                </resource>                            </resources>                            <!-- 指定复制到该目录下 -->                            <outputDirectory>                                ${project.build.directory}/${output.software.name}/${output.software.config}                            </outputDirectory>                        </configuration>                    </execution>                </executions>            </plugin>             <!-- 以上配置后你的文件打包后的文件目录如下                -lib                -config                -项目名.jar             -->            <!-- 这个插件使用来将分离出来的静态资源和依赖的jar包(就是上面说到的文件目录),        压缩成一个zip文件。个人感觉这个蛮方便的 -->            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-assembly-plugin</artifactId>                <configuration>                    <!-- 这个插件需要指定一个配置文件 【重点:这里的assembly.xml 就是讲到分层打包】 -->                    <descriptors>                        <descriptor>src/assembly/assembly.xml</descriptor>                    </descriptors>                </configuration>                <executions>                    <execution>                        <!-- 自定义 -->                        <id>make-assembly</id>                        <phase>package</phase>                        <goals>                            <!-- 只执行一次 -->                            <goal>single</goal>                        </goals>                    </execution>                </executions>            </plugin>        </plugins>    </build>AI写代码XML 2.properties需要引入配置参数<properties>        <!-- 打包输出软件名称 -->        <output.software.name>pet</output.software.name>        <!-- 打包输出依赖包路径 -->        <output.software.libs>lib</output.software.libs>        <!-- 打包输出配置文件路径 -->        <output.software.config>config</output.software.config>        <!-- 打包输出软件版本号 -->        <output.software.version>1.0.0</output.software.version>        <!-- 启动类完成名称 -->        <output.software.mainclass>cn.ly.PetHomeApp</output.software.mainclass>        <!-- 激活配置 -->        <spring.auto.active>dev</spring.auto.active>        <!-- os.platform 配置保持为空,不填写任何内容 -->        <os.platform></os.platform>        <platform>linux-x86_64</platform>        <platform.format>tar.gz</platform.format>    </properties>AI写代码XML参数说明:1.output.software.name 打包输出软件名称2.output.software.libs     jar包目录名称3.output.software.config 配置文件目录名称4.output.software.version 打包软件版本号5.output.software.mainclass 启动类路径6.spring.auto.active 多环境下配置的激活配置这里启用的dev(没有就不管,用到了参考下面配置)7.platform 配置的输出软件后缀(可以不用配置)8.platform.format 打包格式,这里配置的 tar.gz上面第6点使用多环境 在application.yml中需要配置spring:  profiles:    active: ${spring.auto.active}AI写代码java运行如果报错,加入配置,支持yml读取pom参数。这里已经引入            <!--支持yaml读取pom的参数-->            <plugin>                <artifactId>maven-resources-plugin</artifactId>                <configuration>                    <encoding>utf-8</encoding>                    <useDefaultDelimiters>true</useDefaultDelimiters>                </configuration>            </plugin>AI写代码XML 3.需要在src/assembly中新建配置文件 assembly.xml 这个文件是把打出来的包进行压缩处理1.文件目录  2.这是assembly.xml文件<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd"><id>release-${platform}</id><formats><format>${platform.format}</format></formats><includeBaseDirectory>true</includeBaseDirectory><files><file><source>./target/${output.software.name}/${output.software.name}-${output.software.version}.jar</source><outputDirectory></outputDirectory><fileMode>755</fileMode></file></files> <fileSets><fileSet><directory>./target/${output.software.name}/${output.software.config}</directory><outputDirectory>${output.software.config}</outputDirectory></fileSet> <fileSet><directory>./target/${output.software.name}/${output.software.libs}</directory><outputDirectory>${output.software.libs}</outputDirectory></fileSet></fileSets> </assembly> AI写代码XML 4.然后直接打包 这是打包之后的结果5.直接进入jar包目录,就可以执行jar包了3.运行jar包 进入jar包目录,进入控制台,输入命令 java -jar 包名————————————————                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                        原文链接:https://blog.csdn.net/QWERTYuuid/article/details/125980426
  • [技术干货] 服务器返回500错误的排查思路
    当服务器返回500错误(Internal Server Error)时,表明服务器遇到了意外情况,无法完成请求。以下是系统化的排查思路,帮助快速定位问题根源:一、基础信息收集复现问题确认错误是否可复现(如特定URL、特定操作触发)。检查错误发生的时间点,关联是否有代码部署、配置变更或流量高峰。查看日志应用日志:检查应用服务器(如Tomcat、Nginx)的日志文件(如/var/log/nginx/error.log或/var/log/tomcat/catalina.out),寻找异常堆栈或错误信息。系统日志:通过journalctl -u <服务名>(Systemd)或/var/log/syslog查看系统级错误。监控工具使用Prometheus、Grafana等工具检查服务器资源(CPU、内存、磁盘)是否过载。检查数据库连接池是否耗尽(如MySQL的SHOW STATUS LIKE 'Threads_connected')。二、常见原因分类排查1. 代码或应用逻辑错误未捕获的异常:检查日志中的堆栈跟踪,定位到具体代码行(如空指针、类型转换错误)。示例:Java中未处理NullPointerException,导致500错误。第三方服务调用失败:确认依赖的API、数据库或缓存服务是否可用(如Redis超时、数据库死锁)。配置错误:检查应用配置文件(如application.properties)中的数据库URL、端口、密钥等是否正确。2. 服务器资源问题内存不足:使用free -h或top检查内存使用率,若Swap频繁使用,可能触发OOM Killer。磁盘空间耗尽:运行df -h,确保根目录或日志分区未满(如Nginx日志未轮转)。文件描述符耗尽:通过lsof | wc -l和ulimit -n检查文件描述符限制。3. 中间件或依赖服务问题数据库连接池耗尽:检查连接池配置(如HikariCP的maximumPoolSize),并监控数据库的慢查询。反向代理配置错误:确认Nginx/Apache的proxy_pass、location规则是否正确,避免循环重定向。缓存服务故障:如Redis宕机或网络分区,导致应用依赖缓存的逻辑失败。4. 权限或路径问题文件权限不足:应用对日志目录、上传文件目录无写入权限(如/var/www/uploads权限为755但需775)。SELinux/AppArmor限制:检查ausearch -m avc -ts recent(SELinux)或aa-status(AppArmor)是否有拒绝记录。5. 部署或版本问题代码冲突:确认部署的代码版本是否与测试环境一致(如Git提交ID是否正确)。依赖版本不兼容:检查pom.xml(Maven)或package.json(Node.js)中依赖库的版本冲突。三、分步排查流程快速定位日志优先查看应用日志中的最新错误(按时间排序),通常500错误会伴随堆栈跟踪。验证依赖服务手动测试数据库、缓存等服务的连通性(如telnet <host> <port>或mysql -h <host> -u <user>)。模拟请求使用curl -v http://example.com/api或Postman复现请求,观察响应头和体中的详细错误。检查环境差异对比生产环境与测试环境的配置(如环境变量、JVM参数),确认是否有遗漏。回滚或降级若近期有变更,尝试回滚到上一版本,验证是否为变更导致。四、工具推荐日志分析:ELK Stack(Elasticsearch+Logstash+Kibana)、Loki+Grafana。APM工具:New Relic、Datadog、SkyWalking(追踪请求链路)。压力测试:使用ab(Apache Benchmark)或wrk模拟高并发,观察500错误是否触发。五、预防措施完善日志:在关键代码路径添加日志,记录输入参数、异常堆栈和上下文信息。监控告警:设置500错误率的阈值告警(如Prometheus Alertmanager)。灰度发布:分批上线新功能,降低影响范围。自动化测试:增加集成测试和端到端测试,覆盖核心业务场景。六、案例参考案例1:现象:用户上传文件后返回500。排查:日志显示FileNotFoundException,因上传目录未创建。解决:修改代码确保目录存在,或初始化时创建目录。案例2:现象:高峰期频繁500。排查:监控显示内存使用率达90%,应用因OOM被终止。解决:优化JVM堆内存配置(-Xmx),并增加缓存清理逻辑。通过以上步骤,可以系统化地定位500错误的根本原因,并采取针对性措施修复。核心原则是从日志出发,逐步缩小问题范围,并结合监控和工具验证假设。
  • [技术干货] 服务器返回400错误的排查思路
    当 HttpResponse 的 isOk() 返回 false 且状态码为 400(Bad Request)时,通常表示客户端发送的请求存在语法或参数错误。要获取更具体的错误原因,可以从以下几个地方排查:1. 响应体(Response Body)关键来源:服务器通常会在响应体中返回详细的错误信息(如 JSON/XML 格式的错误描述)。获取方式:String errorBody = response.getBody(); // 直接读取响应体 System.out.println("Error Body: " + errorBody); 注意:如果响应体是 JSON,可以解析特定字段(如 message、code):import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; ObjectMapper mapper = new ObjectMapper(); JsonNode errorJson = mapper.readTree(errorBody); String errorMessage = errorJson.path("message").asText(); 2. 响应头(Response Headers)可能包含的线索:某些 API 会在响应头中附加错误信息(如 X-Error-Message、WWW-Authenticate)。获取方式:Headers headers = response.getHeaders(); for (Header header : headers) { System.out.println(header.getName() + ": " + header.getValue()); } 3. HTTP 状态码的扩展信息状态码 400 的常见子类型:400 Bad Request:可能因缺失必填参数、参数类型错误、请求体格式无效等。某些框架(如 Spring)会返回更具体的 4xx 状态码,如 422 Unprocessable Entity(数据验证失败)。4. 服务器日志后端调试:如果可能,检查服务器日志,通常会有更详细的错误堆栈或验证失败原因。5. 框架/工具特定的错误信息RestTemplate(Spring):捕获 HttpClientErrorException:try { restTemplate.getForEntity(url, String.class); } catch (HttpClientErrorException e) { System.out.println("Status: " + e.getStatusCode()); System.out.println("Error Body: " + e.getResponseBodyAsString()); } OkHttp/Retrofit:通过 response.errorBody():try { Response response = client.newCall(request).execute(); } catch (IOException e) { if (response.code() == 400) { String errorBody = response.body().string(); // 注意:只能调用一次 } } 6. 调试工具抓包工具:使用 Wireshark、Fiddler 或浏览器开发者工具查看原始请求和响应。日志拦截器:在 OkHttp/Retrofit 中添加日志拦截器:OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build(); 常见错误原因示例场景典型错误信息(响应体)缺失必填参数{"error": "Missing required parameter: id"}参数类型不匹配{"code": 400, "message": "Invalid format for 'age'"}请求体格式错误HTTP 400 Bad Request: JSON parsing error总结步骤优先读取响应体:90% 的具体错误信息在此。检查响应头:寻找自定义错误头。复现请求:用工具(如 Postman)手动发送相同请求,观察完整响应。联系后端团队:如果错误信息不明确,可能需要后端补充日志或文档。通过以上方法,通常可以定位到 400 错误的根本原因。
  • [技术干货] 获取shiro登录的用户名
    获取Shiro登录的用户名在Apache Shiro中,可以通过以下几种方式获取当前登录用户的用户名:1. 使用SecurityUtilsimport org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; // 获取当前Subject Subject currentUser = SecurityUtils.getSubject(); // 检查用户是否已认证 if (currentUser.isAuthenticated()) { // 获取用户名 String username = currentUser.getPrincipal().toString(); System.out.println("当前登录用户: " + username); } else { System.out.println("用户未登录"); } 2. 在Controller中获取(Spring MVC环境)import org.apache.shiro.SecurityUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MyController { @RequestMapping("/userinfo") public String getUserInfo() { String username = SecurityUtils.getSubject().getPrincipal().toString(); System.out.println("当前用户: " + username); return "userPage"; } } 3. 使用注解方式(需要Shiro注解支持)import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @GetMapping("/currentUser") @RequiresAuthentication public String getCurrentUser() { return SecurityUtils.getSubject().getPrincipal().toString(); } } 4. 在自定义Realm中获取如果你需要在Realm中获取当前用户信息:public class MyRealm extends AuthorizingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 获取用户名 String username = (String) token.getPrincipal(); // ... 其他认证逻辑 } } 注意事项确保用户已经登录,否则getPrincipal()可能返回null如果使用自定义的Principal对象(不只是用户名),需要相应地转换类型在多线程环境中,Shiro的Subject是线程绑定的,确保在正确的线程中获取自定义Principal对象如果你使用了自定义的Principal对象(不只是存储用户名):public class UserPrincipal implements Principal { private String username; private Long userId; // getters and setters @Override public String toString() { return username; } } // 获取时 UserPrincipal principal = (UserPrincipal) SecurityUtils.getSubject().getPrincipal(); String username = principal.getUsername(); 以上方法都可以帮助你获取当前通过Shiro登录的用户名。
  • [技术干货] SpringBoot-集成FTP(上传、下载、删除)
    在SpringBoot项目中集成FTP功能,可以通过Apache Commons Net库来实现。下面是一个完整的实现方案,包括配置、上传、下载和删除文件的功能。1. 添加依赖首先,在pom.xml中添加Apache Commons Net依赖:<dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.9.0</version> </dependency> 2. FTP配置类创建一个FTP配置类,用于存储FTP连接信息:import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "ftp") public class FtpConfig { private String host; private int port; private String username; private String password; private String basePath; // 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 String getBasePath() { return basePath; } public void setBasePath(String basePath) { this.basePath = basePath; } } 在application.yml或application.properties中配置FTP信息:ftp: host: 127.0.0.1 port: 21 username: your_username password: your_password basePath: /ftp/files3. FTP工具类创建一个FTP工具类,封装上传、下载和删除功能:import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPFile; import org.apache.commons.net.ftp.FTPReply; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @Component public class FtpUtil { @Autowired private FtpConfig ftpConfig; /** * 连接FTP服务器 */ private FTPClient connect() throws IOException { FTPClient ftpClient = new FTPClient(); ftpClient.setControlEncoding("UTF-8"); ftpClient.connect(ftpConfig.getHost(), ftpConfig.getPort()); ftpClient.login(ftpConfig.getUsername(), ftpConfig.getPassword()); ftpClient.setFileType(FTP.BINARY_FILE_TYPE); ftpClient.enterLocalPassiveMode(); ftpClient.setBufferSize(1024 * 1024); // 设置缓冲区大小 // 检查连接是否成功 int replyCode = ftpClient.getReplyCode(); if (!FTPReply.isPositiveCompletion(replyCode)) { ftpClient.disconnect(); throw new IOException("FTP服务器拒绝连接"); } return ftpClient; } /** * 上传文件到FTP服务器 * @param remotePath 远程路径 * @param fileName 文件名 * @param inputStream 输入流 * @return 是否上传成功 */ public boolean uploadFile(String remotePath, String fileName, InputStream inputStream) { FTPClient ftpClient = null; try { ftpClient = connect(); // 切换目录 if (!ftpClient.changeWorkingDirectory(remotePath)) { // 如果目录不存在,则创建 String[] dirs = remotePath.split("/"); String tempPath = ""; for (String dir : dirs) { if (dir.length() > 0) { tempPath += "/" + dir; if (!ftpClient.changeWorkingDirectory(tempPath)) { if (!ftpClient.makeDirectory(tempPath)) { return false; } } } } } // 上传文件 boolean success = ftpClient.storeFile(fileName, inputStream); return success; } catch (IOException e) { e.printStackTrace(); return false; } finally { if (ftpClient != null && ftpClient.isConnected()) { try { ftpClient.logout(); ftpClient.disconnect(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 从FTP服务器下载文件 * @param remotePath 远程路径 * @param fileName 文件名 * @param outputStream 输出流 * @return 是否下载成功 */ public boolean downloadFile(String remotePath, String fileName, OutputStream outputStream) { FTPClient ftpClient = null; try { ftpClient = connect(); // 切换目录 if (!ftpClient.changeWorkingDirectory(remotePath)) { return false; } // 下载文件 boolean success = ftpClient.retrieveFile(fileName, outputStream); return success; } catch (IOException e) { e.printStackTrace(); return false; } finally { if (ftpClient != null && ftpClient.isConnected()) { try { ftpClient.logout(); ftpClient.disconnect(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 删除FTP服务器上的文件 * @param remotePath 远程路径 * @param fileName 文件名 * @return 是否删除成功 */ public boolean deleteFile(String remotePath, String fileName) { FTPClient ftpClient = null; try { ftpClient = connect(); // 切换目录 if (!ftpClient.changeWorkingDirectory(remotePath)) { return false; } // 删除文件 boolean success = ftpClient.deleteFile(fileName); return success; } catch (IOException e) { e.printStackTrace(); return false; } finally { if (ftpClient != null && ftpClient.isConnected()) { try { ftpClient.logout(); ftpClient.disconnect(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 检查文件是否存在 * @param remotePath 远程路径 * @param fileName 文件名 * @return 文件是否存在 */ public boolean fileExists(String remotePath, String fileName) { FTPClient ftpClient = null; try { ftpClient = connect(); // 切换目录 if (!ftpClient.changeWorkingDirectory(remotePath)) { return false; } FTPFile[] files = ftpClient.listFiles(); for (FTPFile file : files) { if (file.getName().equals(fileName)) { return true; } } return false; } catch (IOException e) { e.printStackTrace(); return false; } finally { if (ftpClient != null && ftpClient.isConnected()) { try { ftpClient.logout(); ftpClient.disconnect(); } catch (IOException e) { e.printStackTrace(); } } } } } 4. 使用示例创建一个控制器来测试FTP功能:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @RestController @RequestMapping("/ftp") public class FtpController { @Autowired private FtpUtil ftpUtil; @PostMapping("/upload") public String uploadFile(@RequestParam("file") MultipartFile file) { try { InputStream inputStream = file.getInputStream(); boolean success = ftpUtil.uploadFile("/upload", file.getOriginalFilename(), inputStream); if (success) { return "文件上传成功"; } else { return "文件上传失败"; } } catch (IOException e) { e.printStackTrace(); return "文件上传异常"; } } @GetMapping("/download") public byte[] downloadFile(@RequestParam String fileName) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); boolean success = ftpUtil.downloadFile("/upload", fileName, outputStream); if (success) { return outputStream.toByteArray(); } else { return null; } } catch (Exception e) { e.printStackTrace(); return null; } } @DeleteMapping("/delete") public String deleteFile(@RequestParam String fileName) { boolean success = ftpUtil.deleteFile("/upload", fileName); if (success) { return "文件删除成功"; } else { return "文件删除失败"; } } @GetMapping("/check") public String checkFile(@RequestParam String fileName) { boolean exists = ftpUtil.fileExists("/upload", fileName); if (exists) { return "文件存在"; } else { return "文件不存在"; } } } 5. 注意事项连接管理:每次操作都新建连接,操作完成后关闭连接。对于高频操作,可以考虑使用连接池。异常处理:实际应用中需要更完善的异常处理和日志记录。性能优化:对于大文件传输,可以考虑分块上传/下载。安全性:确保FTP服务器配置了适当的安全措施,如使用SFTP/FTPS代替普通FTP。被动模式:示例中使用的是被动模式(PASV),适用于大多数防火墙/NAT环境。6. 高级功能扩展如果需要更高级的功能,可以考虑:断点续传:记录已传输的字节位置,支持中断后继续传输目录操作:添加创建目录、删除目录、列出目录等功能批量操作:支持批量上传/下载/删除进度监控:添加传输进度回调接口以上实现提供了SpringBoot集成FTP的基本功能,可以根据实际需求进行扩展和优化。
总条数:212 到第
上滑加载中