业务代码与通用代码分离案例
业务代码与通用代码分离案例
背景
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
上一篇: List并发修改异常的产生和解决
下一篇: Caused by: java.lang.ClassNotFoundException: org.junit.platform.launcher.TestExecutionListener
推荐阅读
-
bootstrap table之通用方法( 时间控件,导出,动态下拉框, 表单验证 ,选中与获取信息)代码分享
-
js代码与html代码分离示例
-
关于Asp代码与页面的分离模板技术第1/3页
-
flash as3.0中get与set的用法与案例代码
-
业务代码与通用代码分离案例
-
Nginx动静分离实现案例代码解析
-
dotnetcore+vue+elementUI 前后端分离---支持前端、后台业务代码扩展的快速开发框架
-
代码与页面的分离
-
关于Asp代码与页面的分离模板技术第1/3页
-
光之翼Java通用代码生成器1.0.0Beta版已公布,经过更新与测试,便携易用,功能强大 光之翼Java通用代码生成器Beta版动词算子代码生成器