数据映射框架之三大神器:反射、注解、动态代理

举报
Byyyi耀 发表于 2024/05/06 11:06:24 2024/05/06
【摘要】 数据映射框架定义:“定死”,适用于快速开发。特征无需实现接口低代码 数据映射数据库中的数据(表)----(框架级别)---- Java中的数据(类/实体)-- 前端的数据(Json格式的数据) 反射定义:获取一个字节码文件对象,然后获取该类的属性和方法(即对类进行解剖),获取之后再将其映射成一个个对象,动态解析类的结构(属性 构造器 方法)。特征代码追踪的时候可能直接进入到抽象层,找不到具...

数据映射框架

定义:“定死”,适用于快速开发。

特征

  • 无需实现接口
  • 低代码

数据映射

数据库中的数据(表)----(框架级别)---- Java中的数据(类/实体)-- 前端的数据(Json格式的数据)
image.png

反射

定义:获取一个字节码文件对象,然后获取该类的属性和方法(即对类进行解剖),获取之后再将其映射成一个个对象,动态解析类的结构(属性 构造器 方法)。

特征

  • 代码追踪的时候可能直接进入到抽象层,找不到具体代码。

常见异常

ClassNotFoundException:Class.forName()中的.class文件路径找不到

IllegalAccessException:非法访问异常

NoSuchElementException:

InvocationTargetException:方法调用异常

InstantiationException:实例化异常

一般来说,我们只获取类型信息而不对属性值进行基本操作,反射提供了一种绕过常规访问修饰符的方式,能够在运行时获取和修改对象的属性,即使是私有的或者是受保护的也可能被修改,可能违反了预期的封装性和安全性。

  • 三种获取字节码文件对象的方法(可以通过该对象来操作或者获取类的相关信息)

    • 只知道类的字节码的情况下,Class.forName(字节码文件的全包路径(由于.class文件和.java文件是对等映射的,),即只需对对应的.java文件进行Copy Reference)

      全包路径:从最大的根包开始的路径(不包括src)

    • 已存在对象的情况下,通过实例对象获取类

    • 已知类源码的情况下,通过类名获取类

 final Class<?> aClass = Class.forName("cha01.Student");

final Student student = new Student("张三丰", 100, "男", 123.456f);
final Class<? extends Student> aClass1 = student.getClass(); 

final Class<Student> stuClass = Student.class;
  • 获取类的基本信息
final String name = stuClass.getName();//cha15.Student
final int mod = stuClass.getModifiers();//1

所谓的类的name指的是类的全包路径。

通过name判断一个类,要看类的全包路径(在同一个包内不会出现同一个类,全包路径相较于单独的类名可以避免重名类冲突)

  • 获取类的属性及其信息
final Field[] fields = stuClass.getFields();
final Field[] declaredFields = stuClass.getDeclaredFields();

 for(Field field:declaredFields){
   final int mod = field.getModifiers();//1
   final String typeName = field.getType().getName();//float
   final String fieldName = field.getName();//salary
   final Object value = field.get(stu);//123.456
 }

field:获取当前类及父类可访问公共字段,DeclaredField:只能获取当前类全部字段,在反射中用的更多的是DeclaredFields(能够获取更多的字段信息)

mapToObject方法中,mapToObject() 方法中的 String 对应的是 clazz 对应的类的属性名,Object是属性的值。在需要对私有属性进行赋值的特定场景下,既可以通过getDelaredFields获取全部字段,设置为可见后在获取名字。

public static <T> T mapToObject(Map<String, Object> data, Class<T> clazz) throws IllegalAccessException, InstantiationException {
        T object = clazz.newInstance();

        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            String fieldName = field.getName();

            if (data.containsKey(fieldName)) {
                Object value = data.get(fieldName);
                field.set(object, value);
            }
        }

        return object;
    }

getType()的返回类型是Class,通过class.getName()来获取类型(原因是在java中存在类型擦除机制,即无法在运行时获取泛型的具体类型信息,需要通过获取字节码文件(返回Class)后通过反射再获取具体类型信息。)

值是面向对象的,对非静态属性的值是通过new对象,通过set方法对属性赋值,因此field.getName()是通过属性利用反射获取对象中该属性的值

  • 字段操作
for(Field field:tClass.getDeclaredFields()){// 获取所有字段
  field.setAccessible(true);// 设置可见
  field.set(Object obj,value)// 为字段赋值
}

构造器

  • 获取构造器

stuClass.getConstructor(args...) 括号内是动态参数,表示构造器中的参数

final Constructor<Student> noArgConstructors = stuClass.getConstructor();
final Constructor<Student> allArgConstructors = stuClass.getConstructor(String.class, int.class, String.class, float.class);// 按顺序的每一个参数的类型信息
  • 用获取的构造器创建对象
final Student stu1 = noArgConstructors.newInstance();
final Student stu2 = allArgConstructors.newInstance("张三丰",100,"男", 123.456f);

方法及其参数

一般只用getMethods(),因为非公共的方法设计之初就不考虑调用。

final int mod = method.getModifiers();// 修饰符
final String returnTypeName = method.getReturnType().getName();// 返回类型名称 
final String methodName = method.getName();// 方法名称
final Parameter[] parameters = method.getParameters();// 参数
System.out.printf("%d\t%s\t%s\n",mod,returnTypeName,methodName);
for (Parameter parameter : parameters) {
  final String paraTypeName = parameter.getType().getName();// 获取参数类型名称
  final String parameterName = parameter.getName();
  System.out.printf("\t%s\t%s\n",paraTypeName,parameterName);
}
  • 对于反射,参数是拿不到名字的(getName()无法拿到参数真实的名字,只能拿到arg0和arg1…)
根据获取的方法参数利用反射机制调用对象的方法(此处以get,set方法为例)
  • 先确定方法的类型

  • 再对方法进行调用

    method.invoke(Object obj,Object args),obj表示对象,args表示方法的参数

get方法
for(Method method:methods){
  if(methodName.matches("(get|is).*") && !methodName.endsWith("Class")){//methodName.startsWith("get") 不可使用 因为可能漏掉以is开头的boolean方法,同时又需要排除掉getClass()方法
    final Object value = method.invoke(stu);// 反过来:对象.方法 -> 方法.invoke(对象)
    System.out.println(value);
  }
  System.out.println();
}
set方法

因为需要传参,set方法具有独特性,所以需要构建属性与方法之间的映射表,利用属性来获取对应的方法,从而实现特定属性的set。

Map<String, Method> setterMap = new HashMap<>();
for (Method method : methods) {
  String methodName = method.getName();
  if (!methodName.startsWith("set")) {
    continue;
  }
  // methodName setAge() setGenderType() -> age genderType
  methodName = methodName.substring(3);
  methodName = methodName.substring(0, 1).toLowerCase() + methodName.substring(1);
  setterMap.put(methodName, method);
}
setterMap.get("age").invoke(stu, 22);// 获取setAge()方法之后,然后再进行调用将年龄设置为22岁
面向数据类作数据解析的反射
  • 步骤

    • 尝试获取类的无参构造器,并new一个对象出来,假设该对象为obj。
    • 获取set方法,并利用set为对象的属性赋值,假设该方法为method。
    • 数据解析:method.invoke(obj,args…)
  • 其他

    在数据映射框架或者数据解析中,不写基本类型(原因:在数据映射框架或数据解析中,不写基本类型的原因是为了避免空值(null)的问题。基本类型(如 int、boolean、double 等)在 Java 中是不能为 null 的,它们有默认的初始值。如果在数据映射或解析过程中遇到一个空值,即无法将其转换为基本类型的有效值,就会导致异常。)

注解

定义

注解可以被视作一种特殊的标记,在编译或者运行时期可以检测到这些标记而做一些特殊的处理,而自定义注解则提供了一种随时调用特定数据的可能。

注解的特征
  • 注解是一种元数据形式。即注解是属于java的一种数据类型,和类、接口、数组、枚举类似。

  • 注解用来修饰,类、方法、变量、参数、包。

  • 注解不会对所修饰的代码产生直接的影响

  • 用系统提供的注解进行自定义注解

  • 访问修饰符默认为public

  • 注解类型元素的名称一般为名词,后面可以补充value

  • 元素类型只能是基本的数据类型

  • 注解中可以一个注解中包含多个不同方法所需的注解,在注解赋值的时候只需要各取所需

注解的调用

image.png

代码
// 常用的元注解(使用反射时必须要有)
@Retention(RetentionPolicy.RUNTIME)// 生命周期:(RUNTIME注解表示将被编译器记录在class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到)
@Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD})// 标志注解可用的内容:METHOD,TYPE用于方法或者类或者属性

public @interface FileAnt {// 注解声明
  	 // 注解类型元素 (访问修饰符) 元素类型 元素名称() default ...
    enum FileType{
        CSV,JSON,TXT
     }// 文件的类型:通过枚举进行限定(框架在使用文件类型时会自动处理)
    String path();// 文件路径:返回类型为字符串,默认为空字符串
    FileType type() default FileType.CSV;// 文件类型:返回类型为FileType,默认为CSV格式
    String desc() default "";
    boolean append() default true;
}
  • 当注解的返回类型是自定义/值有限定范围的时候,选择在注解内定义枚举类型。
反射操作下获取注解
  • 注解是在方法/类/属性哪里上就在哪里进行获取

  • isAnnotationPresent(Class<? extends Annotation> annotationClass)方法是专门判断该元素上是否配置有某个指定的注解;

  • getAnnotation(Class< T > annotationClass)方法是获取该元素上指定的注解。之后再调用该注解的注解类型元素方法就可以获得配置时的值数据;(⭐注意:在获取注解之后需要判断注解对象是否为空再进行调用)

  • 反射对象上还有一个方法getAnnotations(),该方法可以获得该对象身上配置的所有的注解。它会返回给我们一个注解数组。

伪代码模板

if(对象.isAnnotationPresent(xxx.class)){
  注解对象 = 对象.getAnnotation(xxx.class);
  if(null == ant){
    throw new RuntimeException("please set annotation @注解名称 in method");
   }
  注解对象.注解...// 获取值
}

在这里插入图片描述

在这里插入图片描述

  • 在给注解赋值的时候,如果没有默认值/想要改值,就要提供值,有默认值的话就不用给值;所谓的提供值是path = ""(字符串)/(整数)/{}(数组)的方式在引号内赋值。

动态代理

定义:在不实现接口的情况下,完成符合继承关系和带有特定注解的接口的自动实现。动态代理通常写为工厂模式

注解+子接口的模式:子接口是为了继承抽象方法,同时通过注解进行自定义,实现了FileOpr和FileAnt之间的绑定。

注解+子接口的优势:1.子接口可以用这些注解来扩展或者定制代码的行为,注解也可以作为自定义的规则和约束。

2.子接口继承了父接口的定义,并且可以在父接口基础上进行定制和扩展(实例中CommodityFileOpr只是继承定义而并无扩展)

3.在动态代理的过程中,传入子接口创建代理对象,再将代理对象进行强转后,能够实现父类引用操作子类。

FileOpr类——文件操作接口
public interface FileOpr {
    <T> List<T> read(Class<T> tClass);// 从文件中以行为单位读取,利用反射将每行解析成一个对象,最后用集合存储起来
    <T> void write(List<T> list);// 向文件中写东西
}
CommodityFileOpr类——商品文件操作接口
public interface CommodityFileOpr extends FileOpr{
    @FileAnt(path="C:\\Users\\lenovo\\Desktop\\commodity.log"
            ,type = FileAnt.FileType.TXT
            ,desc = "Commodity\\{sku=\"(.*?)\",brand=\"(\\S+)\",categoryId=(\\d+),price=(\\d+\\.\\d+)}")
    @Override
    <T> List<T> read(Class<T> tClass);
    // <T> List<T> read(Function<String,T> func); 由调用者提供由String->T的方法,只需要负责搭建框架
    @FileAnt(path="C:\\Users\\lenovo\\Desktop\\commodity.log"
            ,type = FileAnt.FileType.TXT
            ,append = false)// 如果 没有默认值/所需要的值并非提供值 就要提供值,有默认值的话就不用填
    @Override
    <T> void write(List<T> list);

}
  • 继承接口的目的是,将FileOpr中的抽象方法继承过来
  • 设置该子接口的目的:将接口的抽象方法与注解进行绑定,从而实现自定义。
文件实体映射类(工厂模式)

实现方式:利用Proxy.newProxyInstance()创建代理对象(Object),在创建代理对象的过程中实现父类接口,方法中传入与子接口有关的参数。

图如下:obj表示的即为商品类的对象
image.png

// 实体和文件的映射工厂
public class EntityFileMapperFactory {
    // 如果找不到实现层,很可能就是反射变为字节码文件。
    // 获取的是FileOpr的子接口的类名信息。

    /**
     * 获取所有set方法,构建分组编号与set方法对应的setters映射集。
     * @param aClass
     * @return
     */
    private static Map<Integer, Method> parseMethod(Class aClass){
        Map<Integer,Method> setters = new HashMap<>();
        for (Method method : aClass.getMethods()) {
            if(method.getName().startsWith("set")){
                SetterOrder ant = method.getAnnotation(SetterOrder.class);
                if(null == ant){
                    throw new RuntimeException("no @SetterOrder Annotation Exception");
                }
                setters.put(ant.order(), method);
            }
        }
        return setters;
    }

    /**
     * 由于传入set()方法中的参数不一定是字符串,而由正则group()分组提取后的数据都为字符串,即构造出set()方法对应的所传参数为字符串的构造器
     * 先获取set()方法对应的参数类型信息,利用其再获取构造器,将数据传入构造器获取实例。
     */
    public static Object parameterParse(Method method,String group)
            throws NoSuchMethodException
            , InvocationTargetException
            , InstantiationException
            , IllegalAccessException {
        Class<?> parameterType = method.getParameterTypes()[0];// 获取所有的参数类型,但是set方法通常只有一种参数类型
        Constructor<?> constructor = parameterType.getConstructor(String.class);// 获取对应的基本类型的以String为参数的构造器
        return constructor.newInstance(group);
    };

    /**
     * 通过代理对象来实现对文件操作接口中所有方法的实现。
     * @param foClass FileOpr的子接口的类型信息
     * @return (FileOpr)代理对象
     */
    public static FileOpr newInstance(Class<? extends FileOpr> foClass){
        // 多态:返回类型为父接口
        // 类加载器,类对象的数组,(代理对象 Object,方法(指的是read&write) method(args1,args2...),方法实参 Object[])
        // 返回(FileOpr)代理对象->通过代理的核心类Proxy创建实例
        return (FileOpr) Proxy.newProxyInstance(foClass.getClassLoader(), new Class[]{foClass}, (proxy, method, args) -> {
            FileAnt ant = method.getAnnotation(FileAnt.class);

            if(null == ant){
                throw new RuntimeException("please set annotation @FileAnt in method");
            }// 若子接口是不存在注解的,就会选择抛出异常
            String path = ant.path();// 获取被注解的类的路径
            FileAnt.FileType type = ant.type();// 获取被注解的类的类型
            File file = new File(path);// 创建文件对象->用于判断路径是否存在
            String methodName = method.getName();// 获取方法名字,来判断是读还是写方法
            switch (methodName){
                case "read":
                    if(!file.exists()){
                        throw new RuntimeException("File not found Exception :" + path);
                    }
                    String desc = ant.desc();
                    Pattern pat = null;
                    if(type == FileAnt.FileType.TXT){
                        pat = Pattern.compile(desc);
                    }
                    BufferedReader br = null;
                    Class aClass = (Class) args[0];// <T> List<T> read(Class<T> tClass); 通过参数获取类
                    Constructor noArgsConstructor = aClass.getConstructor();// 无参构造
                    Map<Integer, Method> setters = parseMethod(aClass);
                    List list = new ArrayList();
                    try {
                        br = new BufferedReader(new FileReader(file));
                        String line;
                        while(null != (line = br.readLine())){
                            switch (type){
                                case TXT:
                                    Matcher matcher = pat.matcher(line);
                                    if(matcher.matches()){
                                        Object obj = noArgsConstructor.newInstance();;
                                        for (int i = 1; i <= matcher.groupCount(); i++) {
                                            Method methodSetter = setters.get(i);
                                            methodSetter.invoke(obj,parameterParse(methodSetter,matcher.group(i)));
                                        }
                                        list.add(obj);
                                    }
                                    break;
                                case CSV:

                                    break;
                                case JSON:

                                    break;
                            }
                        }
                    }catch(Exception e){
                        throw new RuntimeException(e.getMessage());
                    }finally{
                        if(null != br){
                            try {
                                br.close();
                            }catch(Exception e){
                                e.printStackTrace();
                            }
                        }
                    }
                    return list;
                case "write":
                    if (!file.getParentFile().exists()) {// 文件是可以不存在的,但是文件的上一个目录是必须存在的
                        throw new RuntimeException("Directory for File to write not existed Exception :"+file.getParent());
                    }
                    List array = (List)args[0];// array为write()方法的参数,即为对象的List集合
                    BufferedWriter bw = null;// 如果写成bw = null,在try中进行再一次赋值就是变凉了
                    try{
                        bw = new BufferedWriter(new FileWriter(file,ant.append()));
                        for(Object obj:array){
                            bw.write(obj.toString());
                            bw.newLine();// 一个对象算一行
                        }
                        bw.flush();// bw.close()会默认清空一次缓存,但为了防止因为bw.close()报错而导致的没有执行清空缓存的操作,于是要有bw.flush()
                    }catch(Exception e){
                        throw new RuntimeException(e.getMessage());
                    }finally {
                        if(null != bw){
                            try {
                                bw.close();
                            }catch(Exception e){
                                e.printStackTrace();
                            }
                        }
                    }
                    break;
                default:
                    throw new RuntimeException(methodName+" Not Support Exception,only read(...) and write(...) supported." );
            }
            return null;
        });
    }

}
FileAnt注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface FileAnt {
    enum FileType{
        CSV,JSON,TXT
     }// 文件的类型:通过枚举进行限定
    String path();// 文件路径:返回类型为字符串,默认为空字符串
    FileType type() default FileType.CSV;// 文件类型:返回类型为FileType,默认为CSV格式
    String desc() default "";// 正则表达式
    boolean append() default true;// write的模式(true为追加,false为覆盖)
}
SetterOrder注解
/**
 * 用于 为每一个set方法提供特定的编号 防止getMethods()所出来的方法具有随机性
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)// 运用于set方法上的注解
public @interface SetterOrder {
    int order();
}

FileOprTest测试类
public static void main(String[] args) {
  final FileOpr fileOpr = EntityFileMapperFactory.newInstance(CommodityFileOpr.class);// 动态代理面向抽象,获取资料类型信息实现父类的接口
  /*final List<Object> list = Arrays.asList(
     new Commodity("iPhone14", "Apple", 1023L, 120.23),
     new Commodity("HuaWei Metail", "华为", 2023L, 3600.00),
     new Commodity("Lenovo 小新", "Lenovo", 30986L, 9900.00)
   );
   fileOpr.write(list);*/
  final List<Commodity> list = fileOpr.read(Commodity.class);
  list.forEach(c-> System.out.println(c));
}

调用<T> void write(List<T> list)<T> List<T> read(Class<T> tClass)

Commodity类——商品类
/**
 * 商品类:为了在CommodityFileOpr中进行写操作的时候能够不用toString,而是重新修改toString的格式。
 *        便于构造出商品对象组成的list
 */

public class Commodity {
    private String sku;// sku:便于搜索优化的长关键字名称
    private String brand;
    private Long categoryId;// 商品的分类(最少二级分类,最多三级分类)
    private Double price;

    public Commodity(){

    };
    public Commodity(String sku, String brand, Long categoryId, Double price){
        this.sku = sku;
        this.brand = brand;
        this.categoryId = categoryId;
        this.price = price;
    }
    public String getSku() {
        return sku;
    }
    @SetterOrder(order = 1)
    public void setSku(String sku) {
        this.sku = sku;
    }
    public String getBrand() {
        return brand;
    }
    @SetterOrder(order = 2)
    public void setBrand(String brand) {
        this.brand = brand;
    }

    public Long getCategoryId() {
        return categoryId;
    }
    @SetterOrder(order = 3)
    public void setCategoryId(Long categoryId) {
        this.categoryId = categoryId;
    }

    public Double getPrice() {
        return price;
    }
    @SetterOrder(order = 4)
    public void setPrice(Double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return String.format("Commodity{sku=\"%s\",brand=\"%s\",categoryId=%d,price=%.2f}",sku,brand,categoryId,price);// 在原来toString()的基础上进行修改
    }
}

最终的实现结果如图:

写操作

image.png

读操作

image.png

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。