AOP封装进行批量的数据查询并填充

在我们日常的项目开发中,我们经常会遇到这样的问题。我们有一张用户表,用户表中有用户ID和用户名称。我们其他表中会记录我们当前操作人的ID,一般,我们会记录一个创建人ID和修改人ID。那么,这个时候问题来了,我们在查询数据的时候,怎样优雅的根据用户ID来查询出相应的用户名称,并且填充到我们返回给前端的数据中。

我们每次在查询数据的时候,都与用户表进行关联查询吗?每次根据创建人的ID和修改人的ID来查询出用户姓名,然后填充到我们返回的数据中?这样当然是可以的,但是,这种重复性的代码,我们为什么不能写一个AOP封装,通过一个注解的形式来进行实现呢?那么,废话不多说,直接上代码;

1、填充创建人和修改人用户

我再来了解一下我们的需求。现在有一张用户表,表中有用户ID和用户名称;然后,我们现在每张表中都有创建人ID和修改人ID,我们想要查询数据的时候,根据创建人ID和修改人ID查询出相应的创建人姓名和修改人姓名,并且把这两个名称回填到我们方法返回值中。我们使用AOP的环绕通知来实现这个功能。

首先,我们要定义一个注解,这个注解要放在我们返回的实体对象中。指定要映射的字段名称,注意这里的字段名称一定要有规律才行。如(createUserId----->createUserName 、updateUserId----->updateUserName )

package com.scmpt.framework.aop.query.user;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * @Author 张乔
 * @Date 2025/03/20 12:59
 * @Description 自定义的 AOP 注解,用于根据用户ID自动查询用户Grpc填充用户名称
 * 使用时加在返回的视图类上
 * 若字段命名不符合默认规则(如 creator_uid → creator_name),可自定义注解参数:
 * @AutoFillUserInfo(
 * userIdSuffix = "Uid",  // 匹配字段如 creatorUid
 *  userNameSuffix = "Name" // 对应字段如 creatorName
 *  )
 * public class CustomDTO {
 * private Long creatorUid;
 * private String creatorName;
 *}
 * @Version 1.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFillUserInfo {
    /**
     * 指定 UserId 字段的命名模式(默认后缀为 "UserId")
     */
    String userIdSuffix() default "UserId";
    
    /**
     * 指定 UserName 字段的命名模式(默认后缀为 "UserName")
     */
    String userNameSuffix() default "UserName";
}

在自定义一个注解,用来标识,我们那些方法需要字段填充;

package com.scmpt.framework.aop.query.user;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author 张乔
 * @Date 2025/03/20 12:59
 * @Description 自定义的 AOP 注解,用于根据用户ID自动查询用户Grpc填充用户名称
 * @Version 1.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFillUserNameByUserId {
}

最后,实现一个切面通知,并且,绑定我们的方法注解。

package com.scmpt.framework.aop.query.user;

import com.scmpt.framework.core.web.response.ResultData;
import com.scmpt.user.grpc.userInfo.UserInfoDataProto;
import com.scmpt.user.grpc.userInfo.UserInfoListDataProto;
import com.scmpt.user.grpc.userInfo.UserInfoQueryProto;
import com.scmpt.user.grpc.userInfo.UserInfoServiceGrpc;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @Author 张乔
 * @Date 2025/03/20 12:59
 * @Description 自定义的 AOP 注解,用于根据用户ID自动查询用户Grpc填充用户名称
 * @Version 1.0
 */

@Aspect
@Slf4j
public class UserInfoAutoFillAspect {

    @GrpcClient("scmpt-users")
    private UserInfoServiceGrpc.UserInfoServiceBlockingStub userInfoServiceBlockingStub;

    @Around("@annotation(AutoFillUserNameByUserId)")
    public Object autoFillUserInfo(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();
//        log.info("进入切面");
//        long startTime = System.currentTimeMillis(); // 记录方法开始时间
//        processResult(result);
//        log.info("切面执行完毕");
//        long endTime = System.currentTimeMillis(); // 记录方法结束时间
//        long executionTime = endTime - startTime; // 计算执行时间(毫秒)
//        log.info("方法执行时间为---->{} ms", executionTime);
        log.info("进入用户信息切面");
        long startTime = System.currentTimeMillis();
        try {
            processResult(result);
        } catch (Exception e) {
            log.error("用户信息切面执行异常", e);
        }
        finally {
            log.info("用户信息切面执行完毕,耗时:{} ms", System.currentTimeMillis() - startTime);
        }
        return result;
    }

    private void processResult(Object result) {
        if (result == null) return;

        // 1. 收集所有需要处理的对象
        List targetObjects = new ArrayList<>();
        collectTargetObjects(result, targetObjects);

        // 2. 批量收集创建人ID和修改人ID
        Set creatorIds = new HashSet<>();
        Set updaterIds = new HashSet<>();
        Map> creatorMappings = new HashMap<>();
        Map> updaterMappings = new HashMap<>();

        for (Object obj : targetObjects) {
            Class clazz = obj.getClass();
            AutoFillUserInfo annotation = clazz.getAnnotation(AutoFillUserInfo.class);
            if (annotation == null) continue;

            String userIdSuffix = annotation.userIdSuffix();
            String userNameSuffix = annotation.userNameSuffix();

            // 遍历所有字段,识别创建人/修改人字段
            Arrays.stream(clazz.getDeclaredFields())
                    .filter(field -> field.getName().endsWith(userIdSuffix))
                    .forEach(field -> processField(obj, field, userIdSuffix, userNameSuffix,
                            creatorIds, updaterIds, creatorMappings, updaterMappings));
        }

        // 3. 批量查询用户信息(两次gRPC调用)
        Map creatorNameMap = !creatorIds.isEmpty() ?
                queryUserNames(creatorIds) : Collections.emptyMap();

        Map updaterNameMap = !updaterIds.isEmpty() ?
                queryUserNames(updaterIds) : Collections.emptyMap();

        // 4. 批量填充用户名称
        fillUserNames(creatorMappings, creatorNameMap);
        fillUserNames(updaterMappings, updaterNameMap);
    }

    /**
     * 递归收集所有需要处理的对象
     */
    private void collectTargetObjects(Object result, List targetObjects) {
        if (result instanceof ResultData) {
            try {
                Field rowsField = result.getClass().getDeclaredField("rows");
                rowsField.setAccessible(true);
                Object rows = rowsField.get(result);
                collectTargetObjects(rows, targetObjects);
            } catch (Exception e) {
                log.error("ResultData rows字段访问失败", e);
            }
        } else if (result instanceof List) {
            for (Object item : (List) result) {
                collectTargetObjects(item, targetObjects);
            }
        } else if (result != null) {
            targetObjects.add(result);
        }
    }

    /**
     * 处理单个字段,识别创建人/修改人字段
     */
    private void processField(Object obj, Field userIdField,
                              String userIdSuffix, String userNameSuffix,
                              Set creatorIds, Set updaterIds,
                              Map> creatorMappings,
                              Map> updaterMappings) {
        try {
            userIdField.setAccessible(true);
            Long userId = (Long) userIdField.get(obj);
            if (userId == null) return;

            // 根据字段前缀判断类型(create/update)
            String fieldName = userIdField.getName();
            String prefix = fieldName.replace(userIdSuffix, "");
            boolean isCreatorField = prefix.toLowerCase().contains("create");
            boolean isUpdaterField = prefix.toLowerCase().contains("update");

            // 计算对应的用户名字段名
            String userNameFieldName = prefix + userNameSuffix;
            Field userNameField = obj.getClass().getDeclaredField(userNameFieldName);
            userNameField.setAccessible(true);

            // 记录映射关系
            FieldMapping mapping = new FieldMapping(userIdField, userNameField);
            if (isCreatorField) {
                creatorIds.add(userId);
                creatorMappings.computeIfAbsent(obj, k -> new HashMap<>()).put(mapping, userNameField);
            } else if (isUpdaterField) {
                updaterIds.add(userId);
                updaterMappings.computeIfAbsent(obj, k -> new HashMap<>()).put(mapping, userNameField);
            }
        } catch (Exception e) {
            log.error("字段处理失败: {}", userIdField.getName(), e);
        }
    }

    /**
     * 批量查询用户名称
     */
    private Map queryUserNames(Set userIds) {
        UserInfoQueryProto query = UserInfoQueryProto.newBuilder()
                .addAllIds(userIds)
                .build();
        UserInfoListDataProto response = userInfoServiceBlockingStub.getUserInfoByIds(query);
        return response.getUserListList().stream()
                .collect(Collectors.toMap(
                        UserInfoDataProto::getId,
                        UserInfoDataProto::getUserName,
                        (oldVal, newVal) -> newVal
                ));
    }

    /**
     * 批量填充用户名称
     */
    private void fillUserNames(Map> mappings,
                               Map nameMap) {
        mappings.forEach((obj, fieldMap) -> fieldMap.forEach((mapping, userNameField) -> {
            try {
                Long userId = (Long) mapping.userIdField.get(obj);
                String userName = nameMap.getOrDefault(userId, "");
                userNameField.set(obj, userName);
            } catch (IllegalAccessException e) {
                log.error("用户名称填充失败: {}", userNameField.getName(), e);
            }
        }));
    }

    /**
     * 字段映射关系内部类
     */
    private static class FieldMapping {
        Field userIdField;
        Field userNameField;

        FieldMapping(Field userIdField, Field userNameField) {
            this.userIdField = userIdField;
            this.userNameField = userNameField;
        }
    }
} 
  

我这里需要说明的一点是,我是在微服务模块中。所以服务之间的通信时使用grpc远程通信的,我这里就是暴露了一个用户服务的客户端,并且通过一组用户Id来返回一组用户信息。使用Set类型的集合,能够减少我们相同ID的查询率。如果是单体项目的话,你要调用用户模块的接口,传入一组用户ID,返回一组用户信息就行了。

还要特别说明的一点是,我这里默认设置了填充对象只能有两种形式(也就是我在项目中封装好的返回两种类型的数据。如果是树形数据,直接返回就是List;如果是非树形结构,返回一个ResultData对象,这个对象中有一个rows属性,这个属性是List类型)所以,我封装的AOP切面通知,没有考虑到单个对象的填充,也就是不适用于单个T。

ResultData对象如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Schema(name = "非树形数据返回载体")
public class ResultData {

    /**
     * 页码
     */
    @Schema(name = "pageNum",description = "页码",  type = "Integer",example = "1")
    private Integer pageNum;

    /**
     * 每页条数
     */
    @Schema(name = "pageSize",description = "每页条数",  type = "Integer",example = "20")
    private Integer pageSize;
    /**
     * 总页数
     */
    @Schema(name = "totalPage",description = "总页数",  type = "Integer",example = "5")
    private Integer totalPage;

    /**
     * 数据总数
     */
    @Schema(name = "recordCount",description = "数据总数",  type = "Long",example = "50L")
    private Long recordCount;

    /**
     * 返回数据
     */
    @Schema(name = "rows",description = "返回数据",  type = "T")
    private List rows;
}

我们现在要使用时,就是两个注解就搞定了。首先在你返回前端的实体对象T上加上@AutoFillUserInfo 注解。需要注意的是,你这个实体对象中填充的字段命名一定要有规律。如下所示;

@Data
@AutoFillUserInfo 
public class TestEntity {


  private Long createUserId;

private Long updateUserId;

private String createUserName;

private String createUserName;
  
}

接下来,在要填充的方法上加上@AutoFillUserNameByUserId注解即可。

2、填充单个ID和字段

之前介绍的是填充两个值,接下来写法封装是填充一个值的。

我现在要根据我们文件ID、查询出文件的路径。并且这步操作要放在AOP中进行自动填充,那么方法和上个方法是一样的,我们需要创建两个注解和一个切面通知,相应的代码如下;

 实体对象上的注解;

package com.scmpt.framework.aop.query.file.image;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * @Author 张乔
 * @Date 2025/03/20 12:59
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFillFileInfo {
    /**
     * 文件ID字段后缀(默认后缀为 "StorageFileId")
     */
    String fileIdSuffix() default "StorageFileId";

    /**
     * 文件URL字段后缀(默认后缀为 "ImageUrl")
     */
    String imageUrlSuffix() default "ImageUrl";
}

方法上的注解;

package com.scmpt.framework.aop.query.file.image;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author 张乔
 * @Date 2025/03/20 12:59
 * @Description 自定义的 AOP 注解,用于根据用户ID自动查询用户Grpc填充用户名称
 * @Version 1.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFillImageUrlByFileId {
}

通知的切面类;

package com.scmpt.framework.aop.query.file.image;

import com.scmpt.files.grpc.storages.StoragesDataProto;
import com.scmpt.files.grpc.storages.StoragesListDataProto;
import com.scmpt.files.grpc.storages.StoragesQueryProto;
import com.scmpt.files.grpc.storages.StoragesServiceGrpc;
import com.scmpt.framework.core.web.response.ResultData;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @Author 张乔
 * @Date 2025/03/20 12:59
 * @Description 自定义的 AOP 注解,用于根据用户ID自动查询用户Grpc填充用户名称
 * @Version 1.0
 */

@Aspect
@Slf4j
public class FilesImageAutoFillAspect {

    @GrpcClient("scmpt-files")
    private StoragesServiceGrpc.StoragesServiceBlockingStub stub;

    @Around("@annotation(AutoFillImageUrlByFileId)")
    public Object autoFillFileInfo(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();
        log.info("进入文件信息切面");
        long startTime = System.currentTimeMillis();
        try {
            processResult(result);
        } catch (Exception e) {
            log.error("文件信息切面执行异常", e);
        }
        finally {
            log.info("文件信息切面执行完毕,耗时:{} ms", System.currentTimeMillis() - startTime);
        }

        return result;
    }

    private void processResult(Object result) {
        if (result == null) return;

        // 1. 收集所有需要处理的对象
        List targetObjects = new ArrayList<>();
        collectTargetObjects(result, targetObjects);

        // 2. 批量收集文件ID
        Set fileIds = new HashSet<>();
        Map> fileMappings = new HashMap<>();

        for (Object obj : targetObjects) {
            Class clazz = obj.getClass();
            AutoFillFileInfo annotation = clazz.getAnnotation(AutoFillFileInfo.class);
            if (annotation == null) continue;

            String fileIdSuffix = annotation.fileIdSuffix();
            String imageUrlSuffix = annotation.imageUrlSuffix();

            // 遍历所有字段,识别文件ID字段
            Arrays.stream(clazz.getDeclaredFields())
                    .filter(field -> field.getName().endsWith(fileIdSuffix))
                    .forEach(field -> processFileField(obj, field, fileIdSuffix, imageUrlSuffix, fileIds, fileMappings));
        }

        // 3. 批量查询文件信息
        Map fileUrlMap = !fileIds.isEmpty() ?
                queryFileUrls(fileIds) : Collections.emptyMap();

        // 4. 批量填充文件URL
        fillFileUrls(fileMappings, fileUrlMap);
    }

    private void collectTargetObjects(Object result, List targetObjects) {
        // 复用原有收集逻辑(支持ResultData、List等嵌套结构)
        if (result instanceof ResultData) {
            try {
                Field rowsField = result.getClass().getDeclaredField("rows");
                rowsField.setAccessible(true);
                Object rows = rowsField.get(result);
                collectTargetObjects(rows, targetObjects);
            } catch (Exception e) {
                log.error("ResultData rows字段访问失败", e);
            }
        } else if (result instanceof List) {
            for (Object item : (List) result) {
                collectTargetObjects(item, targetObjects);
            }
        } else if (result != null) {
            targetObjects.add(result);
        }
    }

    private void processFileField(Object obj, Field fileIdField,
                                  String fileIdSuffix, String imageUrlSuffix,
                                  Set fileIds,
                                  Map> fileMappings) {
        try {
            fileIdField.setAccessible(true);
            Long fileId = (Long) fileIdField.get(obj);
            if (fileId == null) return;

            // 构建对应的URL字段名称
            String fieldName = fileIdField.getName();
            String prefix = fieldName.replace(fileIdSuffix, "");
            String imageUrlFieldName = prefix + imageUrlSuffix;

            // 获取对应的URL字段
            Field imageUrlField = obj.getClass().getDeclaredField(imageUrlFieldName);
            imageUrlField.setAccessible(true);

            // 记录映射关系
            FieldMapping mapping = new FieldMapping(fileIdField, imageUrlField);
            fileIds.add(fileId);
            fileMappings.computeIfAbsent(obj, k -> new HashMap<>()).put(mapping, imageUrlField);
        } catch (Exception e) {
            log.error("文件字段处理失败: {}", fileIdField.getName(), e);
        }
    }

    private Map queryFileUrls(Set fileIds) {
        StoragesQueryProto request = StoragesQueryProto .newBuilder()
                .addAllIds(fileIds)
                .build();
        StoragesListDataProto response = stub.getStoragesInfoByIds(request);
        return response.getStorageListList().stream()
                .collect(Collectors.toMap(
                        StoragesDataProto::getId,
                        StoragesDataProto::getPath
                ));
    }
    private void fillFileUrls(Map> mappings,
                              Map fileUrlMap) {
        mappings.forEach((obj, fieldMap) -> fieldMap.forEach((mapping, imageUrlField) -> {
            try {
                Long fileId = (Long) mapping.fileIdField.get(obj);
                String fileUrl = fileUrlMap.getOrDefault(fileId, "");
                imageUrlField.set(obj, fileUrl);
            } catch (IllegalAccessException e) {
                log.error("文件URL填充失败: {}", imageUrlField.getName(), e);
            }
        }));
    }

    private static class FieldMapping {
        Field fileIdField;
        Field imageUrlField;

        FieldMapping(Field fileIdField, Field imageUrlField) {
            this.fileIdField = fileIdField;
            this.imageUrlField = imageUrlField;
        }
    }
} 
  

接下来,我们就可以在使用这个注解了。注意,如果你的文件ID和要填充的字段属性名称是自定义的时,只需要在类注解@AutoFillFileInfo 上指定相应的ID名称和要填充的属性值名称即可。

如下使用方式;

@Data
@AutoFillFileInfo(imageUrlSuffix="filePathUrl"  ,fileIdSuffix ="fileId")
public class TestEntity {


  private Long fileId;


private String filePathUrl;
  
}

然后,在要填充的方法上加上方法注解就可以了

你可能感兴趣的:(spring,boot整合第三方技术,AOP,spring,boot)