注解案例:山寨Junit与山寨JPA

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

上篇讲了什么是注解,以及注解的简单使用,这篇我们一起用注解+反射模拟几个框架,探讨其中的运行原理。

山寨Junit

上一篇已经讲的很详细了,这里就直接上代码了。请大家始终牢记,用到注解的地方,必然存在三角关系,并且别忘了设置保留策略为RetentionPolicy.RUNTIME。

代码结构

案例代码

MyBefore注解(定义注解)

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

MyTest注解(定义注解)

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

MyAfter注解(定义注解)

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

EmployeeDAOTest(使用注解)

/**
 * 和我们平时使用Junit测试时一样
 *
 * @author mx
 */
public class EmployeeDAOTest {
    @MyBefore
    public void init() {
        System.out.println("初始化...");
    }

    @MyAfter
    public void destroy() {
        System.out.println("销毁...");
    }

    @MyTest
    public void testSave() {
        System.out.println("save...");
    }

    @MyTest
    public void testDelete() {
        System.out.println("delete...");
    }
}

MyJunitFrameWork(读取注解)

/**
 * 这个就是注解三部曲中最重要的:读取注解并操作
 * 相当于我们使用Junit时看不见的那部分(在隐秘的角落里帮我们执行标注了@Test的方法)
 *
 * @author mx
 */
public class MyJunitFrameWork {
    public static void main(String[] args) throws Exception {
        // 1.先找到测试类的字节码:EmployeeDAOTest
        Class clazz = EmployeeDAOTest.class;
        Object obj = clazz.newInstance();

        // 2.获取EmployeeDAOTest类中所有的公共方法
        Method[] methods = clazz.getMethods();

        // 3.迭代出每一个Method对象,判断哪些方法上使用了@MyBefore/@MyAfter/@MyTest注解
        List<Method> myBeforeList = new ArrayList<>();
        List<Method> myAfterList = new ArrayList<>();
        List<Method> myTestList = new ArrayList<>();
        for (Method method : methods) {
            if (method.isAnnotationPresent(MyBefore.class)) {
                //存储使用了@MyBefore注解的方法对象
                myBeforeList.add(method);
            } else if (method.isAnnotationPresent(MyTest.class)) {
                //存储使用了@MyTest注解的方法对象
                myTestList.add(method);
            } else if (method.isAnnotationPresent(MyAfter.class)) {
                //存储使用了@MyAfter注解的方法对象
                myAfterList.add(method);
            }
        }

        // 执行方法测试
        for (Method testMethod : myTestList) {
            // 先执行@MyBefore的方法
            for (Method beforeMethod : myBeforeList) {
                beforeMethod.invoke(obj);
            }
            // 测试方法
            testMethod.invoke(obj);
            // 最后执行@MyAfter的方法
            for (Method afterMethod : myAfterList) {
                afterMethod.invoke(obj);
            }
        }
    }
}

执行结果:

山寨JPA

要写山寨JPA需要两个技能:注解+反射。

注解已经学过了,反射其实还有一个进阶内容,之前那篇反射文章里没有提到,放在这里补充。至于是什么内容,一两句话说不清楚。慢慢来吧。

首先,要跟大家介绍泛型中几个定义(记住最后一个):

  • ArrayList<E>中的E称为类型参数变量
  • ArrayList<Integer>中的Integer称为实际类型参数
  • 整个ArrayList<E>称为泛型类型
  • 整个ArrayList<Integer>称为参数化的类型ParameterizedType

好,接下来看这个问题:

class A<T>{
    public A(){
       /*
        我想在这里获得子类B、C传递的实际类型参数的Class对象
        class java.lang.String/class java.lang.Integer
       */
    }
}

class B extends A<String>{
}

class C extends A<Integer>{
}

我先帮大家排除一个错误答案:直接T.class是错误的。

所以,你还有别的想法吗?

我觉得大部分人可能都想不到,这不是技术水平高低的问题,而是知不知道相关API的问题。知道就简单,不知道想破脑袋也没辙。

我们先不直接说怎么做,一步步慢慢来。

父类中的this是谁?

请先看下面代码:

public class Test {
	public static void main(String[] args) {
		new B();
	}
}

class A<T>{
	public A(){
        // this是谁?A还是B?
		Class clazz = this.getClass();
		System.out.println(clazz.getName());
	}
}

class B extends A<String>{
}

请问,clazz.getName()打印的是A还是B?

答案是:B。因为从头到尾,我们new的是B,这个Demo里至始至终只初始化了一个对象,所以this指向B。

好的,到这里我们已经迈出了第一步:在泛型父类中得到了子类的Class对象!

如何根据子类Class获取父类Class?

我们再来分析:

class A<T>{
	public A(){
        //clazz是B.class
		Class clazz = this.getClass();
	}
}
class B extends A<String>{
}

现在我们已经在class A<T>中得到子类B的Class对象,而我们想要得到的是父类A<T>中泛型的Class对象。且先不说泛型的Class对象,我们先考虑如何通过子类B的Class对象获得父类A的Class对象?

查阅API文档,我们发现有这么个方法:

Generic Super Class,直译就是“带泛型的父类”。也就是说调用getGenericSuperclass()就会返回泛型父类的Class对象。这非常符合我们的情况,因为Class A确实是泛型类。试着打印一下:

如何获取带实际类型参数的父类Class?

上面已经证明通过子类Class是可以获取父类Class的,接下来我们尝试如何获取带实际类型参数的父类Class。

虽然genericSuperclass是Type接收的,但可以看出实际类型为ParameterizedTypeImpl:

这里我们不去关心Type、ParameterizedType还有Class之间的继承关系,总之以我们多年的编码经验,子类的方法总是更多,所以毫不犹豫地向下转型:

public class JpaTest {
    public static void main(String[] args) {
        new B();
    }
}

class A<T> {
    public A() {
        Class<? extends A> subClass = this.getClass();
        // 得到泛型父类
        Type genericSuperclass = subClass.getGenericSuperclass();

        // 本质是ParameterizedTypeImpl,可以向下强转
        ParameterizedType parameterizedTypeSuperclass = (ParameterizedType) genericSuperclass;

        // 强转后可用的方法变多了,比如getActualTypeArguments()可以获取Class A<String>的泛型的实际类型参数
        Type[] actualTypeArguments = parameterizedTypeSuperclass.getActualTypeArguments();

        // 由于A类只有一个泛型,这里可以直接通过actualTypeArguments[0]得到子类传递的实际类型参数
        Class actualTypeArgument = (Class) actualTypeArguments[0];
        System.out.println(actualTypeArgument);
        System.out.println(subClass.getName());
    }
}

class B extends A<String> {
}

class C extends A<Integer> {
}

把main方法中的new B()换成new C():

这下成了!现在我们能在父类中得到子类继承时传递的泛型的实际类型参数。

接下来正式开始编写山寨JPA。

第一版JPA

需要额外依赖数据库连接池,这里使用dbcp:

<dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.4</version>
    <scope>test</scope>
</dependency>

User

CREATE TABLE `User` (
  `name` varchar(255) DEFAULT NULL COMMENT '名字',
  `age` tinyint(4) DEFAULT NULL COMMENT '年龄'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@Data
@AllArgsConstructor
public class User {
    private String name;
    private Integer age;
}

BaseDao<T>

public class BaseDao<T> {
    private static BasicDataSource datasource;

    // 静态代码块,设置连接数据库的参数
    static {
        datasource = new BasicDataSource();
        datasource.setDriverClassName("com.mysql.jdbc.Driver");
        datasource.setUrl("jdbc:mysql://localhost:3306/test");
        datasource.setUsername("root");
        datasource.setPassword("123456");
    }

    // 得到jdbcTemplate
    private JdbcTemplate jdbcTemplate = new JdbcTemplate(datasource);
    // DAO操作的对象
    private Class<T> beanClass;

    /**
     * 构造器
     * 初始化时完成对实际类型参数的获取,比如BaseDao<User>插入User,那么beanClass就是user.class
     */
    public BaseDao() {
        beanClass = (Class<T>) ((ParameterizedType) this.getClass()
                .getGenericSuperclass())
                .getActualTypeArguments()[0];
    }

    public void add(T bean) {
        // 得到User对象的所有字段
        Field[] declaredFields = beanClass.getDeclaredFields();

        // 拼接sql语句,表名直接用POJO的类名,所以创建表时,请注意写成User,而不是t_user
        StringBuilder sql = new StringBuilder()
                .append("insert into ")
                .append(beanClass.getSimpleName())
                .append(" values(");
        for (int i = 0; i < declaredFields.length; i++) {
            sql.append("?");
            if (i < declaredFields.length - 1) {
                sql.append(",");
            }
        }
        sql.append(")");

        // 获得bean字段的值(要插入的记录)
        ArrayList<Object> paramList = new ArrayList<>();
        try {
            for (Field declaredField : declaredFields) {
                declaredField.setAccessible(true);
                Object o = declaredField.get(bean);
                paramList.add(o);
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        int size = paramList.size();
        Object[] params = paramList.toArray(new Object[size]);

        // 传入sql语句模板和模板所需的参数,插入User
        int num = jdbcTemplate.update(sql.toString(), params);
        System.out.println(num);
    }
}

UserDao

public class UserDao extends BaseDao<User> {
	@Override
	public void add(User bean) {
		super.add(bean);
	}
}

测试类

public class UserDaoTest {
    public static void main(String[] args) {
        UserDao userDao = new UserDao();
        User user = new User("bravo1988", 20);
        userDao.add(user);
    }
}

测试结果

桥多麻袋!这个和JPA有半毛钱关系啊!上一篇的注解都没用上!!

不错,细心的朋友肯定已经发现,我的代码实现虽然不够完美,但是最让人蛋疼的还是:要求数据库表名和POJO的类名一致,不能忍...

第二版JPA

于是,我决定抄袭一下JPA的思路,给我们的User类加一个Table注解,用来告诉程序这个POJO和数据库哪张表对应:

CREATE TABLE `t_jpa_user` (
  `name` varchar(255) DEFAULT NULL COMMENT '名字',
  `age` tinyint(4) DEFAULT NULL COMMENT '年龄'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

@Table注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Table {
	String value();
}

新的User类(类名加了@Table注解)

这下真的是山寨JPA了~

另类注解

学习注解时,我们一直强调3个步骤:

  • 定义注解
  • 使用注解
  • 读取注解,完成操作

但实际上,注解最最基本的功能是“标注”,如果我们只需要注解的“标注”功能,不用额外操作时,就可以省略第3步。

比如,日常开发时我们经常需要注明哪些参数可以为null:

此时可以借助注解达到相同甚至更好的效果:

/**
 * 仅用于标记参数是否可以为null
 */
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface Nullable {

}

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/174357.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

注册中心CAP架构剖析

Nacos 支持 AP 或 CP AP Nacos 通过临时节点实现 AP 架构&#xff0c;将服务列表放在内存中&#xff1b; CP Nacos 通过持久化节点实现 CP 架构&#xff0c;将服务列表放在文件中&#xff0c;并同步到内存&#xff0c;通过 Raft 协议算法实现&#xff1b; 通过配置 epheme…

集中签约 深算院YashanDB关键行业商用提速

11月17日&#xff0c;深圳计算科学研究院在2023深圳企业创新发展大会主论坛上&#xff0c;与金融、能源和交通等关键行业的7家标杆企业签约&#xff0c;签约总金额近亿元&#xff0c;标志着YashanDB在重点行业的商用落地迈入全面提速阶段。 会上&#xff0c;深圳计算科学研究院…

人工智能的时代---AI的影响

人工智能&#xff08;AI&#xff09;是当前科技领域的一个热门话题&#xff0c;它正在以前所未有的速度改变着我们的生活方式和工作方式。从智能家居到自动驾驶&#xff0c;从智能医疗到智能金融&#xff0c;人工智能正在渗透到我们生活的方方面面。在这篇文章中&#xff0c;我…

C语言基础---函数、数组

目录 一、函数 二、数组​ 一、函数 交换两个数&#xff1a; 发现这样并没有交换a和b的值&#xff0c;只是交换了x和y的值&#xff0c;这是因为&#xff1a; //当实参传递给形参的时候&#xff0c;形参是实参的一份临时拷贝 //对形参的修改不能改变实参 实参与形参是…

禁止linux shell 终端显示完整工作路径,如何让linux bash终端不显示当前工作路径

在操作linux时&#xff0c;默认安装的linux终端会显示当前完整的工作目录&#xff0c;如果目录比较短还是可以接收&#xff0c;如果目录比较长&#xff0c;就显得比较别扭&#xff0c;操作起来不方便&#xff0c;因此需要关闭这种功能。 要关闭这个功能&#xff0c;请按如下步骤…

Vue 3实战:打造交互丰富的任务管理应用

Vue 3实战&#xff1a;打造交互丰富的任务管理应用 前言搭建Vue 3项目步骤 1: 安装Vue CLI 3步骤 2: 创建Vue 3项目步骤 3: 进入项目目录步骤 4: 启动项目步骤 5: 查看项目结构 组件设计与复用1. **组件的职责单一化:**2. **Props传递:**3. **插槽&#xff08;Slots&#xff09…

【C++进阶之路】第七篇:异常

文章目录 一、C语言传统的处理错误的方式二、 C异常概念三、异常的使用1.异常的抛出和捕获2.异常的重新抛出3.异常安全4.异常规范 四、自定义异常体系五、C标准库的异常体系&#xff08;了解&#xff09;六、异常的优缺点 一、C语言传统的处理错误的方式 传统的错误处理机制&am…

【docker下安装jenkins】(一)

目的&#xff1a;在Linux操作系统&#xff08;x86_64)下&#xff0c;使用docker部署jenkins&#xff0c;python使用压缩包安装 安装jenkins的步骤 &#xff11;、编排jenkins的docker-compose.yml文件 说明&#xff1a;这里遇到部署jenkins后&#xff0c;占用内存8G,所以重新…

OpenAI宫斗大戏,奥特曼面临的选择

首先不得不说&#xff0c;这件事情进展真快&#xff0c;三四天时间之内&#xff0c;大量的消息&#xff0c;各种不同渠道的&#xff0c;各种不同角度的&#xff0c;其中也包括各种决策&#xff0c;速度之快真的是应接不暇&#xff0c;仿佛在看真人秀一般 这里简单帮大家梳理一…

【ArcGIS Pro微课1000例】0034:矢量数据几何校正案例(Spatial Adjustment)

本案例讲解矢量数据几何校正&#xff0c;根据一个矢量数据去校正另外一个矢量数据。 文章目录 一、加载实验数据二、空间校正三、注意事项 一、加载实验数据 在ArcGIS Pro中加载数据效果如下&#xff1a; design&#xff1a;需要校正的数据图层planroadcenter&#xff1a;目标…

2023.11.20使用flask做一个简单图片浏览器

2023.11.20使用flask做一个简单图片浏览器 功能&#xff1a; &#xff08;1&#xff09;输入指定路径&#xff0c;打开文件夹 &#xff08;2&#xff09;判断文件格式为图片 &#xff08;3&#xff09;在前端进行预览 &#xff08;4&#xff09;使用bootstrap进行简单美化 ma…

数据仓库

一. 各种名词解释 1.1 ODS是什么&#xff1f; ODS层最好理解&#xff0c;基本上就是数据从源表拉过来&#xff0c;进行etl&#xff0c;比如mysql 映射到hive&#xff0c;那么到了hive里面就是ods层。 ODS 全称是 Operational Data Store&#xff0c;操作数据存储.“面向主题的…

【代码随想录】算法训练计划28

回溯 1、491. 递增子序列 题目&#xff1a; 给你一个整数数组 nums &#xff0c;找出并返回所有该数组中不同的递增子序列&#xff0c;递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。 数组中可能含有重复元素&#xff0c;如出现两个整数相等&#xff0c;也可以…

YOLOv7(目标检测)入门教程详解---检测,推理,训练

目录 一.前言 二.yolov7源码下载 三.detect&#xff08;检测&#xff09; 四.Train&#xff08;训练&#xff09; 数据准备&#xff1a; labellmg: 配置训练的相关文件 配置数据集文件 正式训练&#xff1a; 推理&#xff1a; 推理效果&#xff1a; 五.总结 一.前言 …

MyBatis Generator 插件 详解自动生成代码

MyBatis Generator&#xff08;MBG&#xff09;是MyBatis和iBATIS的代码生成器。可以生成简单CRUD操作的XML配置文件、Mapper文件(DAO接口)、实体类。实际开发中能够有效减少程序员的工作量&#xff0c;甚至不用程序员手动写sql。 它将为所有版本的MyBatis以及版本2.2.0之后的i…

如何在3dMax中使用Python按类型选择对象?

如何在3dMax中使用Python按类型选择对象&#xff1f; 3dMax提供了pymxs API&#xff0c;这是MAXScript的Python包装器&#xff0c;可帮助您扩展和自定义3dMax&#xff0c;并更轻松地将其集成到基于Python的管道中。 pymxs模块包含一个运行时成员&#xff0c;该成员提供对MAXSc…

2021秋招-数据结构-栈、队列、数组、列表

栈、队列、数组、列表 实现方式 队列 class Queue:def __init__(self):self.items []def enqueue(self, item):self.items.append(item)def dequeue(self):return self.items.pop(0)def empty(self):return self.size() 0def size(self):return len(self.items)应用: 约瑟…

redis的数据类型的增删改查

redis的高可用 在集群中有一个非常重要的指标&#xff0c;提供服务的时间的百分比&#xff08;365天&#xff09;99.9% redis的高可用含义更加宽泛&#xff0c;正常服务是指标之一&#xff0c;数据容量的扩展&#xff0c;数据的安全性 在redis中实现高可用技术 持久化&…

【Flink】Process Function

目录 1、ProcessFunction解析 1.1 抽象方法.processElement() 1.2 非抽象方法.onTimer() 2、Flink中8个不同的处理函数 2.1 ProcessFunction 2.2 KeyedProcessFunction 2.3 ProcessWindowFunction 2.4 ProcessAllWindowFunction 2.5 CoProcessFunction 2.6 ProcessJo…

CentOS7安装Docker遇到的问题笔记

笔记/朱季谦 以下是笔者本人学习搭建docker过程当中记录的一些实践笔记&#xff0c;过程当中也遇到了一些坑&#xff0c;但都解决了&#xff0c;就此记录&#xff0c;留作以后再次搭建时可以直接参考。 一、首先&#xff0c;先检查CentOS版本&#xff0c;保证在CentOS7版本以…