注解案例:山寨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 {@MyBeforepublic void init() {System.out.println("初始化...");}@MyAfterpublic void destroy() {System.out.println("销毁...");}@MyTestpublic void testSave() {System.out.println("save...");}@MyTestpublic void testDelete() {System.out.println("delete...");}
}

MyJunitFrameWork(读取注解)

/*** 这个就是注解三部曲中最重要的:读取注解并操作* 相当于我们使用Junit时看不见的那部分(在隐秘的角落里帮我们执行标注了@Test的方法)** @author mx*/
public class MyJunitFrameWork {public static void main(String[] args) throws Exception {// 1.先找到测试类的字节码:EmployeeDAOTestClass 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.classClass 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");}// 得到jdbcTemplateprivate 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_userStringBuilder 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语句模板和模板所需的参数,插入Userint num = jdbcTemplate.update(sql.toString(), params);System.out.println(num);}
}

UserDao

public class UserDao extends BaseDao<User> {@Overridepublic 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

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

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ros2机器人上位机与下位机连接方式(转载)

从硬件连接、通信协议和软件设计开发&#xff0c;上位机如何控制下位机&#xff1f; 由你创科技2023-09-07 10:38广东 随着科技的不断发展&#xff0c;自动化控制系统已经广泛应用于各个行业。在自动化控制系统中&#xff0c;上位机和下位机是两个重要的组成部分。上位机主要…

【漏洞复现】泛微E-Office信息泄露漏洞(CVE-2023-2766)

漏洞描述 Weaver E-Office是中国泛微科技(Weaver)公司的一个协同办公系统。 Weaver E-Office 9.5版本存在安全漏洞。攻击者利用该漏洞可以访问文件或目录。 影响版本 Weaver E-Office 9.5版本 免责声明 技术文章仅供参考,任何个人和组织使用网络应当遵守宪法法律,遵…

vim模式用法总结

0.前言 我们用gcc编译文件的时候&#xff0c;如果发生了下面的错误&#xff0c;那么如何用vim打开的时候就定位到&#xff1f; 我们可以知道&#xff0c;这是第6行出现了错误&#xff1b; 所以我们使用vim打开的时候多输入个这个&#xff0c;我们就可以快速定位了 vim test.c 6…

外卖小程序系统:数字化时代餐饮业的技术奇迹

在当今数字化时代&#xff0c;外卖小程序系统正以其强大的技术背后支持&#xff0c;成为餐饮业务的一项奇迹。这个系统不仅提供了便捷的点餐体验&#xff0c;更通过先进的技术手段&#xff0c;实现了高效订单处理、智能推荐以及实时配送追踪。下面&#xff0c;我们将深入探讨外…

【六祎 - Dubbo】Dubbo 应用 XML配置分析;Dubbo 配置篇;Dubbo参考手册

Dubbo 应用 XML配置分析 演示案例&#xff1a;提供者代码xml配置消费者代码xml配置 参考地址&#xff1a; 手动配置 https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/config/overview/ 配置说明 xml配置 https://cn.dubbo.apache.org/zh-cn/ov…

人工智能AI 全栈体系(十三)

第二章 计算机是如何学会下棋的 人类棋手在下棋时&#xff0c;会根据自己的经验只考虑在当前棋局下最重要的几个可能的走法&#xff0c;但是计算机没有这种经验。 知识太复杂了&#xff0c;需要考虑很多具体的情况&#xff0c;一旦知识总结的不到位&#xff0c;可能就会出现大…

#gStore-weekly | gBuilder功能详解之表单录入

gBuilder除了可以提供结构化数据映射以及非结构化数据抽取两种构建知识图谱的方式以外&#xff0c;还提供了表单录入的方式来构建知识图谱的数据&#xff0c;用户只需要根据设计好的schema将实体、属性以及关系通过填写表单的形式录入&#xff0c;再通过一键生成NT文件即可获得…

酷开科技丨这么好用的酷开系统,不用真的会后悔!

掀开一幕幕精彩剧情&#xff0c;手机已经成为了我们身边必不可少的追剧神器。在这个信息爆炸的时代&#xff0c;我们渴望能够随时随地享受到精彩的影视作品&#xff0c;尤其是在家的休息的时候&#xff0c;希望电视也能同手机一样&#xff0c;想看啥就搜啥。酷开科技大内容战略…

系列五、线程间通信

一、synchronized实现 1.1、案例一&#xff08;2个线程交替对变量执行1、-1操作&#xff0c;来10轮&#xff09; 1.1.1、资源类ShareDataOne /*** Author : 一叶浮萍归大海* Date: 2023/11/20 10:44* Description: 资源类* 说明&#xff1a;2个线程使用if判断变量的值&#…

Linux 信号

目录 1.什么是信号 2.信号的产生 处理信号的常见方式&#xff1a; 如何理解这些组合键是如何变成信号的&#xff1f; 信号发送本质​编辑 信号的种类 ​编辑 3.信号处理常见方式概览 4.产生信号 5.信号其他相关常见概念 ​编辑 6.捕捉信号 signal函数&#xff1a; …

【Go语言实战】(26) 分布式搜索引擎

Tangseng 基于Go语言的搜索引擎 github地址&#xff1a;https://github.com/CocaineCong/tangseng 详细介绍地址&#xff1a;https://cocainecong.github.io/tangseng 这两周我也抽空录成视频发到B站的&#xff5e; 本来应该10月份就要发了&#xff0c;结果一鸽就鸽到现在hh…

Conditional GAN

Text-to-Image 对于根据文字生成图像的问题&#xff0c;传统的做法就是训练一个NN&#xff0c;然后输入一段文字&#xff0c;输出对应一个图片&#xff0c;输出图片与目标图片越接近越好。存在的问题就是&#xff0c;比如火车对应的图片有很多张&#xff0c;如果用传统的NN来训…

shell编脚本概述和变量解释

shell的基本概述和规范 shell面向的对象 面向过程语言 &#xff08;开发的时候&#xff0c;需要一步一步的执行&#xff09; 做一件事情&#xff0c;排出个步骤&#xff0c;第一步干什么&#xff0c;第二步干什么&#xff0c;如果出现情况A&#xff0c;做什么处理&#xff0…

澳洲猫罐头如何?我亲自喂养过的优质猫罐头分享

猫罐头要符合三点&#xff1a;营养配方完整均衡、原料新鲜优质、生产工艺科学可靠。只有具备这些特点&#xff0c;才是品质上乘的猫罐头。 猫罐头的三个要素&#xff0c;一个都不能少。配方不均衡&#xff0c;营养就不足&#xff1b;原料不新鲜&#xff0c;生产出来的猫罐头就…

NX二次开发UF_CAM_ask_lower_limit_plane_data 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;里海NX二次开发3000例专栏 UF_CAM_ask_lower_limit_plane_data Defined in: uf_cam_planes.h int UF_CAM_ask_lower_limit_plane_data(tag_t object_tag, double origin [ 3 ] , double normal [ 3 ] ) overview 概述 Query …

鸿蒙开发|鸿蒙系统项目开发前的准备工作

文章目录 鸿蒙项目开发的基本流程介绍鸿蒙项目开发和其他项目有什么不同成为华为开发者-注册和实名认证1.登录官方网站 鸿蒙项目开发的基本流程介绍 直接上图&#xff0c;简单易懂&#xff01; 整个项目的开发通过4个模块进行&#xff1a;开发准备、开发应用、运行调试测试和发…

关于使用宝塔页面Nginx的一些注意事项:Nginx不生效情况,以及解决方案

判断Nginx是否正常运行 使用宝塔页面保存Nginx配置后&#xff0c;有的时候不生效&#xff0c;这就说明Nginx没有正常运行 可以通过以下几种方式排查 通过宝塔 如果能够打开负载状态&#xff0c;说明Nginx运行正常 如果打不开&#xff0c;说明肯定是配置文件或者什么导致ngi…

NameServer源码解析

1 模块入口代码的功能 本节介绍入口代码的功能&#xff0c;阅读源码的时候&#xff0c;很多人喜欢根据执行逻辑&#xff0c;先从入口代码看起。NameServer部分入口代码主要完成命令行参数解析&#xff0c;初始化Controller的功能。 1.1 入口函数 首先看一下NameServer的源码目…

代码随想录算法训练营第五十九天丨 单调栈02

503.下一个更大元素II 思路 做本题之前建议先做739. 每日温度 (opens new window)和 496.下一个更大元素 I (opens new window)。 这道题和739. 每日温度 (opens new window)也几乎如出一辙。 不过&#xff0c;本题要循环数组了。 关于单调栈的讲解我在题解739. 每日温度 …

el-table 对循环产生的空白列赋默认值

1. el-table 空白列赋值 对el-table中未传数据存在空白的列赋默认值0。使用el-table 提供的插槽 slot-scope&#xff1a;{{ row || ‘0’ }} 原数据&#xff1a; <el-table-column label"集镇" :propcity ><template slot-scope"{row}">{{…