一.问题背景

博主所在的业务组近期做架构升级。引入统一的基础工程模块,在其他业务模块引入都正常启动运行的情况下,其中一个拥有聊天室功能【使用websocket实现】的业务包怎么也启动不了。报错如下

Caused by: javax.websocket.DeploymentException: Cannot deploy POJO class [com.xxxx.service.impl.ChatWebSocketImpl$$EnhancerBySpringCGLIB$$7792c1b8] as it is not annotated with @ServerEndpoint
at org.apache.tomcat.websocket.server.WsServerContainer.addEndpoint(WsServerContainer.java:245)
at org.apache.tomcat.websocket.server.WsServerContainer.addEndpoint(WsServerContainer.java:228)
at org.springframework.web.socket.server.standard.ServerEndpointExporter.registerEndpoint(ServerEndpointExporter.java:156)
... 11 common frames omitted
复制代码

二.寻找问题

当时出现这个问题,我第一反应是,淦,websocket是不是有问题,怎么其他应用都好好的,就它不行。


一次springboot启动失败排雷之路_sed 然后立马告诉运转我的聪明小脑瓜。好吧,其实是一顿google操作。

一次springboot启动失败排雷之路_spring_02

在网上搜索到了如下答案​

大致意思就是标注了@ServerEndpoint的websocket的类,不能被AOP所代理。

一次springboot启动失败排雷之路_java_03

心里美滋滋,立马commad+shift+F全局所有@Aspect,然后就这么惊喜,这么意外,啥也没有。

一次springboot启动失败排雷之路_Spring Boot_04

我当时以为我的打开方式变了,然后怀疑是不是我的依赖产生了冲突,然后使用maven-helper一看没有冲突。

一次springboot启动失败排雷之路_sed_05

然后我还在一个demo工程里面引入了websocket,再引入新架构的基础工程,启动,demo工程直接启动了。。。

我直接呆住。然后我按照堆栈的报错将断点打在了此处!

一次springboot启动失败排雷之路_spring_06

发现业务模块在这里annotation就是为null,demo工程annotation就是有值的。

三.定位问题

后面我不信邪的在demo工程里面写了一个AOP切面,并且将切点定义在websocket的服务类,发现启动也失败了。好家伙,瞬间脑子清醒了。我们工程里面没有AOP,还不允许外部框架引入切面生成代理类了?


PS:关于外部框架自动配置注入大家可以自行百度,主要是通过配置/META-INF/spring.factories文件实现。


为了找到我们webSocket这个配置类的代理生成路径,这里就要提到spring中bean的生命周期。

spring的生命周期主要分为4个部分


实例化->属性赋值->初始化->销毁


生成AOP代理发生在初始化阶段的BeanPostProcessor后置处理器中的AnnotationAwareAspectJAutoProxyCreatorpostProcessAfterInitialization

代码如下

/**
* Create a proxy with the configured interceptors if the bean is
* identified as one to proxy by the subclass.
* @see #getAdvicesAndAdvisorsForBean
*/
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
复制代码

进入关键方法wrapIfNecessary

* Wrap the given bean if necessary, i.e. if it is eligible for being proxied.
* @param bean the raw bean instance
* @param beanName the name of the bean
* @param cacheKey the cache key for metadata access
* @return a proxy wrapping the bean, or the raw bean instance as-is
*/
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
复制代码

根据注释找到生成代理的方法为

reateProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean))
复制代码

在此处打上断点,这里有个小技巧,如果直接打断点在此处,可能会导致所有被切面切到的类都在此处进入断点,idea中可以设置条件断点,因为我们知道bean的名称,则将beanName作为判断条件进入此断点

一次springboot启动失败排雷之路_sed_07

发现了这个三方的aop代理类org.springframework.cloud.sleuth.instrument.scheduling.TraceSchedulingAspect

里面的切点定义如下

@Around("execution (@org.springframework.scheduling.annotation.Scheduled  * *.*(..))")
public Object traceBackgroundThread(final ProceedingJoinPoint pjp) throws Throwable {
//不重要代码忽略
}
复制代码

发现只要是spring的定时任务注解所标注的类与方法都会被切面给切到。

顺着这个思路在websocket对应的业务实现类一搜索这个注解果然发现了如下代码

@Scheduled(cron = "*/20 * * * * ?")
public void run(){
//业务逻辑忽略
}
复制代码

四.解决问题

知道问题点在哪里了,就要去解决它,知道这个问题后,我去看了一下TraceSchedulingAspect这个切面类,发现上面已经被标注为**@Deprecated**,将在高版本不被推荐使用。然后点击这个类发现它会被TraceSchedulingAutoConfiguration这个自动配置类给加载到,并且这个自动配置类也被标注了**@Deprecated**。

代码如下

@Deprecated
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "org.aspectj.lang.ProceedingJoinPoint")
@ConditionalOnProperty(value = "spring.sleuth.scheduled.enabled", matchIfMissing = true)
@ConditionalOnBean(Tracing.class)
@AutoConfigureAfter(TraceAutoConfiguration.class)
@EnableConfigurationProperties(SleuthSchedulingProperties.class)
public class TraceSchedulingAutoConfiguration {

@Bean
public TraceSchedulingAspect traceSchedulingAspect(Tracer tracer,
SleuthSchedulingProperties sleuthSchedulingProperties) {
return new TraceSchedulingAspect(tracer,
Pattern.compile(sleuthSchedulingProperties.getSkipPattern()));
}

}


看到这@ConditionalOnProperty(value = "spring.sleuth.scheduled.enabled", matchIfMissing = true),想必大家思路都很清晰了,我在配置文件中增加了配置spring.sleuth.scheduled.enabled=false,应用就可以启动成功了。

五.总结

其实日常业务开发中还是不太多会遇到类似的这种框架型冲突的问题,但是在遇到此类问题的时候,合理利用google资源与自己的源码知识能力定位bug还是很重要的。

一次springboot启动失败排雷之路_java_08

六.联系我