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服务器端,
 * 注解的值将被用于监听用户连接的终端访问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>
     * &#64;ServerEndpoint("/chat")
     * &#64;ServerEndpoint("/chat/{user}")
     * &#64;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);
    }
}

处理会能获取到注解了,服务也能正常启动了
在这里插入图片描述

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Boot使用AOP监听注解可以通过以下步骤实现: 1. 在`pom.xml`文件添加`spring-boot-starter-aop`依赖,以使用Spring AOP。 2. 创建一个注解类,例如`@MyAnnotation`,用于标记需要被监听的方法。 3. 创建一个切面类,用于监听被`@MyAnnotation`标记的方法。可以使用`@Aspect`注解来标记这个类。 4. 在切面类定义一个切点,用于匹配被`@MyAnnotation`标记的方法。可以使用`@Pointcut`注解来定义切点。 5. 在切面类定义一个通知,用于在匹配到切点时执行某些操作。可以使用`@Before`、`@After`、`@Around`等注解来定义通知。 6. 在Spring Boot应用程序的主类添加`@EnableAspectJAutoProxy`注解,以启用AOP功能。 以下是一个示例代码: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyAnnotation { } @Aspect @Component public class MyAspect { @Pointcut("@annotation(com.example.demo.MyAnnotation)") public void myAnnotationPointcut() {} @Before("myAnnotationPointcut()") public void beforeMyAnnotation() { System.out.println("Before My Annotation"); } } @EnableAspectJAutoProxy @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ``` 在上面的示例,`@MyAnnotation`注解用于标记需要被监听的方法,在`MyAspect`切面类定义了一个切点`myAnnotationPointcut`,用于匹配被`@MyAnnotation`标记的方法,在`beforeMyAnnotation`方法定义了一个前置通知,在匹配到切点时执行。在`DemoApplication`主类添加了`@EnableAspectJAutoProxy`注解,以启用AOP功能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值