当前位置: 首页 > news >正文

注解案例:山寨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

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

 

相关文章:

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

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 上篇讲了什么是注解&am…...

Codeforces Round 822 (Div. 2)(D前缀和+贪心加血量)

A.选三条相邻的边遍历一次求最小值 #include<bits/stdc.h> using namespace std; const int N 1e610,mod1e97; #define int long long int n,m; vector<int> g[N]; int a[N]; void solve() {cin>>n;int res2e18;for(int i1;i<n;i) cin>>a[i];sort…...

不停的挖掘硬盘的最大潜能

从 NAS 上退休的硬盘被用在了监控的存储上了。 随着硬盘使用寿命的接近尾声&#xff0c;感觉就是从高附加值数据到低附加值数据上。监控数据只会保留那么几个月的时间&#xff0c;很多时候都会被覆盖重新写入。 有人问为什么监控数据不保留几年的&#xff0c;那是因为监控数据…...

Java游戏之飞翔的小鸟

前言 飞翔的小鸟 小游戏 可以作为 java入门阶段的收尾作品 &#xff1b; 需要掌握 面向对象的使用以及了解 多线程&#xff0c;IO流&#xff0c;异常处理&#xff0c;一些java基础等相关知识。一 、游戏分析 1. 分析游戏逻辑 &#xff08;1&#xff09;先让窗口显示出来&#x…...

PostgreSQL (Hologres) 日期生成

PostgreSQL 生成指定日期下一个月的日期 &#xff08;在Hologres中&#xff0c;不支持递归查询&#xff09; SELECTto_char(T, YYYYMMDD)::int4 AS date_int,date(T) AS date_str,date_part(year, T)::int4 AS year_int,date_part(month, T)::int4 AS month_int,date_part(da…...

HCIP-一、RSTP 特性及安全

一、RSTP 特性及安全 实验拓扑实验需求及解法 实验拓扑 实验需求及解法 //1.SW1/2/3是企业内部交换机&#xff0c;如图所示配置各设备名称。 //2.配置VLAN&#xff0c;需求如下&#xff1a; //1&#xff09;SW1/2/3创建vlan10 [SW1]vlan batch 10 [SW2]vlan batch 10 [SW3]vla…...

智能高效的转运机器人,为物流行业注入新动力

在当今社会&#xff0c;随着科技的不断发展&#xff0c;机器人已经逐渐融入到我们的生活中。其中&#xff0c;转运机器人作为物流行业的新秀&#xff0c;正以其高效、智能的特点&#xff0c;引起了广泛的关注。 转运机器人&#xff0c;是指能够自主进行物品搬运和运输的机器人…...

操作系统(三)| 进程管理下 经典进程问题分析 线程 死锁

文章目录 6.经典进程同步问题6.1 生产者-消费者问题 (既有同步又有互斥)6.2 读者-写者问题6.3 哲学家进餐问题6.4理发师问题 7. 进程之间通信7.1 共享存储区7.2 消息传递7.3 管道 8.线程8.1 线程的实现机制 9 进程调度9.1 调度方式9.2 常见算法先来先服务 FCFS短进程优先 SPN最…...

vue3使用tsx自定义弹窗组件

1.在ts代码中使用css 我这里使用了styils/vue&#xff0c;npm install styils/vue --save-dev&#xff0c;在tsx文件中引入即可&#xff1a;import { styled } from "styils/vue"; 2.在tsx中初始化组件&#xff0c;创建在src的utils目录中创建messagebox.tsx impo…...

[笔记] 错排问题 #错排

参考&#xff1a;刷题笔记-错排问题总结 错排问题&#xff1a; 一个n个元素的排列&#xff0c;若一个排列中所有的元素都不在自己原来的位置上&#xff0c;那么这样的一个排列就称为原排列的一个错排。而研究一个排列的错排个数的问题&#xff0c;就称为错排问题&#xff08;或…...

Ajax进阶

前后端传输数据的编码格式(contentType) # 提示: 主要研究post请求数据的编码格式.get请求数据就是直接放在url?号后面的每个参数之间用&符连接, 如下:url?usernamejason&password123 # 可以朝后端发送post请求的方式1 .form表单2. ajax请求# 基于post请求. 前后端传…...

RedisTemplate使用详解

RedisTemplate介绍StringRedisTemplate介绍RedisConnectionFactory介绍RedisConnectionFactory源码解析 RedisOperations介绍RedisOperations源码解析 RedisTemplate使用连接池配置RedisTemplate连接池连接池配置 RedisTemplate应用场景RedisTemplate主要特点RedisTemplate使用…...

6.Gin 路由详解 - GET POST 请求以及参数获取示例

6.Gin 路由详解 - GET POST 请求以及参数获取示例 GET POST 请求以及参数获取示例 Get 请求&#xff1a;获取 Quary 参数 // 获取query参数示例&#xff1a;GET /user?uid20&namejack&page1 r.GET("/user", func(c *gin.Context) {// 获取参数// Query获取参…...

CMakeLists.txt基础指令与cmake-gui生成VS项目的步骤

简介 本博客主要介绍cmake的基本指令&#xff0c;同时&#xff0c;很多使用Visual Studio小白从Gitbub下载项目源码后&#xff0c;看到CMakeLists.txt&#xff0c;不知道如何使用Visual Studio编译源码&#xff1b;针对以上问题&#xff0c;做一下简单操作与解释&#xff0c;方…...

IT应用运维最常用指标

可用性&#xff08;Availability&#xff09; 系统或服务在特定时间范围内可用的百分比。 计算方式&#xff1a;&#xff08;总时间 - 不可用时间&#xff09;/ 总时间 * 100%。 参考值&#xff1a;99.9%。 应用范围&#xff1a;应用系统、网络设备。 故障率&#xff08;Fa…...

Go中各种newreader和newbuffer的使用

一、bytes.NewBuffer和bytes.NewReader func main() {var byteArr []bytebuf : bytes.NewBuffer(byteArr)buf.Write([]byte("今天不错"))fmt.Println(buf.String()) }package mainimport ("bytes""fmt" )func main() {data : []byte("路多…...

visual studio 如何建立 C 语言项目

安装这个 模块。 新建 空项目 创建完成 写demo 点击运行&#xff1a;...

app小程序定制开发的优势|企业软件网站建设

app小程序定制开发的优势|企业软件网站建设 小程序定制开发是目前互联网行业中备受关注的领域之一。随着智能手机的普及和移动互联网的迅猛发展&#xff0c;越来越多的企业和个人开始重视小程序的潜力&#xff0c;并积极寻求定制开发的服务。那么&#xff0c;为什么小程序定制开…...

物联网AI MicroPython学习之语法 WDT看门狗外设

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; WDT 介绍 模块功能: 看门狗WDT&#xff08;WatchDog Timer&#xff09;外设驱动模块 接口说明 WDT - 构建WDT对象 函数原型&#xff1a;WDT(timeout)参数说明&#xff1a; 参数类型必选参数&#xff1f…...

JVM线程的几种状态

1.New 新建的线程&#xff0c;线程还没启动。 2.Runnable 线程正在运行或者等待操作系统中的其他资源&#xff0c;例如线程运行过程中&#xff0c;系统分配资源给其他操作&#xff0c;此时这个线程还是Runnable状态&#xff0c;可以理解为可运行的线程。 3.Blocked 阻塞状…...

conda相比python好处

Conda 作为 Python 的环境和包管理工具&#xff0c;相比原生 Python 生态&#xff08;如 pip 虚拟环境&#xff09;有许多独特优势&#xff0c;尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处&#xff1a; 一、一站式环境管理&#xff1a…...

【Python】 -- 趣味代码 - 小恐龙游戏

文章目录 文章目录 00 小恐龙游戏程序设计框架代码结构和功能游戏流程总结01 小恐龙游戏程序设计02 百度网盘地址00 小恐龙游戏程序设计框架 这段代码是一个基于 Pygame 的简易跑酷游戏的完整实现,玩家控制一个角色(龙)躲避障碍物(仙人掌和乌鸦)。以下是代码的详细介绍:…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

Python:操作 Excel 折叠

💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...

【git】把本地更改提交远程新分支feature_g

创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

Element Plus 表单(el-form)中关于正整数输入的校验规则

目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入&#xff08;联动&#xff09;2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...

Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?

Redis 的发布订阅&#xff08;Pub/Sub&#xff09;模式与专业的 MQ&#xff08;Message Queue&#xff09;如 Kafka、RabbitMQ 进行比较&#xff0c;核心的权衡点在于&#xff1a;简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...

Netty从入门到进阶(二)

二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架&#xff0c;用于…...

08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险

C#入门系列【类的基本概念】&#xff1a;开启编程世界的奇妙冒险 嘿&#xff0c;各位编程小白探险家&#xff01;欢迎来到 C# 的奇幻大陆&#xff01;今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类&#xff01;别害怕&#xff0c;跟着我&#xff0c;保准让你轻松搞…...

苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会

在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...