JAVA如何编写切面以及切面解决的问题场景

        工作以后,已经写过好多个切面类用来处理业务逻辑了,切面编程还是很方便的,可以用来避免一些重复工作,减少代码量,平日里CRUD写太多,偶尔写一下还是很有成就感的,在下面对切面编程做一些个人总结(因为网上的基础知识教学已经太多了,所以这里只写一些个人感受)。

        最近用到的一个使用场景(B端):计划,合同,订单从提交到发布中间存在着很多的审批流程。有这样一个场景,俩个对同一页面拥有同一权限(修改,删除,提交)的用户都处在这个页面,但是有一个用户没有进行操作,只是停留在了这个页面,另一个用户点击了提交,数据进入到了审批流程,但是另外一个用户不知道,这个条数据还存在于他的页面中,他点击了修改或者删除,修改了已经处在审批流程中的数据,这样问题就出现了。

        解决方案:1.前端长时间位操作刷新页面。2.后端校验。这种对于数据要求严格的地方,必须要做后端数据校验,所以我们选择后者。后端数据校验又有几种思路:1.为每条记录增加一个版本号,修改,删除和提交都要在where条件处加上版本号限制(不适用批量操作,因为批量操作的话’每条数据‘的版本号可能都不相同,不要循环操作数据库);2.先去查询对应的记录是否处在可以操作的状态(并发操作可能存在问题,需要加锁控制,因为先查在修改是非原子操作)。3.一般删除,修改和提交都属于update操作可以直接在where条件上加上业务限制(比如说库存扣减要大于零,审批级别要处在哪一级等等),属于原子操作。第一种和第三种属于差不多的思路,但是批量操作时比如按照ID几个批量修改无法感知哪个ID修改成功,无法生成后续的修改日志,只是版本号比较方便不用做太多复杂的’业务逻辑‘的判断。

        这次我们做的是一个管理后台,对数据和日志的要求较为严格,而且批量操作很多,所以采用第二种方案。因为有很多地方都需要进行这种先查后修改的操作,属于重复度较高的工作,所以这里这里采用了切面(减少重复代码,代码整洁,是程序员编写代码能力的提升方向)。下面是代码实现:

        切面定义在那一层都可以,我们这个项目是DDD,所以我把它放在了入口层做一个拦截。因为需要拦截的方法和类都不相同,所以这里采用注解比较方便。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckAnnotation {

    /**
     * LIST就是List<Object> 不是string
     * OBJECT_LIST 就是id以list集合的方式存在于Object中
     */
    enum Type {OBJECT,LIST,OBJECT_LIST};

    /**
     * 数据源
     */
    String value();

    /**
     * 用于获取主键ID
     */
    String idKeyName() default "ids";

    /**
     * 用于获取订单,合同,计划应处的修改级别
     */
    String levelKeyName() default "";

    /**
     * 参数类型
     */
    Type argsType() default Type.OBJECT;
}

        写好切面最重要的就是,你要知道你这个切面运用的所有场景,在这里面我定义了一个枚举用来穷举我的所有场景(具体看业务)。注解知识需要注意的点就是default,存在默认值的属性就不是必须赋值得了。接下来是切面类。

@Aspect
@Component
@Slf4j
public class CheckAspect {

    @Autowired
    private IntlAspectRepository intlAspectRepository;

    @Pointcut("@annotation(checkAnnotation)")
    public void checkPointCut(CheckAnnotation checkAnnotation) {
    }

    @Around("checkPointCut(checkAnnotation)")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint, CheckAnnotation checkAnnotation) {
        try {
            //如果是object还要判断object里放的id的是不是id数组
            //用selectList方法查询id判断返回list的长度=0直接返回buildSuccess(false)大于零就替换然后正常执行
            String tableName = checkAnnotation.value();
            String idsKey = checkAnnotation.idKeyName();
            String levelKeyName = checkAnnotation.levelKeyName();
            CheckAnnotation.Type argsType = checkAnnotation.argsType();
            Object[] args = proceedingJoinPoint.getArgs();//还是要看args是什么

            if (argsType.equals(CheckAnnotation.Type.OBJECT)) {
                Class clazz = args[0].getClass();
                Field idField = clazz.getDeclaredField(idsKey);
                Field levelField = clazz.getDeclaredField(levelKeyName);
                idField.setAccessible(true);
                levelField.setAccessible(true);
                //id可能是集合 也可能是单个id
                String id = (String) idField.get(args[0]);
                String level = (String) levelField.get(args[0]);
                List<String> ids = intlAspectRepository.getCanUpdateId(new IntlAspectModel(tableName, Arrays.asList(id), level));
                if (ids.size() > 0) {
                    return proceedingJoinPoint.proceed(args);
                }
            } else if (checkAnnotation.argsType().equals(CheckAnnotation.Type.OBJECT_LIST)) {
                Class clazz = args[0].getClass();
                Field idField = clazz.getDeclaredField(idsKey);
                Field levelField = clazz.getDeclaredField(levelKeyName);
                idField.setAccessible(true);
                levelField.setAccessible(true);
                //id可能是集合 也可能是单个id
                List<String> id = (List<String>) idField.get(args[0]);
                String level = (String) levelField.get(args[0]);
                //这个可以操作的肯定比原来的小
                List<String> ids = intlAspectRepository.getCanUpdateId(new IntlAspectModel(tableName, id, level));
                if (ids.size() != 0) {
                    idField.set(args[0], ids);
                    return proceedingJoinPoint.proceed(args);
                }
            } else {
                //args中存储的是参数的种类的个数
                List<Object> list = (ArrayList<Object>)args[0];
                Class<?> clazz = list.get(0).getClass();
                Field levelField = clazz.getDeclaredField(levelKeyName);

                levelField.setAccessible(true);
                String level = (String) levelField.get(list.get(0));
                List<String> idList = list.stream().map(item -> {
                    Class clazzs = item.getClass();
                    String id = null;
                    try {
                        Field idField = clazzs.getDeclaredField(idsKey);
                        idField.setAccessible(true);
                        //id可能是集合 也可能是单个id
                        id = (String) idField.get(item);
                    } catch (Throwable var2) {
                        throw new RuntimeException(var2.getMessage());
                    }
                    return id;
                }).collect(Collectors.toList());
                List<String> ids = intlAspectRepository.getCanUpdateId(new IntlAspectModel(tableName, idList, level));
                if (ids.size()!=0){
                    List<Object> newArgs = list.stream().filter(item -> {
                        Class clazzs = item.getClass();
                        String id = null;
                        try {
                            Field idField = clazzs.getDeclaredField(idsKey);
                            idField.setAccessible(true);
                            //id可能是集合 也可能是单个id
                            id = (String) idField.get(item);
                        } catch (Throwable var2) {
                            throw new RuntimeException(var2.getMessage());
                        }
                        return ids.contains(id);
                    }).collect(Collectors.toList());
                    if (newArgs.size()>0){
                        args[0]=newArgs;
                        return proceedingJoinPoint.proceed(args);
                    }
                }
            }
        } catch (Throwable var1) {
            log.warn("数据校验异常");
        }
        return SingleResponse.buildSuccess(false);
    }
}

        这里使用的是around环绕通知,其实使用before前置通知也可以,这里的思路就是通过反射拿到方法的入参,然后根据注解上的表名去查询对应的符合修改条件的数据,再去执行正常的操作流程。这里是查询方法getCanUpdateId,不会出现sql注入,因为我们的表名是设置在后台代码的,和参数无关其他人无法修改。

<select id="listIdCanUpdate" resultType="java.lang.String">
        select id from
        <if test="tableName != null">
            ${tableName}
        </if>
        <where>
            del_flag=0
            <if test="ids != null and ids.size() > 0">
                AND id in
                <foreach item="item" index="index" collection="ids" open="(" separator="," close=")">
                    #{item}
                </foreach>
            </if>
            <if test="checkLevel != null">
                AND CHECK_LEVEL = #{checkLevel}
            </if>
        </where>
    </select>

        注意:1.getArgs方法拿到的方法入参是你方法上入参的种类的数组(例:假如说你的入参是一个集合,那么你arg[0]就是这个集合,如果是一个集合和一个字符串那就是arg[0]是集合,arg[1]是那个字符串);2.通过发射拿到的类,其中的属性都是私有的,需要使用setAccessible(true)打开,查询出可操作的数据之后,再通过field.set(值,设置进去的对象)方法修改入参的值。

方法举例如下:

@CheckAnnotation(value = "表名",idKeyName = "ids",levelKeyName = "oldCheckLevel",argsType = CheckAnnotation.Type.OBJECT_LIST)
    public SingleResponse<Boolean> cancelOrderById(CancelIntlOrderRequest cancelIntlOrderRequest) {方法体}

 对应实体类如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@StructureObject(name = "取消订单请求")
public class CancelIntlOrderRequest {

    @Field(name = "主键ID")
    private List<String> ids;

    @Field(name = "审批级别")
    private String checkLevel;

    @Field(name = "审批结果")
    private String checkResult;

    @Field(name = "原审批级别")
    private String oldCheckLevel;

}

       注: idKeyName和levelKeyName必须和参数中的属性名相同才能使用反射拿到信息。

        总结:其实个人觉得不论代码如何写都是在不同的地方处理数据,但是代码的好坏就在于使用是否方便,以后修改是否方便(复用性强,可扩展)。面向切面编程是java中必不可少的知识,还需要好好学习,但语言也只是工具,如何抽取出’切面(业务逻辑)‘的’思想‘是最重要的前面的业务场景和解决方案是最重要的。

  • 29
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Spring Cloud 项目的切面中根据数据字典格式化返回值,你可以使用 AOP(面向切面编程)来实现。下面是一个示例的 Java 代码: 首先,你需要创建一个数据字典的实现类,用来管理具体的键值对。这里使用 `HashMap` 作为数据字典的示例: ```java import org.springframework.stereotype.Component; import java.util.HashMap; @Component public class DataDictionary { private HashMap<String, String> dictionary; public DataDictionary() { dictionary = new HashMap<>(); // 添加键值对到数据字典 dictionary.put("name", "John"); dictionary.put("age", "25"); dictionary.put("city", "New York"); } public String getValue(String key) { // 检查数据字典中是否包含指定的键 if (dictionary.containsKey(key)) { // 获取指定键的值 return dictionary.get(key); } else { return null; } } } ``` 接下来,你需要创建一个切面类,用来拦截方法并处理返回值的格式化: ```java import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Aspect @Component public class ReturnValueFormattingAspect { @Autowired private DataDictionary dataDictionary; @Pointcut("execution(* com.example.YourService.*(..))") public void serviceMethods() {} @AfterReturning(pointcut = "serviceMethods()", returning = "returnValue") public void formatReturnValue(Object returnValue) { if (returnValue instanceof String) { String formattedValue = dataDictionary.getValue((String) returnValue); if (formattedValue != null) { // 根据需要进行返回值的格式化操作 // 这里只是简单地替换为数据字典中的对应值 formattedValue = "Formatted value: " + formattedValue; // 将格式化后的值设置为方法的返回值 ((String) returnValue).replace((String) returnValue, formattedValue); } } } } ``` 在上述代码中,我们首先通过 `@Autowired` 注解将 `DataDictionary` 类注入到切面类中。然后,使用 `@Pointcut` 注解定义了一个切点,用来匹配需要拦截的方法。接着,使用 `@AfterReturning` 注解定义了一个后置通知,当匹配的方法执行完毕并返回值时,会进入该通知进行处理。在通知方法中,我们首先检查返回值是否为 `String` 类型,然后调用 `DataDictionary` 类中的 `getValue` 方法获取对应的格式化值,并进行自定义的格式化操作(在这里只是简单地替换为数据字典中的对应值)。最后,我们将格式化后的值设置为方法的返回值。 请注意,上述代码中的 `com.example.YourService.*(..)` 部分需要替换为你实际项目中的服务类和方法的包名和方法名。 希望这个示例能帮助到你在 Spring Cloud 项目中根据数据字典格式化返回值的需求。请根据你的具体场景进行调整和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值