1、问题复现springboot中在使用aop时,会使用动态代理,如果此时再获取被代理的类上的注解会导致获取失败。
比如使用websocket时候如果在方法上使用aop就会出现问题。
下面websocket类中使用了@ServerEndpoint注解,并在@OnOpen方法上添加了一个自定义注解@LogRecord,这个自定义注解会使用aop,从而会复现问题。
package com.iscas.biz.config;
import com.iscas.biz.config.log.LogRecord;
import com.iscas.biz.config.log.LogType;
import com.iscas.biz.config.log.OperateType;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/websocket")
@Component
public class WebsocketBean {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
@OnOpen
@LogRecord(type = LogType.AUTH, desc = "", operateType = OperateType.add)
public void onOpen(Session session){
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
}
@OnClose
public void onClose(){
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
@OnMessage
@LogRecord(type = LogType.AUTH, desc = "", operateType = OperateType.add)
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);
//群发消息
for(WebsocketBean item: webSocketSet){
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}
@OnError
public void onError(Session session, Throwable error){
System.out.println("发生错误");
error.printStackTrace();
}
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebsocketBean.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebsocketBean.onlineCount--;
}
}
启动服务会发现服务已无法启动,报错信息如下:
2021-12-28 22:02:42.242 [main] INFO [org.springframework.boot.autoconfigure.logging.ConditionevaluationReportLoggingListener:136] - Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2021-12-28 22:02:42.315 [main] ERROR [org.springframework.boot.SpringApplication:819] - Application run failed java.lang.IllegalStateException: Failed to register @ServerEndpoint class: class com.iscas.biz.config.WebsocketBean$$EnhancerBySpringCGLIB$$a6156046 at org.springframework.web.socket.server.standard.ServerEndpointExporter.registerEndpoint(ServerEndpointExporter.java:159) ~[spring-websocket-5.3.14.jar:5.3.14] at org.springframework.web.socket.server.standard.ServerEndpointExporter.registerEndpoints(ServerEndpointExporter.java:134) ~[spring-websocket-5.3.14.jar:5.3.14] at org.springframework.web.socket.server.standard.ServerEndpointExporter.afterSingletonsInstantiated(ServerEndpointExporter.java:112) ~[spring-websocket-5.3.14.jar:5.3.14] at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:972) ~[spring-beans-5.3.14.jar:5.3.14] at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.14.jar:5.3.14] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.14.jar:5.3.14] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.6.2.jar:2.6.2] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730) ~[spring-boot-2.6.2.jar:2.6.2] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:412) ~[spring-boot-2.6.2.jar:2.6.2] at org.springframework.boot.SpringApplication.run(SpringApplication.java:302) ~[spring-boot-2.6.2.jar:2.6.2] at com.iscas.biz.BizApp.main(BizApp.java:80) ~[classes/:?] Caused by: javax.websocket.DeploymentException: UT003027: Class class com.iscas.biz.config.WebsocketBean$$EnhancerBySpringCGLIB$$a6156046 was not annotated with @ClientEndpoint or @ServerEndpoint at io.undertow.websockets.jsr.ServerWebSocketContainer.addEndpointInternal(ServerWebSocketContainer.java:735) ~[undertow-websockets-jsr-2.2.14.Final.jar:2.2.14.Final] at io.undertow.websockets.jsr.ServerWebSocketContainer.addEndpoint(ServerWebSocketContainer.java:628) ~[undertow-websockets-jsr-2.2.14.Final.jar:2.2.14.Final] at org.springframework.web.socket.server.standard.ServerEndpointExporter.registerEndpoint(ServerEndpointExporter.java:156) ~[spring-websocket-5.3.14.jar:5.3.14] ... 10 more 2021-12-28 22:02:42.325 [main] INFO [com.iscas.base.biz.config.health.DefaultHealthCheckHandler:23] - 健康检测-readiness-检测失败-服务未准备好或即将关闭 2021-12-28 22:02:42.337 [main] INFO [com.atomikos.icatch.imp.TransactionServiceImp:28] - Transaction Service: Entering shutdown (false, 9223372036854775807)... 2021-12-28 22:02:42.345 [main] INFO [org.springframework.scheduling.quartz.SchedulerFactoryBean:847] - Shutting down Quartz Scheduler 2021-12-28 22:02:42.345 [main] INFO [org.quartz.core.QuartzScheduler:666] - Scheduler quartzScheduler_$_NON_CLUSTERED shutting down. 2021-12-28 22:02:42.345 [main] INFO [org.quartz.core.QuartzScheduler:585] - Scheduler quartzScheduler_$_NON_CLUSTERED paused. 2021-12-28 22:02:42.346 [main] INFO [org.quartz.core.QuartzScheduler:740] - Scheduler quartzScheduler_$_NON_CLUSTERED shutdown complete. Disconnected from the target VM, address: '127.0.0.1:64213', transport: 'socket'
从报错的源码中寻找会发现时获取@ServerEndpoint注解为空造成的
会发现此时endpint时cglib代理对象,从cglib代理对象上是获取不到ServerPoint注解的,其实如果调用Spring的AnnotationUtils.findAnnotation会可以获取到代理对象的注解的,它的实现有缺陷吧,只能想办法改进了。
2、问题修复要修复此问题首先要了解为什么获取不到注解,通过现象我们知道这是因为cglib代理后对象已不是原来的对象,所以无法从Class中获取@ServerPoint。CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类,新生成的类是原来类的子类。
关键点在于它是一个子类,为什么没有自动继承父类的注解呢,我们翻看一下@ServerPoint注解的源码:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ServerEndpoint {
public String value();
public String[] subprotocols() default {};
public Class extends Decoder>[] decoders() default {};
public Class extends Encoder>[] encoders() default {};
public Class extends ServerEndpointConfig.Configurator> configurator() default ServerEndpointConfig.Configurator.class;
}
注意头部的注解,它不支持注解的继承,如果想让子类继承父类的注解,需要使用一个@Inherited,问题找到了,如果这个@ServerPoint中有这个注解应该就没问题了。
怎么来让@ServerPoint支持继承呢?
如果是自定义的注解,很容易办,但@ServerPoint是第三方包里的,改源码?改动量很大,关联处理的地方太多。可不可以在服务启动时候通过反射来修改一下呢?在什么时机修改呢?
最后决定在@BeanProcessor的postProcessBeforeInitialization中通过反射修改注解,postProcessBeforeInitialization中还能获取到未代理前的对象,可以在此反射添加Inheited。
具体实现如下:
package com.iscas.biz.config;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import javax.websocket.server.ServerEndpoint;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Objects;
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
handleServerEndPoint(bean);
return bean;
}
private void handleServerEndPoint(Object bean) {
//获取serverEndpoint
ServerEndpoint serverEndpoint = AnnotationUtils.findAnnotation(bean.getClass(), ServerEndpoint.class);
if (!Objects.isNull(serverEndpoint)) {
//设置@ServerEndpoint注解支持继承,相当于注解@Inherited,应对动态代理导致类上的@ServerEndpoint注解丢失
InvocationHandler h = Proxy.getInvocationHandler(serverEndpoint);
try {
Field typeField = h.getClass().getDeclaredField("type");
typeField.setAccessible(true);
Field annotationTypeField = Class.class.getDeclaredField("annotationType");
annotationTypeField.setAccessible(true);
Object o = annotationTypeField.get(typeField.get(h));
Field inheritedField = o.getClass().getDeclaredField("inherited");
this.updateFinalModifiers(inheritedField);
inheritedField.set(o, true);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("修改@ServerEndPoint注解失败");
}
}
}
private void updateFinalModifiers(Field field) throws NoSuchFieldException, IllegalAccessException {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
}
处理会能获取到注解了,服务也能正常启动了



