泛型和注解是框架技术必备的技能
5 泛型
5.1泛型理解
5.1.1 泛型概念
泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。本文综合多篇文章后,总结了Java 泛型的相关知识,希望可以提升你对Java中泛型的认知效率。
5.1.2 泛型的特点
泛型只在编译阶段有效。看下面的代码:
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if(classStringArrayList.equals(classIntegerArrayList)){
Log.info("泛型测试,类型相同");
}
通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
5.1.3 如何理解Java中的泛型是伪泛型?
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。
擦除原则
- 消除类型参数声明,即删除<>及其包围的部分。
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性
如何擦除类型?
(1) 无限制的转化为Object
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。
(2). 升级为上限
擦除类定义中的类型参数 - 有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。
(3).擦除方法定义中的类型参数
除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。
如何证明被擦除了呢?
看测试代码
@Test
public void t1() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
// list.add("字符串");//编译器语法检查会报错,因为引用了泛型,无法编译通过
//通过反射注入值,编译能通过
list.getClass().getMethod("add", Object.class).invoke(list, "字符串");
for (Object o:list){
System.out.println(o);
}
}
在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型
5.1.4 泛型的价值
既然编译后会擦除泛型,那为什么又要使用泛型呢,不是没事找事吗,有以下原因:
(1)程序的健壮和安全性
以集合为例子,在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。
有了泛型后,出现不符合预期的代码就会编译不通过。相当于告诉编译器每个集合接收的对象类型是什么,编译器在编译期就会做类型检查,告知是否插入了错误类型的对象,使得程序更加安全,增强了程序的健壮性。
(2)避免了不必要的装箱、拆箱操作,提高程序的性能
以集合为例子,在没有泛型之前,从集合中读取到的每一个对象都必须进行类型强制转换,大量的开箱拆箱工作将会降低代码性能。
(3)避免重复代码,提升程序优雅性
当我们为处理不同的对象,必须增加不同方法或者类时,用泛型可以避免这些,必须当你需要创建一个通用的数据结构,例如列表、栈、队列、字典等,这些结构可以处理各种类型的数据时,可以使用泛型类,用泛型代替Object
5.2 泛型语法
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
5.2.1 泛型类
泛型类:把泛型定义在类上
语法:把类体里面要用的泛型类型,在类后声明,可以1个或者多个,泛型的名字无限制
public class 类名 <泛型类型1,…> { }
注意事项:
- 泛型类型必须是引用类型(非基本数据类型)
- 定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔:
- 参数名称可以任意
当然,这个后面的参数类型也是有规范的,通常类型参数我们都使用大写的单个字母表示:
实例1:单个泛型
public class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
使用泛型类
@Test
public void t2() {
Pair<String> pair = new Pair<>();
pair.setValue("www");
String r = pair.getValue();
Pair<Integer> pair1=new Pair<>();
Integer i=pair1.getValue();
}
实例2:多个泛型
public class MoreGenerics <k,v>{
private k id;
private v name;
public k getId() {
return id;
}
public void setId(k id) {
this.id = id;
}
public v getName() {
return name;
}
public void setName(v name) {
this.name = name;
}
}
使用
@Test
public void t4(){
MoreGenerics<String,String> mg=new MoreGenerics<>();
mg.setId("1");
mg.setName("jzk");
MoreGenerics<Integer,String> mg2=new MoreGenerics<>();
Integer id=mg2.getId();
}
5.2.2 泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中
public interface 接口名 <泛型类型1,…> { }
例如
public interface GenericeServeice<T> {
public T getKey();
}
实现接口的类,有三种方式
- 指定具体类型:就是在实现接口时,明确指定泛型参数的具体类型;
- 保留泛型参数:在实现接口时,不明确指定泛型参数的具体类型,而是保留泛型参数。
- 保留泛型参数:并增加新的泛型类型
注意语法
class A implements GenericeServeice<String>{}//指定具体类型
class B<T> implements GenericeServeice<T>{}//保留泛型
class C<K,T> implements GenericeServeice<T>{}//保留并新增泛型
接口
ublic interface GenericeServeice<T> {
public T getKey();
}
实现类
public class GenericeServeiceImp {
//1.就是在实现接口时,明确指定泛型参数的具体类型;注意 A implements B <具体类型>{}
class A implements GenericeServeice<String>{
@Override
public String getKey() { // T getKey() T 用具体类型 String代替
return "大太阳";
}
}
//2.在实现接口时,不明确指定泛型参数的具体类型,而是保留泛型参数
class B<T> implements GenericeServeice<T>{
private T id;
@Override
public T getKey() {
return id;
}
public void setId(T id){
this.id=id;
}
}
//3.继承了接口的泛型参数,并新增泛型
class C<K,T> implements GenericeServeice<T>{
private T id;
private K name;
@Override
public T getKey() {
return id;
}
public void setName(K name){
this.name=name;
}
public K getName(){
return name;
}
public void setKey(T id){
this.id=id;
}
}
@Test
public void t1(){
A a=new A();
String key=a.getKey();
B<Integer> b=new B<>();
b.setId(3);
Integer id=b.getKey();
C<String,Integer> c=new C<>();
c.setName("奎哥");
c.setKey(3);
C<Number,Integer> c1=new C<>();
c1.setName(34);
c1.setKey(3);
System.out.println("A.getKey()="+key);
System.out.println("B.getKey()="+id);
}
}
3.2.3 泛型方法
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
注意事项:
- 泛型方法必须标注在方法修饰符和返回值之间<泛型变量…>,和对应类是否时泛型无关,彼此独立
泛型方法声明:
public class GeneralM {
/***
方法无返回值,入参是一个泛型参数
*/
public <T> void m(T t){
System.out.println(t.getClass().getName());
}
/***
方法无入参,返回值是一个泛型变量
*/
public <T> T m1(){
T t=null;
return t;
}
/***
入参和返回值都是一个泛型变量
*/
public <T> T m2(T t){
return t;
}
/***
声明多个泛型
*/
public <K,V> void m3(K k,V v){
System.out.println(k.getClass().getName());
System.out.println(v.getClass().getName());
}
/***
Class<T>这个表示泛型T的具体类型是Class
*/
public <T> T getObj(Class<T> c) throws InstantiationException, IllegalAccessException {
T t= c.newInstance();
return t;
}
/***
写一个函数把数组转化为List
*/
public <T> List<T> toList(T[] arrs){
List<T> list=new ArrayList<>();
for (T arr : arrs) {
list.add(arr);
}
return list;
}
}
泛型方法使用
@Test
public void t5() throws InstantiationException, IllegalAccessException {
GeneralM gm=new GeneralM();
//对应 public <T> void m(T t){}
System.out.println("对应 public <T> void m(T t){}=============");
gm.m("d");
gm.m(new Pair());
gm.m(1);
//对应 public <T> T m1(){}
System.out.println("对应 public <T> T m1(){}=============");
String ds= gm.m1();
Integer i=gm.m1();
//对应 public <T> T m2(T t){}
System.out.println("对应 public <T> T m2(T t){}=============");
Integer i1=gm.m2(3);
String s=gm.m2("d");
//对应 public <K,V> void m3(K k,V v){}
System.out.println("对应 public <K,V> void m3(K k,V v){}=============");
gm.m3(12, "大哥");
//对应 public <T> T getObj(Class<T> c)
System.out.println("对应 public <T> T getObj(Class<T> c)=============");
VO vo= gm.getObj(VO.class);
vo.setId(1);
vo.setName("dd");
System.out.println(vo);
//对应 public <T> List<T> toList(T[] arrs)
System.out.println("对应 public <T> List<T> toList(T[] arrs)");
Integer[] arr1={1,2,3};
List<Integer> list=gm.toList(arr1);
String[] arr2={"1","2","3"};
List<String> list2=gm.toList(arr2);
//List<Integer> list3=gm.toList(arr2);//编译报错
}
注意:泛型方法使用,并没有泛型类或者接口用<>确定类型,和正常的方法一样,因为方法不会实例化
案例详解
案例说明:
-
定义泛型方法时,必须在返回值前边加一个,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。
-
Class的作用就是指明泛型的具体类型,而Class类型的变量c,可以用来创建泛型类的对象。
为什么要用变量c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象。 -
泛型方法要求的参数是Class类型,而Class.forName()方法的返回值也是Class,因此可以用Class.forName()作为参数。其中,forName()方法中的参数是何种类型,返回的Class就是何种类型。在本例中,forName()方法中传入的是User类的完整路径,因此返回的是Class类型的对象,因此调用泛型方法时,变量c的类型就是Class,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。
当然,泛型方法不是仅仅可以有一个参数Class,可以根据需要添加其他参数。
我们再看解析json的中间件Gson的源码,加深理解:
Gson
泛型类和泛型方法往往都在一起,容易让初学者混淆,查看实例
public class GenericTest {
//这个类是个泛型类,在上面已经介绍过
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
//我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getKey(){
return key;
}
/**
* 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
public E setKey(E key){
this.key = keu
}
*/
}
/**
* 这才是一个真正的泛型方法。
* 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
* 这个T可以出现在这个泛型方法的任意位置.
* 泛型的数量也可以为任意多个
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void showKeyValue2(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
public <T> T showKeyName(Generic<E> container){
...
}
*/
/**
* 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
* 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
* 所以这也不是一个正确的泛型方法声明。
public void showkey(T genericObj){
}
*/
public static void main(String[] args) {
}
}
泛型方法有什么优势
泛型类必须要在实例化对象时指明具体的泛型的替代类型,不同的化必须实例化一个新对象如:
List<String> list=new ArrayList<>();
List<Integer> list=new ArrayList<>();
但泛型方法就更为简单,不需要再实例化对象,也不需要专门用<>声明数据类型,更为灵活,如上面例子提到的:
//对应 public <T> List<T> toList(T[] arrs)
System.out.println("对应 public <T> List<T> toList(T[] arrs)");
Integer[] arr1={1,2,3};
List<Integer> list=gm.toList(arr1);
String[] arr2={"1","2","3"};
List<String> list2=gm.toList(arr2);
//List<Integer> list3=gm.toList(arr2);//编译报错
3.2.4泛型的上下边界
- 上限
在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
语法 :泛型变量 extends 具体类型
测试代码:这里泛型参数T只能是Number及其子类
public class Info <T extends Number>{
private T id;
public Info(T id){
this.id=id;
}
public static void main(String[] args) {
Info<Integer> info1=new Info<>(12); //ok
Info<Float> info2=new Info<>(12.45f); //ok
// Info<String> info3=new Info<>("123"); //编译出错
}
}
- 下限
class Info<T>{
private T var ; // 定义泛型变量
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
public String toString(){ // 直接打印
return this.var.toString() ;
}
}
public class GenericsDemo21{
public static void main(String args[]){
Info<String> i1 = new Info<String>() ; // 声明String的泛型对象
Info<Object> i2 = new Info<Object>() ; // 声明Object的泛型对象
i1.setVar("hello") ;
i2.setVar(new Object()) ;
fun(i1) ;
fun(i2) ;
}
public static void fun(Info<? super String> temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类
System.out.print(temp + ", ") ;
}
}
小结
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
通配符 ?
通配符?表示任意的,一般和泛型的上下界限制一起搭配使用
3.2.5创建泛型数组
看代码
List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建
List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告
List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建
List<?>[] list15 = new ArrayList<?>[10]; //OK
List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告
上面都不是创建泛型数组的最佳方式
我们在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用 java.lang.reflect.Array.newInstance(Class componentType, int length) 方法来创建一个具有指定类型和维度的数组,如下
public class ArrayWithTypeToken<T> {
private T[] array;
public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] create() {
return array;
}
}
//...
ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();
所以使用反射来初始化泛型数组算是优雅实现,因为泛型类型 T在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。
5.3泛型应用场景
泛型最到的特征是在定义类时并不指定类里的具体参数,这样就可以把一些共性抽象出来,在泛型之前,我们只能把具体参数对象抽象成Object类,在使用时再强制转化成具体对象,但其健壮性和安全性存在一定问题,所以泛型在做通用基础组件里得到广泛的应用,在实际业务场景中,我们在结合类的反射,这样就能形成通用的封装类,我们查看很多三方组件,都有大量的泛型应用。
5.3.1数据库操作组件封装
BaseDao定义了基本的数据库增删查改, 之后可以继承该泛型类,实现各自的增删查改,或者使用超类的增删查改,同时每个继承类还能增加自己的操作:
思路:
利用类得反射原理,把传入对象的字段属性和值都读出来,动态生成SQL语句,为简单我们假设类名和属性名称和数据库表与字段一一对应
示意代码
public class BaseDAO <T>{
//数据库连接信息
private String dbURL="";
public void save(T t){
Class cls=t.getClass();
//利用反射原理获得T的属性和值,动态生成SQL语句
}
public T getByKey(Integer id,Class<T> c) {
try{
T o=c.newInstance();
//把sql查询值利用反射机制注入obj
System.out.println("执行getByKey()");
return o;
}
catch (Exception e){
throw new RuntimeException("映射出错");
}
}
}
继承基类
public class StudentDAO extends BaseDAO<Student>{
}
public class OrderDAO extends BaseDAO<Order>{
}
使用
@Test
public void t9() {
StudentDAO sdao=new StudentDAO();
sdao.save(new Student());
Student st=sdao.getByKey(1, Student.class);
OrderDAO odao=new OrderDAO ();
odao.save(new Order());
Student st=sdao.getByKey(1, Order.class);
}
5.3.2 数据库分页组件封装
在使用java对数据库操作时候,很常见的一个功能分页操作,java接收的常常是一个count和相应的记录列表,然后,一般的定bean的方法如下:
vo类 如:order
@Data
public class Order {
private Integer id;
private String orderCoder;
private Integer goodId;
private Integer buyNum;
}
分页类:
一般包含分页的公共数据和当前记录数据
@Data
public class PageSpitOrder {
//分页通用数据
private Integer pageNum;//页数
private Integer curPage;//当前页
private Integer pageSize;//每页条数
private List<Order> list;//当前当前页数据
}
如果我们再有商品分页,最简单的方法再定义一个PageSpitGood的封装类,这样肯定就不通用了,泛型以前我们会一定义一个List类来存储当前页数据,在使用类里在强转到具体对象。
用泛型就优雅得多:
代码示意图
public class PageSpitComm<T> {
//分页通用数据
private Integer pageNum;//页数
private Integer curPage;//当前页
private Integer pageSize;//每页条数
private List<T> list;//当前当前页数据,使用T类型
public List<T> getList(){return list;}
}
使用代码
@Test
public void t8(){
PageSpitComm<Order> pg1=new PageSpitComm<>();
List<Order> list1=pg1.getList();
PageSpitComm<VO> pg2=new PageSpitComm<>();
List<VO> list2=pg2.getList();
}
5.3.3 第三方中间件
Gson.fromJson,原理解析json字符串,反映射注入类中
mybatis中间件
BaseMapper源码
6.注解
注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它是框架学习和设计者必须掌握的基础
6.1注解的理解
6.1.1注解定义
Java 注解(Annotation)用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。
Annotion(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。
我们常常使用的注解,@Data、@Controller等等,这些都是注解,创建一个注解,也很简单,创建一个类,然后将class改为 @interface就是一个注解
Java代码中的包、类型、构造方法、方法、成员变量、参数、本地变量的声明都可以用注解来修饰。注解本质上可以看作是一种特殊的标记,程序在编译或者运行时可以检测到这些标记而进行一些特殊的处理
6.1.2 注解的作用
-
生成文档
这是最常见的,也是 Java 最早提供的注解。如@param、@return等等
跟踪代码依赖性,实现替代配置文件功能。作用就是减少配置,如 Spring中Bean的装载注入,而且现在的框架基本上都是使用注解来减少配置文件的数量,同时这样也使得编程更加简洁,代码更加清晰。 -
在编译时进行格式检查
如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出;
标识作用。当Java编译时或运行时,检测到这里的注解,做什么的处理,自定义注解一般如此。 -
携带信息
注解的成员提供了程序元素的关联信息,Annotation 的成员在 Annotation类型中以无参数的方法的形式被声明。其方法名和返回值定义了该成员的名字和类型。在此有一个特定的默认 语法:允许声明任何Annotation成员的默认值。一个Annotation可以将name=value对作为没有定义默认值的Annotation 成员的值,当然也可以使用name=value对来覆盖其它成员默认值。这一点有些近似类的继承特性,父类的构造函数可以作为子类的默认构造函数,但是也 可以被子类覆盖。
6.1.3注解的应用
注解在基础框架有最广泛的应用:
- 生成文档这是最常见的,也是java 最早提供的注解;
- 在编译时进行格式检查,如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出
- 跟踪代码依赖性,实现替代配置文件功能,比较常见的是spring 2.5 开始的基于注解配置,作用就是减少配置;
- 在反射的 Class, Method, Field 等函数中,有许多于 Annotation 相关的接口,可以在反射中解析并使用 Annotation。
6.1.4注解分类
- Java自带的标准注解
-包括@Override、@Deprecated和@SuppressWarnings,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。 - 元注解
元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented,@Retention用于标明注解被保留的阶段,@Target用于标明注解使用的范围,@Inherited用于标明注解可继承,@Documented用于标明是否生成javadoc文档。 - 自定义注解
可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。
6.2内置注解
注解语法离开案例会很抽象,我们利用java自带的内置注解和元注解来讲解注解的语法
编写一个案例:
public class A {
public void test(){
}
}
public class B extends A{
@Override
public void test(){
System.out.println("@Override覆盖父类test().");
}
@Deprecated
public void d(){
System.out.println("@Deprecated过期声明");
}
@SuppressWarnings("all")
public void dd(){
String s="";
int t=s.length();
}
}
java内置的注解有以下3个:@Override、@Deprecated和@SuppressWarnings
内置注解 | 用途 |
---|---|
@Override | 表示当前的方法定义将覆盖父类中的方法,如果注解的方法非父类中的方法,编译出错 |
@Deprecated | 表示此方法已经废弃,有更好的解决方案,不建议使用它,调用时有中划线展示 |
@SuppressWarnings | 表示忽略编译器的编译警告,编译器一些不符合规范的代码会有警告,此方法可以忽略警告,如果是all,则忽略所有警告,让强迫症舒服一点 |
我们说注解本身是一个接口,我们打开内置注解的接口类
@Override
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Deprecated
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
String since() default "";
boolean forRemoval() default false;
}
初学者可能看不懂,因为你还需要了解元注解的知识
6.3 元注解
如上文一样,定义注解的注解,如@Documented,我们叫元注解,元注解只能做注解的注解(有点绕口)上述内置注解的定义中使用了一些元注解(注解类型进行注解的注解类),在JDK 1.5中提供了4个标准的元注解:
@Target,@Retention,@Documented,@Inherited,
在JDK 1.8中提供了两个元注解 @Repeatable和@Native
最重要两个注解:@Target,@Retention
注解 | 简要说明 |
---|---|
@Target | 注解用到哪些地方,属性,方法,类… |
@Retention | 注解生效的时候,源代码、编译、运行三个阶段 |
@Documented | 能生成文档 |
6.3.1 元注解 - @Target
Target注解的作用是:描述注解的使用范围(即:被修饰的注解可以用在什么地方)
Target注解用来说明那些被它所注解的注解类可修饰的对象范围:注解可以用于修饰 packages、types(类、接口、枚举、注解类)、类成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数),在定义注解类时使用了@Target 能够更加清晰的知道它能够被用来修饰哪些对象,它的取值范围定义在ElementType 枚举中。
- @Target(ElementType.TYPE) —— 接口、类、枚举、注解
- @Target(ElementType.FIELD) —— 字段、枚举的常量
- @Target(ElementType.METHOD) —— 方法
- @Target(ElementType.PARAMETER) —— 方法参数
- @Target(ElementType.CONSTRUCTOR) —— 构造函数
- @Target(ElementType.LOCAL_VARIABLE) —— 局部变量
- @Target(ElementType.ANNOTATION_TYPE) —— 注解
- @Target(ElementType.PACKAGE) —— 包
public enum ElementType {
TYPE, // 类、接口、枚举类
FIELD, // 成员变量(包括:枚举常量)
METHOD, // 成员方法
PARAMETER, // 方法参数
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类
PACKAGE, // 可用于修饰:包
TYPE_PARAMETER, // 类型参数,JDK 1.8 新增
TYPE_USE // 使用类型的任何地方,JDK 1.8 新增
}
语法
@Target(value={CONSTRUCTOR, FIELD,…}) //多个用逗号隔离
6.3.2 元注解-@Retention
定义了该注解的生命周期,或者叫作用范围(即:被描述的注解在它所修饰的类中可以被保留到何时)
Reteniton注解用来限定那些被它所注解的注解类在注解到其他类上以后,可被保留到何时,一共有三种策略,定义在RetentionPolicy枚举中。
public enum RetentionPolicy {
SOURCE, // 源文件保留
CLASS, // 编译期保留,默认值
RUNTIME // 运行期保留,可通过反射去获取注解信息
}
RetentionPolicy.SOURCE | 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;也就是编译时有效 |
RetentionPolicy.CLASS | 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;加载时被抛弃 |
RetentionPolicy.RUNTIME | 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;一直有效! |
语法:
@Retention(RetentionPolicy.SOURCE/CLASS /RUNTIME)
常见案例说明:
常见注解 | 说明 |
---|---|
@Over -> @Retention(RetentionPolicy.SOURCE) | 说明@Over 注解只在编译阶段有效 |
@Data -> @Retention(RetentionPolicy.SOURCE) | 说明lombok的Data注解只在编译阶段有效,编译时自动生成get/set函数 |
@Deprecated -> @Retention(RetentionPolicy.RUNTIME) | 说明废弃编码在运用时间有效,我们自定义注解一般都用RUNTIME |
6.3.3 元注解-@Documented
Documented注解的作用是:描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息。
6.4 自定义注解
6.4.1 注解与反射接口
定义注解后,如何获取注解中的内容呢?反射包java.lang.reflect下的AnnotatedElement接口提供这些方法。这里注意:只有注解被定义为RUNTIME后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
6.4.2 自定义注解步骤
1.创建注解接口: public @interface 注解名
注意事项:
1)必须是public
2) 注解名就是类名,符合类名命名规范,
3)可以理解为创建接口,然后在接口前加@
public @interface TestInheritedAnnotation {
}
2.加上元注解,最主要是@Target和@Retention两个元注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface TestInheritedAnnotation {
}
3.定义注解的成员变量(其实就是接口方法)
注意注解的成员变量, 不是字段,而是方法,虽然没有规定如何命名,一般不以方法名命名,而是以字段名命名。
案例:
@Inherited //继承注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface TestInheritedAnnotation {
String [] values();
int number() default 1;
String address() default "天府大道";
String value();
String sex(Sring param);//编译通不过
void tel();//编译通不过
}
成员变量规则:
- 成员以无参数无异常的方式声明 String constructorName() default “”,注意一定是无参无异常,否则编译通不过,没有返回值,编译也通不过。
- 可以使用default为成员指定一个默认值 public String name() default “defaultName”;
- 成员类型是受限的,合法的类型包括原始类型以及String、Class、Annotation、Enumeration (JAVA的基本数据类型有8种:byte(字节)、short(短整型)、int(整数型)、long(长整型)、float(单精度浮点数类型)、double(双精度浮点数类型)、char(字符类型)、boolean(布尔类型)
- 注解类可以没有成员,没有成员的注解称为标识注解,例如JDK注解中的@Override、@Deprecation
- 如果注解只有一个成员,并且把成员取名为value(),则在使用时可以忽略成员名和赋值号“=”
例如JDK注解的@SuppviseWarnings ;如果成员名 不为value,则使用时需指明成员名和赋值号"="
6.4.3 注解的使用规则
注解:
@Inherited //继承注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface TestInheritedAnnotation {
String [] values();
int number() default 1;
String address() default "天府大道";
String value();
}
使用注解:
@TestInheritedAnnotation(values = {"1","2"}, number = 10,address = "",value = "ddd")
public class C {
}
使用规则:
1.@注解名(值1,值2,…)
2.注解成员里赋值:成员名=值,多个用逗号隔离
3.如果只有一个注解成员可简写:
返回类型 value(),使用时可以直接用@注解名(值),可省略value=值,这种方式非常常用
案例:
注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SimpleAnnotation {
String value();
}
引用
@SimpleAnnotation("参数值")
public void t2(){
}
6.4.4 注解接口方法
注解实际是一个接口,如何获得注解的相关操作呢
我们Class,Field,Method都继承了AnnotatedElement接口类,这个可以获得注解相关操作
AnnotatedElement的相关方法说明
- isAnnotationPresent方法
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
return getAnnotation(annotationClass) != null;
}
作用:判断一个注解类是否注解到一个类、类方法、类字段上
Class(Field/Method) obj; //得到Class/Field/Method
AnnotationService ;//对应的注解接口
//方法入参是对应注解类的Class返回值:如果【类、类方法、类字段】有这个注解,则返回true
boolean b=obj.isAnnotationPresent(AnnotationService .class);
private static void printClassAnno(Class<?> clazz) throws ClassNotFoundException {
//判断是否有AuthorAnnotatin注解
if(clazz.isAnnotationPresent(ClassAnnitation.class)) {
}
}
- getAnnotation()方法
T getAnnotation(Class annotationClass);
作用:得到一个具体的注解接口
入参:注解接口对应的Class
出参:返回值是对应的注解接口,如没有,返回的是null
用途:获得注解接口,就可以调用接口方法,获得相应的注解值
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
例子:
ClassAnnitation annotation = clazz.getAnnotation(ClassAnnitation.class);
- getAnnotations方法
Annotation[] getAnnotations();
作用:用于获取这个元素上的所有注解,并以数组形式放回。如果该元素上没有注解,那么将返回一个长度为0的数组。
这个方法适合有多个注解的方式,一次性获得;获得的Annotation是所有注解的总接口,可以下溯到具体子接口,用Annotation.annotationType()方法得到具体注解对应的Class
@Group( {@Person(person = "奎哥"), @Person(person = "李四")})
public class Company {
@Test
public void t1(){
Class clazz = Company.class;
Annotation[] annotations = clazz.getAnnotations(); //得到类上的所有注解元素
for (Annotation annotation : annotations) {
//得到具体注解对用的Class
Class<? extends Annotation> cls=annotation.annotationType();
System.out.println( cls.getName());
if(cls.isAssignableFrom(Person.class)){//是不是Person注解
Person person=(Person)annotation;
}
if(cls.isAssignableFrom(Group.class)){//是不是Group注解
Group group=(Group)annotation;
System.out.println("="+group.value().length);
}
}
}
- getAnnotationsByType方法
T[] getAnnotationsByType(Class annotationClass)
这个方式是和T getAnnotation(Class annotationClass);区别在于专门为重复注解@Repeatable定制的,具体方法查看下面案例
- 其他
T getDeclaredAnnotation(Class annotationClass)
T[] getDeclaredAnnotationsByType(Class annotationClass)
Annotation[] getDeclaredAnnotations()
这三个方法和去掉Declared的上面方法对应,只是取注解的范围不一样Declared不考虑继承@Inherited的注解,如果大家学过Method,Feld的xxxDeclared方法,他们是一个意思
6.4.5 获得注解值(1)
利用反射获得注解和上面注解接口的方法,可以非常方便的获得注解值
案例:
(1)定义三个注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE}) //类的注解
public @interface ClassAnnitation {
String author() default "蒋增奎";
String createDate();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD}) //字段的注解
public @interface FieldAnnotation {
String name();
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) //方法的注解
public @interface MethodAnnotation {
String name();
String value();
}
(2)使用注解
@ClassAnnitation(createDate = "2024-01-04")
public class AnnotionTest2 {
@FieldAnnotation(name="name",value = "李逵")
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@MethodAnnotation(name="desc",value = "我是一个大帅哥")
public void desc(){
System.out.println("hello");
}
}
(3)解析注解
package com.jsoft.annotion;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* @class: com.jsoft.annotion.AnnotionTest
* @description:
* @author: jiangzengkui
* @company: 教育家
* @create: 2024-01-04 11:54
*/
public class AnnotionTest {
@Test
public void t1(){
B b=new B();
b.d();
b.dd();
}
@Test
public void t2() throws ClassNotFoundException{
AnnotionUse use=new AnnotionUse();
Class<?> clazz = use.getClass();
//打印类上的注解
printClassAnno(clazz);
//打印字段上的注解
printFieldAnno(clazz);
//打印方法上的注解
printMethodAnno(clazz);
}
/**
* 打印类的注解ClassAnnitation信息
*/
private static void printClassAnno(Class<?> clazz) throws ClassNotFoundException {
//判断是否有AuthorAnnotatin注解
if(clazz.isAnnotationPresent(ClassAnnitation.class)) {
//获取AuthorAnnotatin类型的注解
ClassAnnitation annotation = clazz.getAnnotation(ClassAnnitation.class);
System.out.println("ClassAnnitation.author="+ annotation.author());
System.out.println("ClassAnnitation.createDate="+ annotation.createDate());
}
else{
System.out.println(clazz.getName()+"类上没有ClassAnnitation对应的注解");
}
}
/**
* 打印成员变量的注解
*/
private static void printFieldAnno(Class<?> clazz) throws ClassNotFoundException {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {//循环属性
//如果上面有FieldAnnotation注解
if(field.isAnnotationPresent(FieldAnnotation.class)) {
//真实的业务场景一般获取到值,去改变字段的值
FieldAnnotation annotation = field.getAnnotation(FieldAnnotation.class);
System.out.println("字段"+field.getName()+"的注解.name="+annotation.name()+",.value="+annotation.value());
}
}
}
/**
* 打印成员变量的注解
*/
private static void printMethodAnno(Class<?> clazz) throws ClassNotFoundException {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if(method.isAnnotationPresent(MethodAnnotation.class)) {
MethodAnnotation annotation = method.getAnnotation(MethodAnnotation.class);
System.out.println("方法"+method.getName()+"的注解MethodAnnotation.name="+annotation.name()+";value="+annotation.value());
}
}
}
}
6.5 其他元注解
我们@Inherited、 @Repeatable两个元注解放到自定义注解后说明,主要让大家更容易理解
6.5.4 元注解 - @Inherited
Inherited注解的作用:如果一个类的注解元素有@Inherited装饰,则这个类的子类也将继承这个注解
1.注解类
@Inherited //继承注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface TestInheritedAnnotation {
String [] values();
int number();
}
2.测试类
@TestInheritedAnnotation(values = {"1","2"}, number = 10)
public class C {
}
public class D extends C{
//D类自动定继承了C的 @TestInheritedAnnotation(values = {"1","2"}, number = 10)
//也可以覆盖掉父类的注解,直接在D类上注解
//@TestInheritedAnnotation(values = {"a","b"}, number = 20)
//那父类的注解将会覆盖掉变成最新的
@Test
public void test(){
Class clazz = D.class;
Annotation[] annotations = clazz.getAnnotations(); //得到注解元素
for (Annotation annotation : annotations) {
System.out.println(annotation.toString());
}
}
}
代码说明:
D类自动定继承了C的 @TestInheritedAnnotation(values = {“1”,“2”}, number = 10)
也可以覆盖掉父类的注解,直接在D类上注解
@TestInheritedAnnotation(values = {“a”,“b”}, number = 20)
那父类的注解将会覆盖掉变成最新的
6.5.5 元注解 - @Repeatable
@Repeatable是jdk1.8引进的注解,解决重复注解问题
注解其实就是一个接口类,类里面是可以再嵌套类的,在注解里如何展示这种结构呢,比如利用注解传递一个对象数组?
在jdk1.8前,是这样解决的,看代码
@Person 注解 获得一个人
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME )
public @interface Person {
String person();
}
@Group 注解 获得一群人,其包含了@Person注解的数组
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME )
public @interface Group {
Person[] value();
}
测试代码
@Group( {@Person(person = "奎哥"), @Person(person = "李四")})
public class Company {
@Test
public void t1(){
Class clazz = Company.class;
Annotation[] annotations = clazz.getAnnotations(); //得到注解元素
for (Annotation annotation : annotations) {
System.out.println(annotation.toString());
}
}
}
jdk1.8引进了@Repeatable来简化
被包含的引入@Repeatable
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME )
@Repeatable(Group.class) //注意:Repeatable是定义在被包含的数组上
public @interface Person {
String person();
}
@Group 注解不变
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME )
public @interface Group {
Person[] value();
}
测试代码: 就可以直接定义多个注解了@Repeatable的注解
@Person(person = "kuige")
@Person(person = "mike")
public class Company2 {
@Test
public void t1(){
Class clazz = Company2.class;
Annotation[] annotations = clazz.getAnnotations(); //得到注解元素
for (Annotation annotation : annotations) {
System.out.println(annotation.toString());
}
}
}
6.5.6 元注解 - @Native
使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可
6.5.7 获得注解值(2)-重复注解@Repeatable的读取
重复注解直接用 getAnnotation是获取不到的
通过getAnnotationsByType来读取重复注解
public A[] getAnnotationsByType(Class annotationClass)
3个注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME )
@Repeatable(Group.class) //注意:Repeatable是定义在被包含的数组上
public @interface Person {
String person();
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME )
public @interface Group {
Person[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface SimpleAnnotation {
String value();
}
读取注解
@Person(person = "kuige")
@Person(person = "mike")
@SimpleAnnotation("I love you")
public class Company2 {
@Test
public void t1(){
Class<?> clazz = Company2.class;
repeatable1(clazz);
System.out.println("====================");
repeatable2(clazz);
}
/***
1.通过重复注解的关联注解Group来获取
*/
private static void repeatable1(Class<?> clazz){
Annotation[] annotations = clazz.getAnnotations(); //得到注解元素
//这里循环注解是获取不到Person的注解,只有Group的注解
for (Annotation annotation : annotations) {
if(annotation.annotationType().isAssignableFrom(Group.class)){
// System.out.println("true");
Group group=(Group) annotation;
Person[] ps= group.value();
for(Person person:ps){
System.out.println(person.person());
}
}
else if(annotation.annotationType().isAssignableFrom(SimpleAnnotation.class)){
SimpleAnnotation s=(SimpleAnnotation)annotation;
System.out.println(s.value());
}
}
}
/***
2.通过getAnnotationsByType来直接获取重复注解
*/
private void repeatable2(Class<?> clazz){
//只会读取Person注解的,支持重复注解
Person[] ps= clazz.getAnnotationsByType(Person.class);
for (Person p : ps) {
System.out.println(p.person());
}
}
}
6.6 中间件注解的应用
注解在spring mybatis里面有大量的应用,Lombok更是最典型的注解应用,一个最突出的作用就是通过注解来降低对配置文件的依赖
6.7 自定义注解应用
自定义注解是为了和具体业务脱耦,自定义注解的实现一般可借助springMVC的拦截机制和AOP切面编程来实现。
6.7.1 登录检查
自定义注解+SpringMVC拦截器实现权限控制功能
登录注解
/**
* @author sks
* 这个注解用于权限控制
*
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoLogin {
}
实现HandlerInterceptor 接口,完成自定义SpringMVC拦截器,在拦截器内部实现注解的解析功能。
public class MyInceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (!(handler instanceof HandlerMethod)) {
System.out.println("当前操作handler不为HandlerMethod=" + handler.getClass().getName() + ",req="
+ request.getQueryString());
return false;
}
//获得经过拦截器的方法
HandlerMethod handlerMethod=(HandlerMethod) handler;
String methodName = handlerMethod.getMethod().getName();
//通过反射的getAnnotation方法获得其方法上的指定的NoLogin类型的注解。
NoLogin myanno= handlerMethod.getMethod().getAnnotation(NoLogin.class);
if (myanno!=null) { //如果获得的注解不为空的话,说明此方法不需要权限就可执行。
System.out.println("当前操作不需要登录");
return true;
}
//否则就要看其session 的属性里是否有关于LOGIN属性的信息,若没有,则拦截此方法,不执行方法的操作
if (request.getSession().getAttribute("LOGIN")==null) {
System.out.println("当前操作" + methodName + "用户未登录,ip=" + request.getRemoteAddr());
return false;
}
System.out.println("当前操作" + methodName + "用户登录:" + request.getSession().getAttribute("LOGIN"));
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// TODO Auto-generated method stub
}
}
6.7.2 日志记录
springAOP:面向切面编程,是spring的两大核心模块之一,用于将系统中通用的模块或者功能抽取出来。其基本原理是AOP代理(分为动态代理和cglib代理)。利用aop里的通知,实现自定义注解的解析,可以完成相关的工作。
这里我们设计一个注解,即在需要进行日志记录的地方加上此注解即可实现日志自动记录的功能。
首先,仍是定义相关注解:
注解:
/**
* @author sks
* 这个注解用于日志管理
*
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Myanno {
/** 要执行的操作类型比如:add操作 **/
public String operationType() default "";
/** 要执行的具体操作比如:添加用户 **/
public String operationName() default "";
}
其次,定义相关的aop代理通知。这里,把注解的解析工作放在事后通知上,即下面的after方法。这里用简单的system.out来模拟日志记录功能。
/**
* @author sks
*
*/
public class MyAdvice4Anno {
public void before(JoinPoint joinPoint){
System.out.println("获取参数--》前置通知");
for (int i = 0; i < joinPoint.getArgs().length; i++) {
System.out.println("获取参数--》"+joinPoint.getArgs()[i].toString());
}
}
/*
*自定义的注解放在事后通知中
*/
public void after(JoinPoint joinPoint){
System.out.println("后置通知");
//通过连接点JoinPoint 获得代理的方法,进而获取方法上的注解信息
Method[] methods = joinPoint.getTarget().getClass().getMethods();
for (Method method : methods) {
Myanno annotation = method.getAnnotation(Myanno.class);
if (annotation!=null) {
String operationName = annotation.operationName();
String operationType = annotation.operationType();
//*========控制台输出=========*//
System.out.println("=====controller后置通知开始=====");
System.out.println("请求方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()")+"."+operationType);
System.out.println("方法描述:" + operationName);
break;
}
}
}
}
//异常通知
public void afterException(){
System.out.println("出事啦!出现异常了!!");
}
}
然后在springMvc的配置文件中,做好aop的配置工作。
<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
<bean id="myAdvice4Anno" class="com.test.zhujie.myanno.MyAdvice4Anno"></bean>
<aop:config>
<aop:pointcut expression="execution(* com.test.controller.AnnoController.*(..))" id="pointcut2"/>
<aop:aspect ref="myAdvice4Anno">
<aop:before method="before" pointcut-ref="pointcut2"/>
<aop:after method="after" pointcut-ref="pointcut2"/>
<aop:after-throwing method="afterException" pointcut-ref="pointcut2"/>
</aop:aspect>
</aop:config>
</beans>
控制层使用注解
@Myanno(operationType="add操作",operationName="添加用户")
@RequestMapping("/anno/test")
@ResponseBody
public String test() throws JsonProcessingException{
....
}
6.7.3 权限控制
如果我们不采用三方权限框架,可自定义权限,这里提供思路
注解
@Target({ElementType.METHOD,ElementType.TYPE}) // 这个注解可以放在也可以放在方法上的。
@Retention(RetentionPolicy.RUNTIME)
public @interface Authority {
Role[] roles() ;
}
public enum Role {
SADMIN, //超管
ADMIN, //管理
TEACHER, //教师
STUDENT //学生
}
使用注解
@Authority(roles = {Role.ADMIN, Role.SADMIN}) // 放在类上 说明这个类下所有的方法都需要有这个权限才可以进行访问
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/hello")
public String Hello(){
return "hello 你最近还好吗";
}
}
@Controller
@RequestMapping("/student")
public class StudentController {
@Authority(roles = {Role.STUDENT}) // 放在方法上则说明此方法需要注解上的权限才能进行访问
@GetMapping("/test")
public String test(){
return "你好,我已经不是一名学生啦";
}
}
编写 SpringMVC 拦截器及处理注解的Handler
在其中进行 Token,和访问方法的权限判断,看方法上是否有注解,有的话,
就和当前用户对比,成功就可以访问,失败就直接拒绝。
public class LoginInterceptor extends HandlerInterceptorAdapter {
private static final Logger log = LoggerFactory.getLogger(WebExceptionHandler.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//实现权限判断功能
}
6.7.4 自定义注解+AOP+Redis 防止重复提交
先简单说一下防止重复提交注解的逻辑:
1.在需要防止重复提交的接口的方法,加上注解。
2.发送请求写接口携带 Token
3.请求的路径+ Token 拼接程 key,value 值为生成的 UUID 码
4. 然后 set Redis 分布式锁,能获取到就顺利提交(分布式锁默认 5 秒过期),不能获取就是重复提交了,报错。
注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
/**
* 设置请求锁定时间
* @return
*/
int lockTime() default 5;
}
定义处理注解的切面类
import com.eshop.api.ApiResult;
import com.eshop.common.aop.NoRepeatSubmit;
import com.eshop.common.util.RedisLock;
import com.eshop.common.util.RequestUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
/**
* 重复提交aop
*/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {
@Autowired
private RedisLock redisLock;
@Pointcut("@annotation(noRepeatSubmit)")
public void pointCut(NoRepeatSubmit noRepeatSubmit) {
}
@Around("pointCut(noRepeatSubmit)")
public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
int lockSeconds = noRepeatSubmit.lockTime();
HttpServletRequest request = RequestUtils.getRequest();
Assert.notNull(request, "request can not null");
String bearerToken = request.getHeader("Authorization");
String[] tokens = bearerToken.split(" ");
String token = tokens[1];
String path = request.getServletPath();
String key = getKey(token, path);
String clientId = getClientId();
boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);
log.info("tryLock key = [{}], clientId = [{}]", key, clientId);
if (isSuccess) {
log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
// 获取锁成功
Object result;
try {
// 执行进程
result = pjp.proceed();
} finally {
// 解锁
redisLock.releaseLock(key, clientId);
log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
}
return result;
} else {
// 获取锁失败,认为是重复提交的请求
log.info("tryLock fail, key = [{}]", key);
return ApiResult.fail("重复请求,请稍后再试");
}
}
private String getKey(String token, String path) {
return token + path;
}
private String getClientId() {
return UUID.randomUUID().toString();
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
/**
* Redis 分布式锁实现
*/
@Service
public class RedisLock {
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
// 当前设置 过期时间单位, EX = seconds; PX = milliseconds
private static final String SET_WITH_EXPIRE_TIME = "EX";
// if get(key) == value return del(key)
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
*
* 支持重复,线程安全
*
* @param lockKey 加锁键
* @param clientId 加锁客户端唯一标识(采用UUID)
* @param seconds 锁过期时间
* @return
*/
public boolean tryLock(String lockKey, String clientId, long seconds) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
SetParams setParams = new SetParams();
String result = jedis.set(lockKey, clientId, setParams.nx().px(seconds));
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
});
}
/**
* 与 tryLock 相对应,用作释放锁
*
* @param lockKey
* @param clientId
* @return
*/
public boolean releaseLock(String lockKey, String clientId) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
Collections.singletonList(clientId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
});
}
}
6.7 总结
注解在做基础通用组件有非常广泛的应用,我们要多看第三方中间件的源代码,获得使用注解的技巧