当前位置: 移动技术网 > IT编程>软件设计>面向对象 > 业务代码与通用代码分离案例

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

2020年07月27日  | 移动技术网IT编程  | 我要评论
业务代码与通用代码分离案例背景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

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网