本文档记录了 service 项目在华为云 Docker 环境部署过程中遇到的所有问题及解决方案。
目录
- [1. Docker 网络配置问题](#1. Docker 网络配置问题)
- [2. 端口占用问题](#2. 端口占用问题)
- [3. Docker 多阶段构建问题](#3. Docker 多阶段构建问题)
- [4. Alpine 镜像源问题](#4. Alpine 镜像源问题)
- [5. Nacos 认证问题](#5. Nacos 认证问题)
- [6. Spring Boot 配置问题](#6. Spring Boot 配置问题)
- [7. 多数据源配置问题](#7. 多数据源配置问题)
- [8. MyBatis 配置问题](#8. MyBatis 配置问题)
- [9. Spring Boot 版本兼容性问题](#9. Spring Boot 版本兼容性问题)
1. Docker 网络配置问题
问题描述
Nacos 也部署在 mlm Docker 网络中,但配置文件使用的是外部域名 ****.*****.com:8848,导致容器内无法访问。
解决方案
修改配置使用 Docker 内部容器名访问:
docker-compose.yml
yaml
networks:
default:
name: mlm
external: true
application.yml
yaml
spring:
cloud:
nacos:
discovery:
server-addr: nacos:8848 # 使用容器名而非域名
config:
server-addr: nacos:8848
2. 端口占用问题
问题描述
主机 8080 端口已被其他服务占用,导致服务无法启动。
解决方案
修改 docker-compose.yml 端口映射:
yaml
services:
service:
ports:
- "8081:8080" # 修改为 8081
3. Docker 多阶段构建问题
问题描述
原 Dockerfile 需要本地先执行 gradle build,增加了部署复杂度。
解决方案
实现 Docker 多阶段构建,将编译集成到镜像构建过程中:
Dockerfile
dockerfile
# 第一阶段:使用 Gradle 镜像构建
FROM gradle:8.5-jdk21-alpine AS builder
WORKDIR /build
# 复制构建文件
COPY build.gradle settings.gradle ./
# 复制源代码
COPY src ./src
# 构建项目
RUN gradle clean build -x test --no-daemon
# 第二阶段:运行阶段
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# 从构建阶段复制编译好的jar文件
COPY --from=builder /build/build/libs/*.jar app.jar
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 暴露端口
EXPOSE 8080
# 启动应用
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]
4. Alpine 镜像源问题
问题描述
在华为云环境构建时,访问 Alpine 官方软件包仓库超时:
fetching https://dl-cdn.alpinelinux.org/alpine/v3.22/main: temporary error
解决方案
添加华为云镜像源配置:
Dockerfile
dockerfile
# 使用华为云镜像源并安装curl用于健康检查
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.huaweicloud.com/g' /etc/apk/repositories && \
apk add --no-cache curl
5. Nacos 认证问题
问题描述
应用启动时报错:
ErrCode:403, ErrMsg:http error, code=403,msg=user not found!
原因分析
- Nacos 配置了
jiahe用户,但应用使用了错误的认证配置 - 命名空间使用了名称而非 ID
解决方案
application.yml
yaml
spring:
cloud:
nacos:
username: <your-nacos-username>
password: <your-nacos-password>
discovery:
server-addr: nacos:8848
namespace: <your-namespace-id> # 使用 namespace ID
config:
server-addr: nacos:8848
namespace: <your-namespace-id>
username: <your-nacos-username>
password: <your-nacos-password>
config:
import:
- optional:nacos:${spring.application.name}.properties
6. Spring Boot 配置问题
问题 6.1: spring.config.import 缺失
错误信息
No spring.config.import property has been defined
解决方案
yaml
spring:
config:
import:
- optional:nacos:${spring.application.name}.properties
问题 6.2: Bean 定义冲突
错误信息
Cannot register bean definition 'hikariPoolDataSourceMetadataProvider'
解决方案
yaml
spring:
main:
allow-bean-definition-overriding: true
7. 多数据源配置问题
问题描述
使用 dynamic-datasource-spring-boot-starter:4.3.1 库时遇到多个版本兼容性问题:
- YAML 配置格式无法识别
DataSourceProperty类无法访问AbstractDataSourceProvider构造参数不兼容DynamicRoutingDataSourceAPI 不可用
解决方案
完全自定义实现多数据源切换机制,不依赖第三方库。
7.1 创建动态数据源类
DynamicDataSource.java
java
package com.jiahetng.service.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSource(String dataSourceKey) {
contextHolder.set(dataSourceKey);
}
public static String getDataSource() {
return contextHolder.get();
}
public static void clearDataSource() {
contextHolder.remove();
}
@Override
protected Object determineCurrentLookupKey() {
String dataSource = getDataSource();
return dataSource != null ? dataSource : "primary";
}
}
7.2 创建自定义 @DS 注解
DS.java
java
package com.jiahetng.service.config;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
String value() default "primary";
}
7.3 创建 AOP 切面
DynamicDataSourceAspect.java
java
package com.jiahetng.service.config;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Order(1)
@Component
public class DynamicDataSourceAspect {
@Around("@annotation(com.jiahetng.service.config.DS) || @within(com.jiahetng.service.config.DS)")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DS ds = method.getAnnotation(DS.class);
if (ds == null) {
ds = method.getDeclaringClass().getAnnotation(DS.class);
}
if (ds != null) {
String dataSourceKey = ds.value();
DynamicDataSource.setDataSource(dataSourceKey);
log.debug("Switching to datasource: {}", dataSourceKey);
}
try {
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
log.debug("Cleared datasource context");
}
}
}
7.4 配置数据源 Bean
DataSourceConfig.java
java
package com.jiahetng.service.config;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.primary.driver-class-name}")
private String primaryDriverClassName;
@Value("${spring.datasource.primary.url}")
private String primaryUrl;
@Value("${spring.datasource.primary.username}")
private String primaryUsername;
@Value("${spring.datasource.primary.password}")
private String primaryPassword;
@Value("${spring.datasource.secondary.driver-class-name}")
private String secondaryDriverClassName;
@Value("${spring.datasource.secondary.url}")
private String secondaryUrl;
@Value("${spring.datasource.secondary.username}")
private String secondaryUsername;
@Value("${spring.datasource.secondary.password}")
private String secondaryPassword;
@Bean(name = "primaryDataSource")
public DataSource primaryDataSource() {
log.info("Creating primary datasource: url={}", primaryUrl);
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName(primaryDriverClassName);
dataSource.setJdbcUrl(primaryUrl);
dataSource.setUsername(primaryUsername);
dataSource.setPassword(primaryPassword);
log.info("Primary datasource created successfully");
return dataSource;
}
@Bean(name = "secondaryDataSource")
public DataSource secondaryDataSource() {
log.info("Creating secondary datasource: url={}", secondaryUrl);
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName(secondaryDriverClassName);
dataSource.setJdbcUrl(secondaryUrl);
dataSource.setUsername(secondaryUsername);
dataSource.setPassword(secondaryPassword);
log.info("Secondary datasource created successfully");
return dataSource;
}
@Primary
@Bean(name = "dataSource")
public DataSource dynamicDataSource(
@Qualifier("primaryDataSource") DataSource primaryDataSource,
@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
log.info("Creating dynamic datasource");
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("primary", primaryDataSource);
targetDataSources.put("secondary", secondaryDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(primaryDataSource);
try {
dynamicDataSource.afterPropertiesSet();
log.info("Dynamic datasource created and initialized successfully");
} catch (Exception e) {
log.error("Failed to initialize dynamic datasource", e);
throw new RuntimeException("Failed to initialize dynamic datasource", e);
}
return dynamicDataSource;
}
}
7.5 更新 Service 类
所有 Service 类的 import 需要更新:
java
// 旧的 import
import com.baomidou.dynamic.datasource.annotation.DS;
// 新的 import
import com.jiahetng.service.config.DS;
使用示例:
java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
@DS("primary")
public User findByUsername(String username) {
return userMapper.findByUsername(username);
}
}
@Service
@RequiredArgsConstructor
@Slf4j
public class LoginLogService {
@DS("secondary")
public List<LoginLog> findByUserId(Long userId, int page, int size) {
return loginLogMapper.findByUserId(userId, offset, size);
}
}
7.6 配置文件
application.yml
yaml
spring:
datasource:
primary:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://mariadb:3306/service_primary?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: <your_username>
password: <your_password>
secondary:
driver-class-name: org.mariadb.jdbc.Driver
url: jdbc:mariadb://mariadb:3306/service_secondary?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: <your_username>
password: <your_password>
8. MyBatis 配置问题
问题 8.1: @MapperScan 注解缺失
错误信息
error: cannot find symbol @MapperScan
解决方案
在 ServiceApplication.java 中添加 import:
java
import org.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.jiahetng.service.mapper")
public class ServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceApplication.class, args);
}
}
问题 8.2: SqlSessionFactory 初始化失败
错误信息
Error creating bean with name 'sqlSessionFactory':
Factory method 'sqlSessionFactory' threw exception with message: null
原因分析
- 动态数据源 Bean 名称不是
dataSource,MyBatis 无法自动发现 - 配置了
mapper-locations但项目使用注解方式
解决方案
- 重命名动态数据源 Bean:
java
@Primary
@Bean(name = "dataSource") // 必须命名为 dataSource
public DataSource dynamicDataSource(...) {
// ...
dynamicDataSource.afterPropertiesSet(); // 必须调用此方法
return dynamicDataSource;
}
- 移除 mapper-locations 配置:
yaml
# MyBatis配置
mybatis:
type-aliases-package: com.jiahetng.service.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
# 不配置 mapper-locations(使用注解方式)
- 手动配置 SqlSessionFactory(可选,用于调试):
MyBatisConfig.java
java
package com.jiahetng.service.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Slf4j
@Configuration
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
log.info("Creating SqlSessionFactory with dataSource: {}", dataSource.getClass().getName());
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
// 配置 MyBatis
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setLogImpl(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
factoryBean.setConfiguration(configuration);
// 设置类型别名包
factoryBean.setTypeAliasesPackage("com.jiahetng.service.entity");
try {
SqlSessionFactory sessionFactory = factoryBean.getObject();
log.info("SqlSessionFactory created successfully");
return sessionFactory;
} catch (Exception e) {
log.error("Failed to create SqlSessionFactory", e);
throw e;
}
}
}
9. Spring Boot 版本兼容性问题
问题描述
应用启动失败,报版本不兼容错误:
Spring Boot [3.5.9] is not compatible with this Spring Cloud release train
Action: Change Spring Boot version to one of the following versions [3.4.x]
原因分析
Spring Cloud Alibaba 2023.0.3.2 要求 Spring Boot 3.4.x,但项目使用了 3.5.9。
解决方案
降级 Spring Boot 并同步调整相关依赖版本:
build.gradle
gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1' // 降级到 3.4.1
id 'io.spring.dependency-management' version '1.1.7'
id 'com.google.protobuf' version '0.9.5'
}
ext {
set('springGrpcVersion', "0.9.0") // 从 1.0.1 降级到 0.9.0
}
dependencies {
// MyBatis 版本调整
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4' // 从 4.0.1 降级
// 其他依赖保持不变
implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery:2023.0.3.2'
implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config:2023.0.3.2'
implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer:4.2.0'
}
版本兼容性矩阵:
| 组件 | 原版本 | 调整后版本 | 说明 |
|---|---|---|---|
| Spring Boot | 3.5.9 | 3.4.1 | 兼容 Spring Cloud 2023.0.x |
| Spring gRPC | 1.0.1 | 0.9.0 | 兼容 Spring Boot 3.4.x |
| MyBatis | 4.0.1 | 3.0.4 | 兼容 Spring Boot 3.4.x |
| Spring Cloud Alibaba | 2023.0.3.2 | 2023.0.3.2 | 无需修改 |
| Spring Cloud LoadBalancer | 4.2.0 | 4.2.0 | 无需修改 |
10. 部署验证
完整部署流程
- 停止现有容器
bash
docker compose down
- 构建镜像
bash
docker compose build
- 启动服务
bash
docker compose up -d
- 查看日志
bash
docker compose logs -f service
成功启动标志
看到以下日志表示启动成功:
Creating primary datasource: url=jdbc:mariadb://<db-host>:3306/<primary-db-name>
Primary datasource created successfully
Creating secondary datasource: url=jdbc:mariadb://<db-host>:3306/<secondary-db-name>
Secondary datasource created successfully
Creating dynamic datasource
Dynamic datasource created and initialized successfully
Creating SqlSessionFactory with dataSource: com.jiahetng.service.config.DynamicDataSource
SqlSessionFactory created successfully
Started ServiceApplication in X.XXX seconds
健康检查
bash
# 检查服务状态
curl http://localhost:8081/actuator/health
# 检查 Nacos 注册
# 访问 Nacos 控制台查看服务列表
11、注册成功后, 服务实例地址是 Docker 内部 IP,
✅ 解决方案:让 Spring Boot 服务注册真实 IP 地址
✅ 使用 spring.cloud.nacos.discovery.ip 强制指定 IP
修改 application.yml:
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: nacos:8848
namespace: <namespace_id>
username: <your_username>
password: <your_password>
# 关键配置:强制使用宿主机 IP
ip: 192.168.1.100 # 替换为你的服务器真实 IP
port: 8080
group: DEFAULT_GROUP
⚠️ 注意:ip 和 port 需要与实际暴露的地址一致。
12. 关键要点总结
- Docker 网络: 容器间使用容器名访问,不要用外部域名
- 端口映射: 注意主机端口占用问题
- 多阶段构建: 将编译集成到 Docker 构建中
- 华为云镜像: 使用华为云 Alpine 镜像源
- Nacos 配置: 使用命名空间 ID 而非名称
- 多数据源: 自定义实现比使用第三方库更可控
- MyBatis :
- 动态数据源必须命名为
dataSource - 必须调用
afterPropertiesSet() - 注解方式不要配置
mapper-locations
- 动态数据源必须命名为
- 版本兼容: Spring Boot 降级时需同步调整 gRPC 和 MyBatis 版本
附录:常用命令
bash
# Docker 相关
docker compose down # 停止容器
docker compose build # 构建镜像
docker compose up -d # 后台启动
docker compose logs -f service # 查看日志
docker compose ps # 查看容器状态
docker builder prune -f # 清理构建缓存
# Gradle 相关(本地调试)
./gradlew clean build -x test # 本地构建
./gradlew dependencies # 查看依赖树
# 日志查看
docker compose logs service | grep ERROR # 查看错误日志
docker compose logs service | grep -A 20 ERROR # 查看错误及后续20行