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 CopyOnWriteArraySetwebSocketSet = 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); } }
处理会能获取到注解了,服务也能正常启动了
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)