欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

业务代码与通用代码分离案例

程序员文章站 2022-10-03 16:23:14
业务代码与通用代码分离案例背景1.关联查询后返回结果数据​由于微服务化,每个服务都具有独立的业务(数据),那么如果我们要查询一个商品列表,商品表中有用户id,但没有用户姓名,因为商品服务和用户服务是两个单独的服务,两者的数据不是存在一个库里(不能使用关联查询),所以我们需要在返回商品列表处,对其进行遍历,然后根据用户id去查询用户的接口,拿到用户名称后,设置给商品列表返回。2.对结果数据进行解密​需求是返回用户列表,返回列表对象中的地址需要进行解密操作(因为地址在数据库中存的是密文),所以需要...

业务代码与通用代码分离案例

背景

1.关联查询后返回结果数据

​ 由于微服务化,每个服务都具有独立的业务(数据),那么如果我们要查询一个商品列表,商品表中有用户id,但没有用户姓名,因为商品服务和用户服务是两个单独的服务,两者的数据不是存在一个库里(不能使用关联查询),所以我们需要在返回商品列表处,对其进行遍历,然后根据用户id去查询用户的接口,拿到用户名称后,设置给商品列表返回。

2.对结果数据进行解密

​ 需求是返回用户列表,返回列表对象中的地址需要进行解密操作(因为地址在数据库中存的是密文),所以需要在返回用户列表处,对其进行遍历,然后用地址去调解密接口,返回解密明文后,设置到用户列表中返回。

分析

​ 以上两种场景,有一个共同点:都需要对结果数据进行某种处理(例如关联查询、解密操作),如果把这类代码直接写到原来的业务代码里,则违背了单一职责原则(案例一:既要查询商品信息又要查询用户信息;案例二:既要查询用户信息,还要查询解密接口),且还违背了开闭原则(案例二:例如原来的代码是不要求解密的,现在产品又要求需要对地址进行解密,如果直接在业务代码里修改,那么就违背了开闭原则,ps.开闭原则:对扩展开放、对修改关闭)。

结论

​ 我们需要把业务代码和非业务代码(通用代码)分离,可以使用动态代理设计模式AOP,通过环绕对其结果进行处理,但仅通过AOP技术是不够的,因为获取到其结果后,你不知道需要对哪个字段处理,这时候就需要注解技术了(what?不知道什么是注解?简单一句话理解就是:计算机可以读懂的代码注释)。把注解打在需要处理的字段上,并通过注解上备注,我们可以在处理结果的时候,再通过反射技术就可以获取到注解字段和注解上的所有备注信息。

​ 基于以上AOP+注解+反射,我们就可以实现一种业务代码和通用代码分离的封装逻辑。

实践

1.确定切入点(AOP)

如下方法,需要对返回的列表中user对象的address进行解密后再返回。则我们需要对其方法进行切面处理。

@RequestMapping(value = "/list")
public List<User> getUserList(){
    List<User> users=this.queryList();
    return users;
}

a.创建切面注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SecretField {
}

b.基于注解的实现

@Aspect
@Component
public class SecretFieldAspect {

    @Autowired
    private SecretUtil secretUtil;

    //这里环绕用的是注解的方式,即注解打在哪里,就会进行切面处理方法
    @Around("@annotation(com.example.demo0719.SecretField)")
    public Object secretFieldValue(ProceedingJoinPoint pjp) throws Throwable {
        Object obj = pjp.proceed();
      	//加密处理,封装在了一个方法里
        secretUtil.decodeObj(obj);
        return obj;
    }

}

c.注解打在需要加密的方法上

//@SecretField,其注解即表示该方法会进入SecretFieldAspect的切面方法,因为在@Around处已经声明了。
@SecretField  
@RequestMapping(value = "/list")
public List<User> getUserList(){
    List<User> users=this.queryList();
    return users;
}

以上,即在执行getUserList方法时,会进入SecretFieldAspect的secretFieldValue方法,这仅仅是完成了aop切面的部分。

具体实现逻辑在secretUtil.decodeObj(obj),我们接着往下看…

2.确定处理字段(annotation)

​ 在进入切面secretFieldValue方法后,我们能拿到的对象为pjp,其可以拿到getUserList的入参,但我们这次讲的不是入参的处理,这块先暂时忽略。

​ Object obj = pjp.proceed(),在调用processd方法后,即走完了getUserList的方法,返回的对象obj也就是getUserList的返回值。这里我们需要处理的对象也就是obj对象,需要拿到它后,取它的字段进行解密后再设置回去。那么问题来了,obj是个list,里头装的是user对象,我们怎么知道对user对象的哪个值进行解密处理呢?

​ 这里就引出注解了,我们都知道注释,注释是写给程序员看的。而注解是写给计算机看的,所以其编译成字节码文件后,注解也是带上的,但注释就不会了。所以我们可以利用这个特点,把需要处理的字段打上我们自定义的注解,这样我们就可以从它的class文件反射后判断,打上了我们自定义字段的即表示需要处理的。

a.自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NeedSecretField {
    //需要调用的类(这里指的是加密类)
    Class<?> beanClass();

    //需要调用的方法(加密类的方法)
    String method();

    //需要调用方法的参数(这里指user对象取哪个属性,因为存在需要取不是注解上的字段,
  	//例如获取用户名称,注解是打在用户名称上的,这里sourceField就是指userId)
    String sourceField();

    //目标字段(即上面方法返回的结果取哪个属性值)
    String targetField();

}

b.注解标注

public class User {

    private String name;

    private Integer age;

  	//如下,通过注解,备注了需要调用哪个对象的哪个方法,以及关于取参的,入参取哪个属性,返回对象取哪个属性。
    @NeedSecretField(beanClass = SecretUtil.class,method = "decodeStr",
                     sourceField = "address",targetField = "value")
    private String address;

}

3.实现设定值(反射)

接下来就是切面里实现解密和赋值了。让我们再看回secretUtil.decodeObj(obj)方法。

@Component
public class SecretUtil {

    @Autowired
    private BeanUtil beanUtil;

  	//这个方法为模拟解密的方法
    public SecretObj decodeStr(String message){
        SecretObj secretObj = new SecretObj();
        String substring = message.substring(0, message.length() - 1);
        secretObj.setValue(substring);
        return secretObj;
    }

  	//这儿才是真正的实现
    public void decodeObj(Object obj) throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException, InvocationTargetException {
        //目前结果只有list
        if (obj instanceof Collection) {
            //拿到集合中任一一个对象class
            Collection col = (Collection) obj;
            Class<?> clazz = col.iterator().next().getClass();
            //拿到对象中所有字段
            Field[] declaredFields = clazz.getDeclaredFields();
            //缓存,因为解密字段如果一样就读缓存
            Map<String, Object> cache = new HashMap<>();
            //遍历字段,找到需要处理的字段(有注解的字段)
            for (Field declaredField : declaredFields) {
                NeedSecretField annotation = declaredField.getAnnotation(NeedSecretField.class);
                if (null == annotation)
                    continue;
                //设置为可见
                declaredField.setAccessible(true);
                Class<?> beanClass = annotation.beanClass();
                String methodName = annotation.method();
                //结果对象中的属性值
                String sourceField = annotation.sourceField();
                String targetFieldName = annotation.targetField();
                Field paramField = clazz.getDeclaredField(sourceField);
                Method beanMethod = beanClass.getMethod(methodName, paramField.getType());
                Object bean = beanUtil.getBean(beanClass);
                Field targetField = null;
                Boolean needInnerField = StringUtils.isNotBlank(targetFieldName);
                String prefixKey = beanClass + "-" + methodName + "-" + annotation.targetField() + "-";
                paramField.setAccessible(true);
                for (Object ob : col) {
                    //拿到原对象的属性值
                    Object paramValue = paramField.get(ob);
                    if (null == paramValue)
                        continue;
                    Object value = null;
                    String key = prefixKey + paramValue;
                    if (cache.containsKey(key)) {
                        value = cache.get(key);
                    } else {
                        //此时的result是个SecretObj对象而不是其里头的value值,要从对象中取到属性值
                        Object result = beanMethod.invoke(bean, paramValue);
                        if (needInnerField) {
                            if (result != null) {
                                if (null == targetField) {
                                    targetField = result.getClass().getDeclaredField(targetFieldName);
                                    targetField.setAccessible(true);
                                }
                                //拿到属性值
                                value = targetField.get(result);
                            }
                        }
                        cache.put(key, value);
                    }
                    //真正目的就是在这赋值
                    declaredField.set(ob, value);
                }
            }
        }
    }
}

涉及到的代码

beanUtils->通过字节码class从spring容器里获取到实例对象

/**
 * beanUtil
 * 如果只知道clazz,则通过它可以获取到spring容器里的clazz的实现对象
 */
@Component
public class BeanUtil implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext=applicationContext;
    }

    /**
     *
     * @param name clazz
     * @return
     */
    public Object getBean(Class<?> name){
        Object bean = applicationContext.getBean(name);
        return bean;
    }
}

解密后返回的对象实体,一般返回都是一个实体,所以这里咱们也模拟了一个实体。

public class SecretObj {
    private Integer id;
    private String value;
}

总结

改变前:

@RequestMapping(value = "/list")
public List<User> getUserList(){
    List<User> users=this.queryList();
    return users;
}

改变后:

@SecretField
@RequestMapping(value = "/list")
public List<User> getUserList(){
    List<User> users=this.queryList();
    return users;
}

对于解密字段的需求,我们只需要在方法上加一个@SecretField字段即可,仅从业务代码实现上,只需一行代码,且没有修改业务代码,符合开闭原则。且解密逻辑是另外写在一个类里,而不是当前业务类里,也符合单一职责原则。对于追求完美的程序员来说,这种实现简直香!

本文地址:https://blog.csdn.net/Just_Doo_IT/article/details/107593092

相关标签: JAVA后端