springboot学习(六十四) 解决springboot中aop使用了cglib代理导致注解丢失引发的问题

springboot学习(六十四) 解决springboot中aop使用了cglib代理导致注解丢失引发的问题,第1张

springboot学习(六十四) 解决springboot中aop使用了cglib代理导致注解丢失引发的问题

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")
@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[] decoders() default {};

    
    public Class[] encoders() default {};

    
    public Class 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);
    }
}

处理会能获取到注解了,服务也能正常启动了

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5683819.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存