成员变量为动态数据时不可轻易使用

问题描述

业务验收阶段,遇到了一个由于成员变量导致的线程问题

有一个kafka切面,用来处理某些功能在调用前后的发送消息,资产类型type是成员变量定义;

资产1类型推送消息是以zichan1为节点;资产2类型推送消息是以zichan2为节点;

当多个线程调用切面类时,由于切面类中使用成员变量且为动态数据时,此时会出现根据资产类型推送消息错误;例如资产1在调用功能时,切面类的type字段为zichan1;同时有资产2调用功能时,此时的切面类的type字段为zichan2;导致资产1在调用功能前推送的是zichan1,在调用功能后推送的是zichan2的消息标识。

原因

当多个线程同时调用时,成员变量则会只采用最后一次调用的值。

下面简单描述下情景:

kafkaAspect类

java 复制代码
import com.alibaba.fastjson.JSON;
import com.example.demo.constant.StageCodeConstant;
import com.example.demo.entity.KafkaSendMessageConstant;
import com.example.demo.util.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Slf4j
public class KafkaProcessAspect {

    private static final Logger log = LoggerFactory.getLogger(KafkaProcessAspect.class);

    private String assetType = "";
    private String startTime = "";

    @Around("@annotation(kafkaProcess)")
    public Object doAround(ProceedingJoinPoint joinPoint, KafkaProcess kafkaProcess) throws Throwable {
        Object object = null;
        assetType = (String) getParamValue(joinPoint, "assetType");
        startTime = DateUtils.getTime();
        object = joinPoint.proceed();
        //推送消息
        String stageCode = StageCodeConstant.STAGE_CODE.get(assetType).get(kafkaProcess.functionName());
        KafkaSendMessageConstant messageConstant = new KafkaSendMessageConstant();
        messageConstant.setStageCode(stageCode);
        messageConstant.setStartTime(startTime);
        messageConstant.setEndTime(DateUtils.getTime());
        log.info("资产类型{}推送消息:{}", assetType, JSON.toJSONString(messageConstant));
        return object;
    }

    private Object getParamValue(ProceedingJoinPoint joinPoint, String paramName) {
        Object[] params = joinPoint.getArgs();
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();

        if (parameterNames == null || parameterNames.length == 0) {
            return null;
        }
        for (String param : parameterNames) {
            if (param.equals(paramName)) {
                int index = ArrayUtils.indexOf(parameterNames, param);
                return params[index];
            }
        }
        return null;
    }

}

由于controller中的请求地址是采用占位符定义,后使用@PathVariable可以获取的类型

Controller类

java 复制代码
import com.example.demo.aop.KafkaProcess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    private static final Logger logger = LoggerFactory.getLogger(TestController.class);

    @PostMapping("/{assetType}/cmpt")
    @KafkaProcess(functionName = "JS")
    public void cmpt(@PathVariable("assetType") String assetType) {
        logger.info("{}接口开始", assetType);
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 20000; i++) {
            for (int j = 0; j < 20000; j++) {
                int m = i * j;
                logger.debug("i*j={}",m);
            }
        }

        long endTime = System.currentTimeMillis();
        logger.info("{}接口结束,耗时{}", assetType, endTime - startTime);
    }

}

资产类型枚举 AssetTypeEnum

java 复制代码
public enum AssetTypeEnum {

    ZICHAN_1("zichan1","资产1"),
    ZICHAN_2("zichan2","资产2");

    // 成员变量
    private String code;

    private String desc;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    // 构造方法
    AssetTypeEnum(String code,String desc) {
        this.code = code;
        this.desc = desc;
    }

}

推送消息 KafkaSendMessageConstant

java 复制代码
public class KafkaSendMessageConstant {

    private String stageCode;
    private String startTime;
    private String endTime;

    public String getStageCode() {
        return stageCode;
    }

    public void setStageCode(String stageCode) {
        this.stageCode = stageCode;
    }

    public String getStartTime() {
        return startTime;
    }

    public void setStartTime(String startTime) {
        this.startTime = startTime;
    }

    public String getEndTime() {
        return endTime;
    }

    public void setEndTime(String endTime) {
        this.endTime = endTime;
    }
}

节点常量类 StageCodeConstant ;根据不同资产类型赋值不同功能的推送标识

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class StageCodeConstant {

    public static final Map<String, Map<String, String>> STAGE_CODE = new HashMap<>();

    static {
        //资产1
        STAGE_CODE.put(AssetTypeEnum.ZICHAN_1.getCode(),
                new HashMap<String, String>() {{
                    put("JS", "ZICHAN1-JS");
                }});
        //资产2
        STAGE_CODE.put(AssetTypeEnum.ZICHAN_2.getCode(),
                new HashMap<String, String>() {{
                    put("JS", "ZICHAN2-JS");
                }});
    }
}

用postman调用资产2接口

后立即调用资产1接口

此时出现这种结果:

会发现,调用资产2的时候发送消息还是资产1的信息;然后资产1发送的消息也是资产1的信息

解决

此时有两个解决办法,一个是将doAround()和其他方法合并为一个方法,将成员变量调整为局部变量;另一个则为将该成员变量设置为一个对象,对这个对象进行线程设置,保证doAround()和doBefore()获取的是同一个对象的数据

解决方法一

java 复制代码
import com.alibaba.fastjson.JSON;
import com.example.demo.constant.StageCodeConstant;
import com.example.demo.entity.KafkaSendMessageConstant;
import com.example.demo.util.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Slf4j
public class KafkaProcessAspect {

    private static final Logger log = LoggerFactory.getLogger(KafkaProcessAspect.class);

    @Around("@annotation(kafkaProcess)")
    public Object doAround(ProceedingJoinPoint joinPoint, KafkaProcess kafkaProcess) throws Throwable {
        Object object = null;
        String assetType = (String) getParamValue(joinPoint, "assetType");
        String startTime = DateUtils.getTime();
        object = joinPoint.proceed();
        //推送消息
        String stageCode = StageCodeConstant.STAGE_CODE.get(assetType).get(kafkaProcess.functionName());
        KafkaSendMessageConstant messageConstant = new KafkaSendMessageConstant();
        messageConstant.setStageCode(stageCode);
        messageConstant.setStartTime(startTime);
        messageConstant.setEndTime(DateUtils.getTime());
        log.info("资产类型{}推送消息:{}", assetType, JSON.toJSONString(messageConstant));
        return object;
    }

    private Object getParamValue(ProceedingJoinPoint joinPoint, String paramName) {
        Object[] params = joinPoint.getArgs();
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();

        if (parameterNames == null || parameterNames.length == 0) {
            return null;
        }
        for (String param : parameterNames) {
            if (param.equals(paramName)) {
                int index = ArrayUtils.indexOf(parameterNames, param);
                return params[index];
            }
        }
        return null;
    }

}

解决方法二

成员变量KafkaSingleDTO

java 复制代码
public class KafkaSingleDTO {

    private String assetType="";
    private String date="";

    public String getAssetType() {
        return assetType;
    }

    public void setAssetType(String assetType) {
        this.assetType = assetType;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }
}

对象单实例获取 KafkaSingleUtil

java 复制代码
import com.example.demo.entity.KafkaSingleDTO;

public class KafkaSingleUtil {

    private static ThreadLocal<KafkaSingleDTO> START = new ThreadLocal<>();

    public static KafkaSingleDTO getObject() {
        KafkaSingleDTO singleDTO = START.get();
        if (singleDTO == null) {
            singleDTO = new KafkaSingleDTO();
        }
        return singleDTO;
    }

}

kafkaAsspect拦截器

java 复制代码
import com.alibaba.fastjson.JSON;
import com.example.demo.constant.StageCodeConstant;
import com.example.demo.entity.KafkaSendMessageConstant;
import com.example.demo.entity.KafkaSingleDTO;
import com.example.demo.util.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Slf4j
public class KafkaProcessAspect {

    private static final Logger log = LoggerFactory.getLogger(KafkaProcessAspect.class);

    private static String assetType="";
    private static String startTime="";

    @Around("@annotation(kafkaProcess)")
    public Object doAround(ProceedingJoinPoint joinPoint, KafkaProcess kafkaProcess) throws Throwable {
        Object object = null;
        assetType = (String) getParamValue(joinPoint, "assetType");
        startTime = DateUtils.getTime();
        KafkaSingleDTO singleDTO = KafkaSingleUtil.getObject();
        singleDTO.setAssetType(assetType);
        singleDTO.setDate(startTime);

        object = joinPoint.proceed();
        //推送消息--方法调用后
        String stageCode = StageCodeConstant.STAGE_CODE.get(singleDTO.getAssetType()).get(kafkaProcess.functionName());
        KafkaSendMessageConstant messageConstant = new KafkaSendMessageConstant();
        messageConstant.setStageCode(stageCode);
        messageConstant.setStartTime(singleDTO.getDate());
        messageConstant.setEndTime(DateUtils.getTime());
        log.info("资产类型{}方法后推送消息:{}", singleDTO.getAssetType(), JSON.toJSONString(messageConstant));
        return object;
    }

    @Before("@annotation(kafkaProcess)")
    public void doBefore(KafkaProcess kafkaProcess){
        //推送消息--方法调用前
        KafkaSingleDTO singleDTO = KafkaSingleUtil.getObject();
        singleDTO.setAssetType(assetType);
        String stageCode = StageCodeConstant.STAGE_CODE.get(singleDTO.getAssetType()).get(kafkaProcess.functionName());
        KafkaSendMessageConstant messageConstant = new KafkaSendMessageConstant();
        messageConstant.setStageCode(stageCode);
        //...消息实体类 可自补充
        log.info("资产类型{}方法前推送消息:{}", singleDTO.getAssetType(), JSON.toJSONString(messageConstant));
    }

    private Object getParamValue(ProceedingJoinPoint joinPoint, String paramName) {
        Object[] params = joinPoint.getArgs();
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();

        if (parameterNames == null || parameterNames.length == 0) {
            return null;
        }
        for (String param : parameterNames) {
            if (param.equals(paramName)) {
                int index = ArrayUtils.indexOf(parameterNames, param);
                return params[index];
            }
        }
        return null;
    }
}

最终结果:

到此结束!

相关推荐
迷糊的『迷』1 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
小池先生2 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
苹果醋33 小时前
2020重新出发,MySql基础,MySql表数据操作
java·运维·spring boot·mysql·nginx
小蜗牛慢慢爬行3 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
azhou的代码园3 小时前
基于JAVA+SpringBoot+Vue的制造装备物联及生产管理ERP系统
java·spring boot·制造
wm10434 小时前
java web springboot
java·spring boot·后端
路在脚下@11 小时前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
啦啦右一11 小时前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien11 小时前
Spring Boot常用注解
java·spring boot·后端
苹果醋313 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx