springboot中在使用aop时,会使用动态代理,如果此时再获取被代理的类上的注解会导致获取失败。
比如使用websocket时候如果在方法上使用aop就会出现问题。
1、问题复现
下面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服务器端,
* 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
*/
@ServerEndpoint("/websocket")
@Component
public class WebsocketBean {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
private static CopyOnWriteArraySet<WebsocketBean> webSocketSet = new CopyOnWriteArraySet<WebsocketBean>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 连接建立成功调用的方法
* @param 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());
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@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;
}
}
}
/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
System.out.println("发生错误");
error.printStackTrace();
}
/**
* 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
* @param message
* @throws IOException
*/
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 {
/**
* The URI or URI-template, level-1 (<a href="http://tools.ietf.org/html/rfc6570">See RFC 6570</a>) where the
* endpoint will be deployed. The URI us relative to the root of the web socket container and must begin with a
* leading "/". Trailing "/"'s are ignored. Examples:
*
* <pre>
* <code>
* @ServerEndpoint("/chat")
* @ServerEndpoint("/chat/{user}")
* @ServerEndpoint("/booking/{privilege-level}")
* </code>
* </pre>
*
* @return the URI or URI-template
*/
public String value();
/**
* The ordered array of web socket protocols this endpoint supports. For example, {"superchat", "chat"}.
*
* @return the subprotocols.
*/
public String[] subprotocols() default {};
/**
* The ordered array of decoder classes this endpoint will use. For example, if the developer has provided a
* MysteryObject decoder, this endpoint will be able to receive MysteryObjects as web socket messages. The websocket
* runtime will use the first decoder in the list able to decode a message, ignoring the remaining decoders.
*
* @return the decoders.
*/
public Class<? extends Decoder>[] decoders() default {};
/**
* The ordered array of encoder classes this endpoint will use. For example, if the developer has provided a
* MysteryObject encoder, this class will be able to send web socket messages in the form of MysteryObjects. The
* websocket runtime will use the first encoder in the list able to encode a message, ignoring the remaining
* encoders.
*
* @return the encoders.
*/
public Class<? extends Encoder>[] encoders() default {};
/**
* The optional custom configurator class that the developer would like to use to further configure new instances of
* this endpoint. If no configurator class is provided, the implementation uses its own. The implementation creates
* a new instance of the configurator per logical endpoint.
*
* @return the custom configuration class, or ServerEndpointConfig.Configurator.class if none was set in the
* annotation.
*/
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;
/**
* @author zhuquanwen
* @vesion 1.0
* @date 2021/12/28 20:58
* @since jdk1.8
*/
@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);
}
}
处理会能获取到注解了,服务也能正常启动了