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


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

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

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

    山寨Junit

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

    代码结构

    案例代码

    MyBefore注解(定义注解)

    1. @Retention(RetentionPolicy.RUNTIME)
    2. @Target(ElementType.METHOD)
    3. public @interface MyBefore {
    4. }

    MyTest注解(定义注解)

    1. @Retention(RetentionPolicy.RUNTIME)
    2. @Target(ElementType.METHOD)
    3. public @interface MyTest {
    4. }

    MyAfter注解(定义注解)

    1. @Retention(RetentionPolicy.RUNTIME)
    2. @Target(ElementType.METHOD)
    3. public @interface MyAfter {
    4. }

    EmployeeDAOTest(使用注解)

    1. /**
    2. * 和我们平时使用Junit测试时一样
    3. *
    4. * @author mx
    5. */
    6. public class EmployeeDAOTest {
    7. @MyBefore
    8. public void init() {
    9. System.out.println("初始化...");
    10. }
    11. @MyAfter
    12. public void destroy() {
    13. System.out.println("销毁...");
    14. }
    15. @MyTest
    16. public void testSave() {
    17. System.out.println("save...");
    18. }
    19. @MyTest
    20. public void testDelete() {
    21. System.out.println("delete...");
    22. }
    23. }

    MyJunitFrameWork(读取注解)

    1. /**
    2. * 这个就是注解三部曲中最重要的:读取注解并操作
    3. * 相当于我们使用Junit时看不见的那部分(在隐秘的角落里帮我们执行标注了@Test的方法)
    4. *
    5. * @author mx
    6. */
    7. public class MyJunitFrameWork {
    8. public static void main(String[] args) throws Exception {
    9. // 1.先找到测试类的字节码:EmployeeDAOTest
    10. Class clazz = EmployeeDAOTest.class;
    11. Object obj = clazz.newInstance();
    12. // 2.获取EmployeeDAOTest类中所有的公共方法
    13. Method[] methods = clazz.getMethods();
    14. // 3.迭代出每一个Method对象,判断哪些方法上使用了@MyBefore/@MyAfter/@MyTest注解
    15. List<Method> myBeforeList = new ArrayList<>();
    16. List<Method> myAfterList = new ArrayList<>();
    17. List<Method> myTestList = new ArrayList<>();
    18. for (Method method : methods) {
    19. if (method.isAnnotationPresent(MyBefore.class)) {
    20. //存储使用了@MyBefore注解的方法对象
    21. myBeforeList.add(method);
    22. } else if (method.isAnnotationPresent(MyTest.class)) {
    23. //存储使用了@MyTest注解的方法对象
    24. myTestList.add(method);
    25. } else if (method.isAnnotationPresent(MyAfter.class)) {
    26. //存储使用了@MyAfter注解的方法对象
    27. myAfterList.add(method);
    28. }
    29. }
    30. // 执行方法测试
    31. for (Method testMethod : myTestList) {
    32. // 先执行@MyBefore的方法
    33. for (Method beforeMethod : myBeforeList) {
    34. beforeMethod.invoke(obj);
    35. }
    36. // 测试方法
    37. testMethod.invoke(obj);
    38. // 最后执行@MyAfter的方法
    39. for (Method afterMethod : myAfterList) {
    40. afterMethod.invoke(obj);
    41. }
    42. }
    43. }
    44. }

    执行结果:

    山寨JPA

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

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

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

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

    好,接下来看这个问题:

    1. class A{
    2. public A(){
    3. /*
    4. 我想在这里获得子类B、C传递的实际类型参数的Class对象
    5. class java.lang.String/class java.lang.Integer
    6. */
    7. }
    8. }
    9. class B extends A{
    10. }
    11. class C extends A{
    12. }

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

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

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

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

    父类中的this是谁?

    请先看下面代码:

    1. public class Test {
    2. public static void main(String[] args) {
    3. new B();
    4. }
    5. }
    6. class A<T>{
    7. public A(){
    8. // this是谁?A还是B?
    9. Class clazz = this.getClass();
    10. System.out.println(clazz.getName());
    11. }
    12. }
    13. class B extends A<String>{
    14. }

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

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

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

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

    我们再来分析:

    1. class A{
    2. public A(){
    3. //clazz是B.class
    4. Class clazz = this.getClass();
    5. }
    6. }
    7. class B extends A{
    8. }

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

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

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

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

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

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

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

    1. public class JpaTest {
    2. public static void main(String[] args) {
    3. new B();
    4. }
    5. }
    6. class A<T> {
    7. public A() {
    8. Class<? extends A> subClass = this.getClass();
    9. // 得到泛型父类
    10. Type genericSuperclass = subClass.getGenericSuperclass();
    11. // 本质是ParameterizedTypeImpl,可以向下强转
    12. ParameterizedType parameterizedTypeSuperclass = (ParameterizedType) genericSuperclass;
    13. // 强转后可用的方法变多了,比如getActualTypeArguments()可以获取Class A<String>的泛型的实际类型参数
    14. Type[] actualTypeArguments = parameterizedTypeSuperclass.getActualTypeArguments();
    15. // 由于A类只有一个泛型,这里可以直接通过actualTypeArguments[0]得到子类传递的实际类型参数
    16. Class actualTypeArgument = (Class) actualTypeArguments[0];
    17. System.out.println(actualTypeArgument);
    18. System.out.println(subClass.getName());
    19. }
    20. }
    21. class B extends A<String> {
    22. }
    23. class C extends A<Integer> {
    24. }

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

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

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

    第一版JPA

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

    1. <dependency>
    2. <groupId>commons-dbcp</groupId>
    3. <artifactId>commons-dbcp</artifactId>
    4. <version>1.4</version>
    5. <scope>test</scope>
    6. </dependency>

    User

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

    BaseDao

    1. public class BaseDao<T> {
    2. private static BasicDataSource datasource;
    3. // 静态代码块,设置连接数据库的参数
    4. static {
    5. datasource = new BasicDataSource();
    6. datasource.setDriverClassName("com.mysql.jdbc.Driver");
    7. datasource.setUrl("jdbc:mysql://localhost:3306/test");
    8. datasource.setUsername("root");
    9. datasource.setPassword("123456");
    10. }
    11. // 得到jdbcTemplate
    12. private JdbcTemplate jdbcTemplate = new JdbcTemplate(datasource);
    13. // DAO操作的对象
    14. private Class<T> beanClass;
    15. /**
    16. * 构造器
    17. * 初始化时完成对实际类型参数的获取,比如BaseDao插入User,那么beanClass就是user.class
    18. */
    19. public BaseDao() {
    20. beanClass = (Class<T>) ((ParameterizedType) this.getClass()
    21. .getGenericSuperclass())
    22. .getActualTypeArguments()[0];
    23. }
    24. public void add(T bean) {
    25. // 得到User对象的所有字段
    26. Field[] declaredFields = beanClass.getDeclaredFields();
    27. // 拼接sql语句,表名直接用POJO的类名,所以创建表时,请注意写成User,而不是t_user
    28. StringBuilder sql = new StringBuilder()
    29. .append("insert into ")
    30. .append(beanClass.getSimpleName())
    31. .append(" values(");
    32. for (int i = 0; i < declaredFields.length; i++) {
    33. sql.append("?");
    34. if (i < declaredFields.length - 1) {
    35. sql.append(",");
    36. }
    37. }
    38. sql.append(")");
    39. // 获得bean字段的值(要插入的记录)
    40. ArrayList<Object> paramList = new ArrayList<>();
    41. try {
    42. for (Field declaredField : declaredFields) {
    43. declaredField.setAccessible(true);
    44. Object o = declaredField.get(bean);
    45. paramList.add(o);
    46. }
    47. } catch (IllegalAccessException e) {
    48. e.printStackTrace();
    49. }
    50. int size = paramList.size();
    51. Object[] params = paramList.toArray(new Object[size]);
    52. // 传入sql语句模板和模板所需的参数,插入User
    53. int num = jdbcTemplate.update(sql.toString(), params);
    54. System.out.println(num);
    55. }
    56. }

    UserDao

    1. public class UserDao extends BaseDao {
    2. @Override
    3. public void add(User bean) {
    4. super.add(bean);
    5. }
    6. }

    测试类

    1. public class UserDaoTest {
    2. public static void main(String[] args) {
    3. UserDao userDao = new UserDao();
    4. User user = new User("bravo1988", 20);
    5. userDao.add(user);
    6. }
    7. }

    测试结果

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

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

    第二版JPA

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

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

    @Table注解

    1. @Retention(RetentionPolicy.RUNTIME)
    2. @Target(ElementType.TYPE)
    3. public @interface Table {
    4. String value();
    5. }

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

    这下真的是山寨JPA了~

    另类注解

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

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

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

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

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

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

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

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

     

  • 相关阅读:
    PHP安装配置
    SpringFramework:SpringBean的注入方式
    【C++】异常
    Python 学习之路
    学习java的第二十七天。。。(输入输出流)
    java毕业设计电商项目mybatis+源码+调试部署+系统+数据库+lw
    在 centos7 上安装Docker
    PowerPoint 教程,如何在 PowerPoint 中添加水印?
    网络安全-抓包和IP包头分析
    【JavaEE基础与高级 第55章】Java中的对象流详细介绍与使用
  • 原文地址:https://blog.csdn.net/smart_an/article/details/134522834