只要你肯在Java后端群里蹲一个月,就会发现Spring相关的提问翻来覆去就那么几类:项目创建不了、Bean注入不上、AOP明明写了却不执行、MyBatis查不到数据、端口号改了没生效、OAuth2登录突然报错……这些问题看起来散,其实原因都特别集中,通常就是一个配置、一个注解或者一个代理细节的问题。我把自己在带团队做业务系统、帮新人排查问题过程中反复踩过的坑整理成了一份排查方案,从项目初始化、Bean容器、Web层、数据访问、配置管理,一直延伸到安全、状态机和微服务,覆盖了大家搜索频率很高的那几个点。
1. 开发环境与项目初始化:别卡在第一步
1.1 IDEA创建不了Spring项目:先分清是网络还是工程结构问题
“IDEA为什么创建不了Spring项目”是我见过提问量最高的问题之一。但很多人卡住并不是代码问题,而是操作入口和网络问题。
先说入口。IDEA有两个版本,社区版(Community)和旗舰版(Ultimate)。社区版默认没有Spring Initializr向导,新建项目时你根本看不到Spring相关选项。这不是软件坏了,而是功能本身不在社区版里。应对方法也很简单:去 start.spring.io 网页上填好你想要的技术栈,下载一个ZIP包,然后用IDEA的 File -> Open 直接打开Maven项目,IDEA会识别pom.xml并自动拉取依赖。
如果你用的是旗舰版,新建项目时选择Spring Initializr之后一直转圈,或者点Finish之后卡在下载进度条,大概率是本地网络访问 start.spring.io 失败。这个服务在国外,国内网络经常会出现连接超时。解决办法有两个:第一,在IDEA的 Initializr URL 自定义项里填国内镜像地址,比如https://start.aliyun.com;第二,直接用网页生成ZIP再导入。核心逻辑是一样的,只是换了一个更稳定的下载通道。
还有一类情况是项目建好了,但pom文件一直飘红,依赖下载不下来。这里要检查两个东西:Maven的settings.xml里面localRepository指向的本地仓库路径是否可用,以及是否配置了镜像。建议在settings.xml里加一个阿里云镜像,不然首次下载Spring Boot全家桶依赖会非常痛苦。
提示:项目名不要用中文,路径不要带空格,否则有些插件解析会出现奇怪的问题。这个习惯建议从一开始就养成。
1.2 VSCode里连Spring Boot都跑不起来怎么办
用VSCode写Spring Boot的人现在越来越多,主要原因是轻量、启动快。但VSCode的问题也很有代表性:不是代码问题,而是扩展和JDK配置问题。
首先必须要装两个扩展:Extension Pack for Java和Spring Boot Extension Pack。只装Java扩展的话,虽然能跑main方法,但Spring Boot Dashboard、自动配置提示、yml文件跳转这些能力都不完整。装完之后,VSCode左下角会出现一个Spring Boot的图标,点进去能看到当前工作区里所有的Boot应用。
常见的“命令不可用”问题,第一反应不是重装扩展,而是执行命令面板里的Java: Clean Java Language Server Workspace,清掉语言服务器的缓存再重新导入。很多情况下是Java Language Server状态乱了,而不是扩展没装好。如果还是不行,检查JAVA_HOME环境变量是否指向了正确的JDK,VSCode自己的终端里执行mvn -v,确认Maven能读到同一个JDK。
VSCode跑Spring Boot还有一种恶心的现象:debug运行时断点不生效。这往往是因为项目没有在Maven的生命周期里重新编译,代码和class不同步。执行mvn clean compile再启动基本能解决。没必要迷信IDE,开发工具的本质都是给你编译和运行的能力,出了问题先看底层命令能不能跑通。
2. 容器核心:Bean装配、三级缓存和“手写”一条龙
2.1 三级缓存到底在缓存什么
很多人面试背了“三级缓存”,但看代码时根本不知道三个Map在哪里。它就在DefaultSingletonBeanRegistry里面:
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);三个Map就是三级缓存。先是singletonFactories存原始对象的工厂,再是earlySingletonObjects存提前暴露的半成品实例,最后singletonObjects存完整Bean。
为什么需要这么绕?因为Spring要解决循环依赖。举个最常见的例子:A依赖B,B又依赖A。A先创建,发现需要注入B,于是去创建B;B创建时又需要A,如果此时拿不到A就完蛋了。Spring的做法是:A实例化后先把一个ObjectFactory放进第三级缓存,这个工厂能返回A的“早期引用”给外部使用。B通过这个早期引用拿到A,完成创建;A再拿到B完成注入。最终两个Bean都进入一级缓存。
那为什么不止两级?关键在AOP。如果A需要被代理,早期暴露出来的对象应该直接是代理对象。但Spring不能确定A会不会遇到循环依赖,它只能通过第三级缓存的ObjectFactory,在需要暴露的那一刻才去生成代理。如果没有第三级,所有Bean在实例化后都得提前生成代理,不仅浪费,而且无法处理异常情况。
这个设计也解释了一个经典教训:构造器注入的循环依赖解决不了。因为构造器注入在实例化阶段就必须拿到完整Bean,而三级缓存只能提供“早期引用”,没法让B在A构造完成前得到A的可注入属性。所以项目里出现循环依赖,优先重构而不是硬扛。
2.2 手写Spring:用200行代码把IOC和AOP打通
“手写Spring”是我特别推荐新人做的一步。你不需要写一个完整的框架,只要实现最简单的注解扫描、实例化和属性注入,思路立刻通透。
核心流程是这样的:
public class MiniApplicationContext { private final Map<String, Object> beans = new ConcurrentHashMap<>(); public void scan(String basePackage) throws Exception { // 1. 扫描basePackage下带@Component的类 // 2. 反射创建实例 // 3. 放入beans缓存 } private void inject(Object bean) throws IllegalAccessException { for (Field field : bean.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { field.setAccessible(true); field.set(bean, beans.get(field.getName())); } } } }这只是个示例,真正的MiniSpring还要处理构造函数注入、接口代理、BeanPostProcessor等。但你会发现,真写起来时循环依赖会让人非常头疼。A需要B,B需要A,你不得不在某个环节提前暴露一个不完整的引用,于是二级缓存出现了;你又需要支持代理,于是三级缓存出现了。知道了这个演化过程,你再去看Spring源码,就不会觉得那三个Map是什么黑魔法了。
手写过程中的第二个启发是AOP。你可以给容器加一个后处理器:在某个Bean完成初始化后,通过反射动态生成一个代理对象放回缓存里。这样调用方拿到的Bean其实是一个代理,日志、事务这类能力就都能织入进去。理解这个点,后面AOP不生效的排查就会特别快。
2.3 NoSuchBeanDefinitionException的排查顺序
遇到NoSuchBeanDefinitionException不要急,按顺序查:
- 包扫描范围。Spring Boot启动类默认扫描它所在包及子包,如果你的Service放在别的包下,没加
@ComponentScan就会找不到。 - 是否加了注解。
@Service、@Repository、@Component等价于注册Bean,但很多人只加了接口实现类,没写注解。 - 有没有多个实现类。两个类实现了同一个接口,注入时Spring不知道选谁,类型匹配失败。解决方案是用
@Primary指定主实现,或者加上@Qualifier指定Bean名字。 - 注入方式。构造器注入、字段注入、setter注入对某个参数不存在时的报错都快差不多,但查看堆栈时注意看行号。
这里给一个比较省事的排查技巧:直接在启动类里注入目标类型,启动时如果报错会明确告诉你“expected at least 1 bean which qualifies as autowire candidate”。把这句话翻译成人话:要么没有这个Bean,要么有两个候选Spring不知道选谁。大多数时候问题就是这两个。
3. Web层与AOP:接口日志、MVC路由问题的排查
3.1 用AOP实现日志记录的两种姿势
AOP日志是搜索热度很高的场景,项目里做操作日志、接口访问日志都离不开它。
最简单的做法是切所有Controller:
@Aspect @Component public class WebLogAspect { @Around("@within(org.springframework.web.bind.annotation.RestController)") public Object around(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); long cost = System.currentTimeMillis() - start; log.info("调用{}.{} 耗时{}ms", pjp.getSignature().getDeclaringTypeName(), pjp.getSignature().getName(), cost); return result; } }这种直接用@within表达式会切所有RestController,适合全局访问日志。如果你想控制粒度,可以自定义一个注解:
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface OpLog { String value() default ""; }然后在切面里用@annotation(opLog),并且把注解当作切面方法的入参传进来,就能拿到具体的操作描述。这个模式在电商类项目里非常常用,比如“用户下单”“商品上架”“删除商品”,每个方法上标一个值,日志整理起来特别清晰。
需要注意,AOP打印参数时经常会遇到序列化失败。HttpServletRequest、MultipartFile这类对象不能直接toString,必须排除掉。最简单的处理方式是把参数列表转换成JSON,但用Jackson序列化时也要过滤掉非标准对象。不然你的“日志切面”反而会成为系统的报错源。
3.2 怎么查看AOP切面到底有没有生效
这个问题比“怎么实现日志记录”更难排查。AOP明明写了却不生效,原因通常不出在语法上,而在代理机制上。
先讲怎么确认AOP有没有启用。Spring Boot环境下,只要引入spring-boot-starter-aop,自动配置会帮你启用@EnableAspectJAutoProxy。如果你想验证是否真的启用了,可以在application.yml里打开自动配置报告:
management: endpoints: web: exposure: include: conditions访问/actuator/conditions,搜索AopAutoConfiguration,如果显示POSITIVE就说明自动配置生效了。更直观的方式是断点调试:在切面里打一个断点,如果请求进入切面,那就说明一切正常。还有一个更简单的办法:在要检查的Bean上打印它的Class名称,如果名字里带CGLIB或者$Proxy,说明它已经被代理了。
真正不生效的常见原因有四个:
- 缺依赖。用了AOP,但没引
spring-boot-starter-aop。 - 切点表达式不对。
execution(* com.xx.service.*.*(..))和实际包名对不上,表达式写歪了。 - 类或方法是
final或private。Spring AOP默认用代理,代理类无法继承或覆盖final方法。 - 内部自调用。类A的方法a()调用了同类的方法b(),b()上面加了
@Around是不会触发的,因为调用发生在类内部,压根没经过代理对象。
解决自调用的标准方案有两种:注入自身实例,或者用AopContext.currentProxy()。用后者需要在启动配置里开启exposeProxy=true,让Spring把代理对象放到ThreadLocal里。但本质上这属于设计问题,真正干净的做法是把切面逻辑抽到另一个组件中,让方法调用通过代理完成。
3.3 Spring MVC的404和跨域问题,不只是路由的锅
Spring MVC最常见的两个问题:一个是你写了个@Controller方法,返回String类型,结果页面显示404;另一个是前端跨域请求被拦截。
返回String出现404,十有八九是视图解析器导致的。@Controller加方法返回String,Spring MVC会把这个字符串当成逻辑视图名,去找对应的Thymeleaf或JSP模板。找不到就报404。你要返回JSON数据,请使用@RestController,或者在方法上加@ResponseBody。这个问题我见过太多新人踩坑,重要到可以背下来:@RestController=@Controller+@ResponseBody。
跨域问题则常见于前后端分离项目,后端请求接口返回正常,但浏览器就是拦截。Spring Boot的解决方法是统一注册CORS配置:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:5173") .allowedMethods("*") .allowedHeaders("*"); } }注意allowedOrigins不能写*同时配allowCredentials(true),否则会报错。前端如果用的是Vite开发服务器,端口默认5173,后端要把这个来源加进去。另外,加了@EnableWebMvc后,Spring Boot对静态资源的默认映射全部失效,这也是很多项目里静态资源突然404的隐藏原因。
4. 数据访问:MyBatis集成与业务项目里的坑
4.1 Spring Boot + MyBatis集成必查的五个配置
有人下载“Spring Boot + MyBatis的多商户跨境商城源码”之后跑不起来,第一反应是源码问题。但绝大多数是配置没对齐。
MyBatis集成到Spring Boot,最容易被忽略的是Mapper扫描。有三种方式任选其一:在配置类上用@MapperScan("com.xxx.mapper"),在每个Mapper接口上加@Mapper,或者在MyBatis的配置里开启包扫描。没配置好,启动时就报“Invalid bound statement”。其次是XML路径:
mybatis: mapper-locations: classpath:mapper/**/*.xml第三个是驼峰映射。数据库字段是user_name,Java属性是userName,如果不开启驼峰转换,查询结果里这个字段就是null。配置如下:
mybatis: configuration: map-underscore-to-camel-case: true第四个是别名包。XML里写resultType="User"而不是com.xxx.entity.User,需要配置type-aliases-package。最后是打印SQL日志,可以用:
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这套配置加上之后,大部分“运行不了”的问题都能解决。
4.2 多商户跨境电商项目、仿天猫购物系统里的登录注册模块
“Spring Boot + MyBatis多商户跨境商城源码”和“基于Spring + Vue仿天猫购物系统”这类项目,核心都绕不开登录注册和用户管理模块。很多源码能跑通,但一旦你往里加功能就出问题,问题大多出在这两处。
第一处是密码加密。老项目里大量使用MD5加盐,但正规项目应该使用BCryptPasswordEncoder。它自带随机盐,同一个密码每次加密结果都不同,校验时用.matches(明文, 密文)。千万别在数据库里存明文密码,也不要自己写一个看起来很高端实际有漏洞的Hash算法。
第二处是多商户的数据隔离。多商户商城里有个很容易被忽略的字段:商户ID。商品表、订单表、优惠券表都有这个字段。如果你每次查询都要手动写WHERE merchant_id = ?,总有一天会漏掉一个Mapper,导致数据越权。靠谱的做法是做一个MyBatis拦截器,自动解析当前登录商户,往查询SQL里追加商户ID条件。这也是很多成熟多租户方案的核心思路。
登录注册的用户管理模块还有一个高频坑:分页。直接用PageHelper时要注意,PageHelper.startPage()必须紧跟第一条查询语句,中间不能有别的SQL操作,否则分页条件会加到错误的查询上。这个注解式分页虽然方便,但也容易让新人摸不着头脑。
4.3 大学生就业推荐系统这类CRUD项目的性能点
“基于Spring Boot的大学生就业推荐系统”是典型的毕业设计或实践项目。它看着功能多,其实就是学生、简历、岗位和推荐记录几张表之间的CRUD。
这类项目最容易出现N+1查询问题。比如查岗位列表时,每查出一个岗位又去查一次所属公司信息,循环里发SQL。解决方法是使用MyBatis的resultMap集合映射,一次联表查询把多层对象组装出来。不过联表也别上来就三表五表乱join,先看字段是否真的需要。
推荐功能也不一定要上机器学习。最简单可解释的方案是做标签匹配:把学生技能和岗位要求分别拆成标签,交集数量越多,推荐分越高。用一条带动态SQL的查询就能搞定。这类系统的数据库表结构设计要注意唯一索引,比如用户名和邮箱都加唯一约束,避免用户注册重复数据。加上数据库层面的兜底,远比在代码里做一次if判断更可靠。
5. 配置管理:端口号修改、配置优先级与占用排查
5.1 修改Spring Boot Demo端口号的五种方式
“Spring Boot修改Demo端口号”是非常基础的问题,但实际工作中你会发现端口号问题远不止写一行配置那么简单。
最直接的方式是在application.properties里写:
server.port=9090如果项目用yml,则是:
server: port: 9090第三种是启动时通过命令行参数覆盖:
java -jar demo.jar --server.port=9090第四种是设置环境变量为SERVER_PORT=9090,Spring Boot会把它映射到server.port属性。第五种是在代码里设置默认属性:
SpringApplication app = new SpringApplication(DemoApplication.class); app.setDefaultProperties(Collections.singletonMap("server.port", 9090)); app.run(args);这几种方式不是乱用的,它们之间有一定优先级。当命令行参数存在时,它优先于环境变量,环境变量又优先于application.yml。这意味着你明明改了application.yml里的端口号,但启动时还是8080,极大可能是IDEA或命令行里带了旧参数。
对于多套环境,比如本地、测试、生产,建议在配置文件里引用环境变量:
server: port: ${SERVER_PORT:8080}没有设置SERVER_PORT时就默认8080,设置了就用环境变量的值。这样代码移植性强,不会每次部署都改配置文件。
5.2 端口被占用和“改了不生效”的真相
端口被占用经常会伴随错误信息Port already in use。在Linux/Windows下,先用命令找到占用进程:
# Linux/macOS lsof -i :8080 # Windows netstat -ano | findstr 8080找到PID后杀掉进程,或者换端口。这类问题排查本身并不难,难的是“我明明改成了9090,却还在报8080被占用”。这种“改了不生效”大概率不是你改错了地方,而是启动方式或配置优先级的问题。
IDEA里有一个很隐蔽的坑:运行配置(Run/Debug Configurations)里把参数写死在Program arguments中,比如--server.port=8080。你改application.properties没用,因为命令行参数优先级更高。解决方法是编辑运行配置,把旧的端口参数删掉。同理,如果环境变量里设置了SERVER_PORT,它也会静静覆盖配置文件。
还有一种情况是Spring Cloud项目使用了bootstrap.yml而不是application.yml。在Spring Cloud早期的版本里,远端的配置优先,本地application.yml反而会被忽略。所以排查的原则很简单:先检查启动命令,再检查环境变量,最后检查配置文件层级,“不生效”的真相往往就在这一步。
6. Spring Security OAuth2、状态机与自定义校验
6.1 OAuth2 Authorization Server配置范本与常见报错
Spring Security的授权服务器在旧版本里是spring-security-oauth2,新项目建议直接用官方维护的spring-boot-starter-oauth2-authorization-server。它承担Authorization Server角色,独立于资源服务器,这个职责拆分经常让新人困惑。
一个最精简的配置包含三个Bean:SecurityFilterChain、RegisteredClientRepository和JWKSource。RegisteredClient用来定义客户端信息:
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("web-client") .clientSecret("{noop}secret") .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri("http://127.0.0.1:8080/login/oauth2/code/web-client") .scope("openid") .build();JWKSource用于签发和验证Token的密钥。用RSA KeyGenerator生成一对密钥,包装成JWKSet即可。生产环境要用持久化的RSA密钥,否则服务重启后已签发的Token全部失效。
最常见的报错有两种:一种是redirect_uri不一致。你配置的注册客户端里写的回调地址必须和请求中的redirect_uri完全一致,包括协议、端口和路径,少一个斜杠都不行,这个规则比你想的严格得多。另一种是unsupported_grant_type,请求的授权类型没有在authorizationGrantType里注册,比如用密码模式但只配置了授权码模式,自然会失败。
还有一点很关键:Authorization Server的过滤链和普通业务的过滤链要分开,用@Order明确优先级。不然/oauth2/token和你的后台接口会被相同的登录逻辑拦截,调试起来会让你怀疑人生。
6.2 Spring状态机:把复杂流程从if else里解放出来
订单状态、审批流、工单流转,这些业务用一堆if else判断也不是不能写,但状态一多就变成谁都不敢动的面条代码。Spring State Machine的价值在把状态、事件、迁移规则和动作彻底拆开。
核心概念就三个:状态、事件、迁移。用状态机描述订单就是:
StateMachineBuilder.Builder<OrderState, OrderEvent> builder = StateMachineBuilder.builder(); builder.configureStates() .withStates() .initial(OrderState.WAIT_PAY) .end(OrderState.CLOSED); builder.configureTransitions() .withExternal() .source(OrderState.WAIT_PAY) .target(OrderState.PAID) .event(OrderEvent.PAY) .and() .withExternal() .source(OrderState.PAID) .target(OrderState.CLOSED) .event(OrderEvent.CLOSE); StateMachine<OrderState, OrderEvent> machine = builder.build(); machine.start(); machine.sendEvent(OrderEvent.PAY);迁移上还可以挂Guard和Action。Guard决定这个事件在当前状态下能不能被接受,Action表示迁移成功后要执行的业务动作,比如支付成功后的发货通知。
这里有个非常容易踩的坑:状态机实例不是线程安全的,而且它自带当前状态。多个用户并发操作同一订单时,不能共用一个状态机Bean来读状态,否则会串。建议每次处理业务时从构建器重新生成状态机,或者使用StateMachinePersister把状态持久化到数据库,每次从数据库恢复。
另一个坑是状态迁移时事件不匹配。发送一个当前状态下不允许触发的事件时,状态机返回false,但不会抛异常。如果你没有监听事件,整个流程看起来就是“什么都没发生”。正确做法是要实现Listener或者在调用处显式校验machine.sendEvent(...)的返回值。
6.3 自定义Validate不触发:很多是注解目标写错了
Spring自定义校验是个很实用的功能。业务上比如校验手机号、校验身份证号,写一个自己的校验注解并不复杂。
先定义一个注解:
@Documented @Constraint(validatedBy = PhoneValidator.class) @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface Phone { String message() default "手机号格式不正确"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }再写对应的校验器:
public class PhoneValidator implements ConstraintValidator<Phone, String> { @Override public boolean isValid(String value, ConstraintValidatorContext context) { return value != null && value.matches("^1\\d{10}$"); } }然后在DTO字段上加上@Phone,并且Controller入参处必须加@Valid或@Validated:
@PostMapping("/user") public Result createUser(@Validated @RequestBody UserDTO dto) { return Result.ok(); }自定义校验不触发,第一个原因就是Controller参数上少了@Valid或@Validated。第二个原因是在Service层校验但忘了在Service类上加@Validated。想在Service方法参数上校验,类上必须有@Validated,参数上再加@Valid。
还有一个容易踩的坑:@Constraint里的validatedBy写错类名时,启动阶段不一定报错,运行期才提示找不到校验器。另外,@Target里如果没有ElementType.PARAMETER,那它就不能用在方法参数上,你会拿到一个非常隐蔽的编译错误。校验失败后的异常也不要忘了全局处理,不然前端拿到的是一个不太友好的500,而不是参数错误提示。
7. Spring Cloud微服务:从体验到快速上手的坑
7.1 快速上手的服务注册与发现配置
“Spring Cloud微服务快速上手PDF”是很多人会搜的资料,但PDF只能告诉你概念,真正上手时会卡在版本兼容和注册中心配置上。
Spring Cloud的版本和Spring Boot版本强绑定。Spring Boot 3.x要用Spring Cloud 2022.0.x以后的版本,DependencyManagement版本对应不上,启动就会报各种ClassNotFound。建议去 start.spring.io 直接选Cloud相关依赖,不要自己手拼坐标。
服务注册最朴素的一段配置长这样:
spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848一定要有spring.application.name,注册中心里显示的服务名就是它,也是后续Feign调用时的名字。如果服务注册不上,先看注册中心里面的服务列表,再去调服务提供者的/actuator/health接口确认健康状态。注册中心只要拉不到服务健康信息,就会一直把服务踢下线。
7.2 Feign调用失败和网关404的排查清单
Feign是微服务之间调用的利器,但报错时排查起来也很典型。
Feign调用失败第一件事看两端:服务提供者有没有启动?@FeignClient(name = "order-service")里的name是不是和注册中心的服务名完全一致?只看一个小写一个横杠,都会导致找不到实例。第二件事看有没有@EnableFeignClients,这个注解要放在启动类上,或者指定扫描的包路径。第三件事看负载均衡依赖,Spring Cloud 2020版本之后把Ribbon移除了,必须额外引入spring-cloud-starter-loadbalancer,否则调多实例服务时会报“No instances available”。
网关404的症结往往在路由配置。一个常见路由配置是:
spring: cloud: gateway: routes: - id: order-route uri: lb://order-service predicates: - Path=/api/order/**这里最容易被忽视的是:服务端接口实际路径是/order/info,但网关断言是/api/order/**,那就要在过滤器里重写路径,比如用StripPrefix=1去掉一层前缀。路由URI必须写成lb://服务名,表示从注册中心按负载均衡找实例;如果写成了http://localhost:8080,那就完全绕过了注册中心,还叫什么微服务。
7.3 学习资料怎么选:源码、视频还是PDF
回到“Spring实践视频”“手写Spring”这类搜索词。我的判断标准其实很简单:你正在解决的问题类型决定了该看什么资料。
学习新框架,直接跑一个完整项目源码,比任何视频都快。网上的多商户商城源码、仿天猫购物系统源码,下载后先不看代码,而是看README、看application.yml,把项目拉起来再说。能把一个别人的项目在自己电脑上跑通,胜过你刷十节视频课。
遇到某个具体技术点不懂,比如AOP为什么没执行、OAuth2为什么报错,这种时候适合看短视频或文章,因为问题太具体,长视频反而浪费时间。PDF适合系统化阅读,比如Spring Cloud架构设计、状态机原理,这类偏基础理论的内容用PDF或官方文档来啃更合适。
还有一类资料是源码本身。比如“手写Spring”的教程,哪怕你只看懂一小段,也会对Bean生命周期豁然开朗。资料不是越贵越好,而是越贴合你当前的问题越好。我自己的习惯是:项目跑不起来的时候不读书,先查配置;配置对了代码还是不对时,再去看原理书,带着问题读书才有效果。
上面这些排查方式,是我在实际项目里反复用过很多遍的路径:环境、Bean、AOP、配置、数据、安全、微服务,按这条线走一遍,大部分Spring问题都不会纠缠太久。养成这个顺序习惯之后,你再碰到新问题,第一反应会是“我去确认一下具体配置”,而不是“我又要开始玄学调参了”。