类加载与反射 3

java零基础入门-高级特性篇(十六) 类加载与反射 3

如果你认为反射只有前面介绍的那些作用,那么就太小看这个功能了。本章再来介绍反射中更加强大的用法,反射功能可以在设计层面更好的处理一些难题,甚至改变编程的方式。

面向切面编程AOP

AOP - Aspect Oriented Programming的缩写,java不是面向对象编程么,怎么又整了个面向切面编程出来了?其实面向切面这种思想是对面向对象思想(OOP-Object Oriented Programming)的一种补充和扩展,让程序在设计上更加灵活,使代码编写的难度降低,功能之间的耦合度降低。普通的业务逻辑都是串行的,一个逻辑接着一个逻辑,从上往下顺序执行,有时需要新增功能时,不得不对每一个功能都进行修改,而AOP提供了另一种解决方案,它可以通过预编译方式和运行期动态代理的方式, 实现在不修改源代码的情况下给程序统一加上新功能。

功能改造

假设现在有一个系统有登录,购物,付款,退出这几个功能。然后有一天,突然有用户说我为什么没登录就可以付钱?我东西寄哪去了?我明明付了钱为啥说我没付钱?这时候怎么办?如果没有一套完善的日志系统,我们无从查起,不是知道具体哪个环节出了问题,所以需要对用户的每一步的操作都记录日志,这样出了问题就可以精确的追踪到出问题的地方。这要怎么改造系统?那还不简单~加班呗,每个具体功能前后全加上日志,一旦用户有操作就给记录日志。说的很轻松,但是要知道上图只是一个简单的例子,真正的系统可是十分庞大的,几十个模块都算少的,比如物流,仓库,财务,会员等等,一旦用户付了钱买了东西,一大片业务都需要同时改变,更新物流信息,库存减少,财务入账,银行扣款,会员积分增加,等等等,你确定你要全公司一起日以继夜的加班新增功能么?

面向切面AOP告诉你,不需要!只需要一个程序员敲几行代码即可。是不是觉得有点不可思议?这么多模块都需要加日志,几行代码怎么可能做得完?其实如果使用AOP的思想来考虑这个问题,这个问题会简单很多,下面看看AOP如何解决。

切面

无需改动原有代码,新增切面即可,将记录日志的功能切入到所有需要记录日志的功能模块中,这样只需要在一个地方维护日志模块功能即可,不需要在每一个功能中都加上记录日志的功能。有了切面,如果后面还有这种需要在部分功能上都加上同样功能的需求的时候,只需要增加切面即可。比如在购物的时候不需要验证登录,付款的时候验证登录就可以了,这时候就只有部分功能加入权限验证切面就可以了。

切面

在讲具体代码之前,除了AOP,还有一个知识需要了解一下,那就是代理,AOP就是代理模式的一种具体实现。那么代理模式又是个什么?代理不懂,代理律师总听过吧?打官司,自己不懂法,需要找一个专业的律师帮忙打官司,自己付钱等结果就行了。这种不直接由自己出面解决问题,而使用中介的方式让中介来完成本该由自己完成工作的模式,就是代理模式。代理模式提供了一个代理对象,并由代理对象控制对原对象的引用,最后由代理对象完成任务。

代理

原告和被告都不懂法,官司没法打,所以都请个代理律师,让律师来完成申诉和辩护的任务。原告就相当于系统原有代码,当系统需要统一增加功能的时候,请个代理律师来完成新的功能就行了,原告和被告是不需要做任何改变的,毕竟为了打个官司,总不能让原告和被告先去学一遍法律知识吧。

原功能

1.首先是登录功能的接口,用来为登录模块创建模板。2.User实体类,用来传递用户信息。3.具体的登录功能实现,如果需要新增日志功能的话,需要在每个方法上面都加上日志记录的代码。4.没有日志记录功能的代码,所以打印出来的只有用户具体的状态。具体的功能都在实现类里面,要增加功能也是在实现类中添加,所以这个实现类就是需要被代理的类。下面就来看看如何设计一个代理类,来为这个实现类服务。

切面

这个类LoginInvocationHandler需要实现InvocationHandler类,然后重写invoke方法。在构造器中需要传入被代理的对象,可以通过反射获取被代理对象的信息,invoke方法替代了原来需要被调用的被代理对象的方法,这样就可以在被代理对象的方法中添加新的功能而无需改动原有的代码。

使用代理调用方法

在调用方法的时候,不要再直接调用原对象的方法,而需要根据切面设置代理,通过反射根据接口创建新的代理对象,此时生成的对象在运行时具备了代理对象新增的功能,最后使用代理对象调用方法即可。在结果中也看到了,新的功能已经生效,在用户的登录和退出之前和之后,都已经加上了日志记录。这个例子很好的体现了反射功能的强大,用反射实现了代理最终实现了面向切面的功能添加。

注解

在上面切面制作的类中,实现InvocationHandler接口的时候,有一个奇怪的东西。在实现接口的方法的时候,会有一个@Override在方法上面。这是个什么玩意?这种代码叫做注解Annotation,它可以在不改变原有代码逻辑的情况下,对代码进行一些补充或功能的添加。注解的功能十分强大,java的jdk有部分注解用于对代码进行修饰,而他的功能强大之处主要体现在它的自定义功能。由于可以自定注解,在各大框架中有大量的封装好的注解供我们使用,极大的方便了我们的开发和提高开发效率。

常用java自带注解

@Override:限定重写父类方法。抽象类中有抽象方法,也可以有实现好的方法,如果继承了抽象类,那么已实现的方法可以重写也可以不重写,而抽象方法必须被重写,接口也是一样,接口中都是抽象方法,在实现接口的时候必须重写接口的抽象方法。而这个注解就是标识出必须被子类重写的父类方法或实现接口的方法,它可以帮助我们避免忘记,或写错需要重写方法带来的问题。其实IDE工具一般都有自动完成重写方法的功能,在自动完成的过程中也会自动加上这个注解,表示已重写该方法。

@Override注解

@Deprecated:标识已过时的类或方法。在使用java的早期版本类或方法的时候会发现有一些类或方法是带有一个删除线的,因为他们已经被打上了@Deprecated注解。这些类或方法表示java已经不再推荐使用,在以后的版本有可能会将这种过时的类删除,所以在写代码的过程中要尽量避免使用这种类或方法。

@Deprecated注解

@SuppressWarnings:取消编译警告。这个注解可以在类上,方法上也可以在变量上,当出现编译警告的时候,通过这个注解就可以告诉编译器别给我警告了,我知道了。此注解会作用到标记位置的所有作用范围,如果在类上,整个类都被取消警告,在方法上,整个方法取消警告。这个注解最常见的时候就是当我们声明一个集合并且不指定泛型,这个时候就会出现警告,如果确实不想指定泛型最好加上此注解。在程序出现多条警告的时候,还可以同时取消多个编译警告,在注解上传入一个字符串数组即可。

@SuppressWarnings注解

自定义注解

java自身的自定义注解功能有限,结合一些框架以后会有更加强大的功能。但是有些简单的功能,依靠java自身也是可以完成的,下面来看看如何一个自定义的注解。

自定义注解肯定会用到元注解,元注解就是用来修饰注解的注解。也就是说定义一个注解的时候需要依赖别的注解,这些被依赖的注解就是元注解。常用的元注解有下面四种。

@Documented – 如果一个类型添加了Documented注解,那么它的注解会成为元素API的一部分。可以被工具文档化,在生成文档的时候会将信息自动生成到API中。

@Target – 指定该注解可以使用在什么地方,比如是在方法上的注解还是类上的注解。如果不指定此元注解,标识该注解可以用在任何元素上。下面是可以指定的位置。

TYPE:类,接口或者枚举 FIELD:域,包含枚举常量 METHOD:方法

PARAMETER:参数 CONSTRUCTOR:构造方法 PACKAGE:包

LOCAL_VARIABLE:局部变量 ANNOTATION_TYPE:注解类型

@Inherited – 子类继承父类中的注解。

@Retention – 表示注解的声明周期。有三种方式,SOURCE:编译完就失效。CLASS:编译级别保留,在jvm运行时失效,默认值。RUNTIME:运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用。

下面来定义一个注解,来帮助我们检查字段的赋值是否满足要求。通常在web项目中,会有大量的数据从前端传递给后端,数据校验就是一个十分重要的环节。比如在用户进行注册的时候,就必须进行数据校验,比如用户名的长度,密码复杂度等等,都需要满足要求才能够发送到后端,而后端也必须再一次对这些数据进行校验。

但是,通常对这些数据进行校验会写在业务中,造成业务代码中充斥了大量的if,else的判断,这样不仅在业务中写了大量与业务无关的代码,还会有大量重复的代码。比如注册的时候校验密码,需要在注册功能中校验一遍,再修改密码的时候,又要在修改密码的功能中再次校验,如果能够将校验规则提取出来,不仅使得代码更加专注于业务,还能大幅提高编码效率。如果将校验规则封装进方法,也会有参数传递,方法调用等逻辑,如果在对对象进行赋值的时候,就进行校验,这样会更加的优雅,没有任何的校验逻辑在代码中。如何做到?使用注解。

数据校验

在对象赋值的时候,将校验逻辑封装进注解,在属性上注解后,在对属性进行赋值的时候就会进行数据校验,对实体类的封装性没有任何的破坏,也不会破坏单一职责原则,因为实体类本身里面是没有任何校验逻辑的。

1.首先定义自定义注解。@Target指定此注解用在字段上,@Retention指定为RUNTIME,运行时可以使用反射来调用。注解中指定可以在注解上使用的参数,参数类型只能是基本类型,boolean是基本类型可以使用。isNotNull()是属性名,default false表示如果注解上不使用此属性,则默认为false。这样一个自定义注解就定义好了。

自定义注解

2.校验器,封装注解中具体的校验逻辑。

注解逻辑

3.使用自定义注解。在需要验证的字段上加上注解,并且对注解属性进行赋值。如果没有声明注解属性,则使用定义注解时的属性默认值。

使用自定义注解

4.校验赋值。在对象赋值的后,使用校验器对对象进行校验。

校验

其实在参数校验方面,已经有很成熟的标准框架可以直接使用,比如JSR303 ,就是一套JavaBean参数校验的标准。除了JSR303内置了大量现成的注解之外,还可以非常灵活的对校验注解进行扩展,定义自己需要的验证注解。这一套注解的使用方法跟上例中我们自定义的注解使用方法类型,但是用法更方便,无需使用验证器传入对象进行校验,在注解中就可以定义需要抛出的异常信息等等。JSR303已有注解如下

@Null 被注释的元素必须为 null

@NotNull 被注释的元素必须不为 null

@AssertTrue 被注释的元素必须为 true

@AssertFalse 被注释的元素必须为 false

@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@Size(max, min) 被注释的元素的大小必须在指定的范围内

@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内

@Past 被注释的元素必须是一个过去的日期

@Future 被注释的元素必须是一个将来的日期

@Pattern(value) 被注释的元素必须符合指定的正则表达式

各位只需要了解即可,在项目中需要进行参数校验的时候再来查阅即可。

原文发布于微信公众号 - 码思客(javamsk)

原文发表时间:2019-04-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券