数据映射框架之三大神器:反射、注解、动态代理
数据映射框架
定义:“定死”,适用于快速开发。
特征
- 无需实现接口
- 低代码
数据映射
数据库中的数据(表)----(框架级别)---- Java中的数据(类/实体)-- 前端的数据(Json格式的数据)
反射
定义:获取一个字节码文件对象,然后获取该类的属性和方法(即对类进行解剖),获取之后再将其映射成一个个对象,动态解析类的结构(属性 构造器 方法)。
特征
- 代码追踪的时候可能直接进入到抽象层,找不到具体代码。
常见异常
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
-
元素类型只能是基本的数据类型
-
注解中可以一个注解中包含多个不同方法所需的注解,在注解赋值的时候只需要各取所需
注解的调用
代码
// 常用的元注解(使用反射时必须要有)
@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表示的即为商品类的对象
// 实体和文件的映射工厂
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()的基础上进行修改
}
}
最终的实现结果如图:
写操作
读操作
- 点赞
- 收藏
- 关注作者
评论(0)