上节我们探讨了反射,反射相关的类中都有方法获取注解信息,我们在前面章节中也多次提到过注解,注解到底是什么呢?
在Java中,注解就是给程序添加一些信息,用字符@开头,这些信息用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等,注解可以被编译器、程序运行时、和其他工具使用,用于增强或修改程序行为等。这么说比较抽象,下面我们会具体来看,先来看Java的一些内置注解。
内置注解 Java内置了一些常用注解,比如:@Override、@Deprecated、@SuppressWarnings,我们简要介绍下。
@Override
@Override修饰一个方法,表示该方法不是当前类首先声明的,而是在某个父类或实现的接口中声明的,当前类"重写"了该方法,比如:
static class Base { public void action() {}; } static class Child extends Base { @Override public void action(){ System.out.println("child action"); } @Override public String toString() { return "child"; } }
Child的action()重写了父类Base中的action(),toString()重写了Object类中的toString()。这个注解不写也不会改变这些方法是"重写"的本质,那有什么用呢?它可以减少一些编程错误。如果方法有Override注解,但没有任何父类或实现的接口声明该方法,则编译器会报错,强制程序员修复该问题。比如,在上面的例子中,如果程序员修改了Base方法中的action方法定义,变为了:
static class Base { public void doAction() {}; }
但是,程序员忘记了修改Child方法,如果没有Override注解,编译器不会报告任何错误,它会认为action方法是Child新加的方法,doAction会调用父类的方法,这与程序员的期望是不符的,而有了Override注解,编译器就会报告错误。所以,如果方法是在父类或接口中定义的,加上@Override吧,让编译器帮你减少错误。
@Deprecated
@Deprecated可以修饰的范围很广,包括类、方法、字段、参数等,它表示对应的代码已经过时了,程序员不应该使用它,不过,它是一种警告,而不是强制性的,在IDE如Eclipse中,会给Deprecated元素加一条删除线以示警告,比如,Date中很多方法就过时了:
@Deprecated public Date(int year, int month, int date) @Deprecated public int getYear()
调用这些方法,编译器也会显示删除线并警告,比如:
在声明元素为@Deprecated时,应该用Java文档注释的方式同时说明替代方案,就像Date中的API文档那样,在调用@Deprecated方法时,应该先考虑其建议的替代方案。
@SuppressWarnings
@SuppressWarnings表示压制Java的编译警告,它有一个必填参数,表示压制哪种类型的警告,它也可以修饰大部分代码元素,在更大范围的修饰也会对内部元素起效,比如,在类上的注解会影响到方法,在方法上的注解会影响到代码行。对于上面Date方法的调用,如果不希望显示警告,可以这样:
@SuppressWarnings({"deprecation","unused"}) public static void main(String[] args) { Date date = new Date(2017, 4, 12); int year = date.getYear(); }
除了这些内置注解,Java并没有给我们提供更多的可以直接使用的注解,我们日常开发中使用的注解基本都是自定义的,不过,一般也不是我们定义的,而是由各种框架和库定义的,我们主要还是根据它们的文档直接使用。 框架和库的注解 各种框架和库定义了大量的注解,程序员使用这些注解配置框架和库,与它们进行交互,我们看一些例子。
Jackson 在63节,我们介绍了通用的序列化库Jackson,并介绍了如何利用注解对序列化进行定制,比如:
在Java提供注解功能之前,同样的配置功能也是可以实现的,一般通过配置文件实现,但是配置项和要配置的程序元素不在一个地方,难以管理和维护,使用注解就简单多了,代码和配置放在一起,一目了然,易于理解和维护。
依赖注入容器 现代Java开发经常利用某种框架管理对象的生命周期及其依赖关系,这个框架一般称为DI(Dependency Injection)容器,DI是指依赖注入,流行的框架有Spring、Guice等,在使用这些框架时,程序员一般不通过new创建对象,而是由容器管理对象的创建,对于依赖的服务,也不需要自己管理,而是使用注解表达依赖关系。这么做的好处有很多,代码更为简单,也更为灵活,比如容器可以根据配置返回一个动态代理,实现AOP,这部分我们后续章节再介绍。
看个简单的例子,Guice定义了Inject注解,可以使用它表达依赖关系,比如像下面这样:
public class OrderService { @Inject UserService userService; @Inject ProductService productService; //.... }
Servlet 3.0 Servlet是Java为Web应用提供的技术框架,早期的Servlet只能在web.xml中进行配置,而Servlet 3.0则开始支持注解,可以使用@WebServlet配置一个类为Servlet,比如:
@WebServlet(urlPatterns = "/async", asyncSupported = true) public class AsyncDemoServlet extends HttpServlet {...}
Web应用框架 在Web开发中,典型的架构都是MVC(Model-View-Controller),典型的需求是配置哪个方法处理哪个URL的什么HTTP方法,然后将HTTP请求参数映射为Java方法的参数,各种框架如Spring MVC, Jersey等都支持使用注解进行配置,比如,使用Jersey的一个配置示例为:
@Path("/hello") public class HelloResource { @GET @Path("test") @Produces(MediaType.APPLICATION_JSON) public Map<String, Object> test( @QueryParam("a") String a) { Map<String, Object> map = new HashMap<>(); map.put("status", "ok"); return map; } }
类HelloResource将处理Jersey配置的根路径下/hello下的所有请求,而test方法将处理/hello/test的GET请求,响应格式为JSON,自动映射HTTP请求参数a到方法参数String a。
神奇的注解 通过以上的例子,我们可以看出,注解似乎有某种神奇的力量,通过简单的声明,就可以达到某种效果。在某些方面,它类似于我们在62节介绍的序列化,序列化机制中通过简单的Serializable接口,Java就能自动处理很多复杂的事情。它也类似于我们在并发部分中介绍的synchronized关键字,通过它可以自动实现同步访问。
这些都是声明式编程风格,在这种风格中,程序都由三个组件组成:
在编程的世界里,访问数据库的SQL语言,编写网页样式的CSS,以及后续章节将要介绍的正则表达式、函数式编程都是这种风格,这种风格降低了编程的难度,为应用程序员提供了更为高级的语言,使得程序员可以在更高的抽象层次上思考和解决问题,而不是陷于底层的细节实现。
创建注解
框架和库是怎么实现注解的呢?我们来看注解的创建。
@Override的定义
我们通过一些例子来说明,先看@Override的定义:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
定义注解与定义接口有点类似,都用了interface,不过注解的interface前多了@,另外,它还有两个元注解@Target和@Retention,这两个注解专门用于定义注解本身。
@Target
@Target表示注解的目标,@Override的目标是方法(ElementType.METHOD),ElementType是一个枚举,其他可选值有:
目标可以有多个,用{}表示,比如@SuppressWarnings的@Target就有多个,定义为:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); }
如果没有声明@Target,默认为适用于所有类型。
@Retention
@Retention表示注解信息保留到什么时候,取值只能有一个,类型为RetentionPolicy,它是一个枚举,有三个取值:
如果没有声明@Retention,默认为CLASS。 @Override和@SuppressWarnings都是给编译器用的,所以@Retention都是RetentionPolicy.SOURCE。
定义参数 可以为注解定义一些参数,定义的方式是在注解内定义一些方法,比如@SuppressWarnings内定义的方法value,返回值类型表示参数的类型,这里是String[],使用@SuppressWarnings时必须给value提供值,比如:
@SuppressWarnings(value={"deprecation","unused"})
当只有一个参数,且名称为value时,提供参数值时可以省略"value=",即上面的代码可以简写为:
@SuppressWarnings({"deprecation","unused"})
注解内参数的类型不是什么都可以的,合法的类型有基本类型、String、Class、枚举、注解、以及这些类型的数组。
参数定义时可以使用default指定一个默认值,比如,Guice中Inject注解的定义:
@Target({ METHOD, CONSTRUCTOR, FIELD }) @Retention(RUNTIME) @Documented public @interface Inject { boolean optional() default false; }
它有一个参数optional,默认值为false。如果类型为String,默认值可以为"",但不能为null。如果定义了参数且没有提供默认值,在使用注解时必须提供具体的值,不能为null。
@Inject多了一个元注解@Documented,它表示注解信息包含到Javadoc中。
@Inherited
与接口和类不同,注解不能继承。不过注解有一个与继承有关的元注解@Inherited,它是什么意思呢?我们看个例子:
public class InheritDemo { @Inherited @Retention(RetentionPolicy.RUNTIME) static @interface Test { } @Test static class Base { } static class Child extends Base { } public static void main(String[] args) { System.out.println(Child.class.isAnnotationPresent(Test.class)); } }
Test是一个注解,类Base有该注解,Child继承了Base但没有声明该注解,main方法检查Child类是否有Test注解,输出为true,这是因为Test有注解@Inherited,如果去掉,输出就变成false了。 查看注解信息 创建了注解,就可以在程序中使用,注解指定的目标,提供需要的参数,但这还是不会影响到程序的运行。要影响程序,我们要先能查看这些信息。我们主要考虑@Retention为RetentionPolicy.RUNTIME的注解,利用反射机制在运行时进行查看和利用这些信息。 在上节中,我们提到了反射相关类中与注解有关的方法,这里汇总说明下,Class、Field、Method、Constructor中都有如下方法:
//获取所有的注解 public Annotation[] getAnnotations() //获取所有本元素上直接声明的注解,忽略inherited来的 public Annotation[] getDeclaredAnnotations() //获取指定类型的注解,没有返回null public <A extends Annotation> A getAnnotation(Class<A> annotationClass) //判断是否有指定类型的注解 public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
Annotation是一个接口,它表示注解,具体定义为:
public interface Annotation { boolean equals(Object obj); int hashCode(); String toString(); //返回真正的注解类型 Class<? extends Annotation> annotationType(); }
实际上,所有的注解类型,内部实现时,都是扩展的Annotation。 对于Method和Contructor,它们都有方法参数,而参数也可以有注解,所以它们都有如下方法:
public Annotation[][] getParameterAnnotations()
返回值是一个二维数组,每个参数对应一个一维数组,我们看个简单的例子:
public class MethodAnnotations { @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) static @interface QueryParam { String value(); } @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) static @interface DefaultValue { String value() default ""; } public void hello(@QueryParam("action") String action, @QueryParam("sort") @DefaultValue("asc") String sort){ // ... } public static void main(String[] args) throws Exception { Class<?> cls = MethodAnnotations.class; Method method = cls.getMethod("hello", new Class[]{String.class, String.class}); Annotation[][] annts = method.getParameterAnnotations(); for(int i=0; i<annts.length; i++){ System.out.println("annotations for paramter " + (i+1)); Annotation[] anntArr = annts[i]; for(Annotation annt : anntArr){ if(annt instanceof QueryParam){ QueryParam qp = (QueryParam)annt; System.out.println(qp.annotationType().getSimpleName()+":"+ qp.value()); }else if(annt instanceof DefaultValue){ DefaultValue dv = (DefaultValue)annt; System.out.println(dv.annotationType().getSimpleName()+":"+ dv.value()); } } } } }
这里定义了两个注解@QueryParam和@DefaultValue,都用于修饰方法参数,方法hello使用了这两个注解,在main方法中,我们演示了如何获取方法参数的注解信息,输出为:
annotations for paramter 1 QueryParam:action annotations for paramter 2 QueryParam:sort DefaultValue:asc
代码比较简单,就不赘述了。 定义了注解,通过反射获取到注解信息,但具体怎么利用这些信息呢?我们看两个简单的示例,一个是定制序列化,另一个是DI容器。 应用注解 - 定制序列化
定义注解 上节我们演示了一个简单的通用序列化类SimpleMapper,在将对象转换为字符串时,格式是固定的,本节演示如何对输出格式进行定制化。我们实现一个简单的类SimpleFormatter,它有一个方法:
public static String format(Object obj)
我们定义两个注解,@Label和@Format,@Label用于定制输出字段的名称,@Format用于定义日期类型的输出格式,它们的定义如下:
@Retention(RUNTIME) @Target(FIELD) public @interface Label { String value() default ""; } @Retention(RUNTIME) @Target(FIELD) public @interface Format { String pattern() default "yyyy-MM-dd HH:mm:ss"; String timezone() default "GMT+8"; }
使用注解 可以用这两个注解来修饰要序列化的类字段,比如:
static class Student { @Label("姓名") String name; @Label("出生日期") @Format(pattern="yyyy/MM/dd") Date born; @Label("分数") double score; public Student() { } public Student(String name, Date born, Double score) { super(); this.name = name; this.born = born; this.score = score; } @Override public String toString() { return "Student [name=" + name + ", born=" + born + ", score=" + score + "]"; } }
我们可以这样来使用SimpleFormatter:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Student zhangsan = new Student("张三", sdf.parse("1990-12-12"), 80.9d); System.out.println(SimpleFormatter.format(zhangsan));
输出为:
姓名:张三 出生日期:1990/12/12 分数:80.9
利用注解信息 可以看出,输出使用了自定义的字段名称和日期格式,SimpleFormatter.format()是怎么利用这些注解的呢?我们看代码:
public static String format(Object obj) { try { Class<?> cls = obj.getClass(); StringBuilder sb = new StringBuilder(); for (Field f : cls.getDeclaredFields()) { if (!f.isAccessible()) { f.setAccessible(true); } Label label = f.getAnnotation(Label.class); String name = label != null ? label.value() : f.getName(); Object value = f.get(obj); if (value != null && f.getType() == Date.class) { value = formatDate(f, value); } sb.append(name + ":" + value + "\n"); } return sb.toString(); } catch (IllegalAccessException e) { throw new RuntimeException(e); } }
对于日期类型的字段,调用了formatDate,其代码为:
private static Object formatDate(Field f, Object value) { Format format = f.getAnnotation(Format.class); if (format != null) { SimpleDateFormat sdf = new SimpleDateFormat(format.pattern()); sdf.setTimeZone(TimeZone.getTimeZone(format.timezone())); return sdf.format(value); } return value; }
这些代码都比较简单,我们就不解释了。 应用注解 - DI容器
定义@SimpleInject
我们再来看一个简单的DI容器的例子,我们引入一个注解@SimpleInject,修饰类中字段,表达依赖关系,定义为:
@Retention(RUNTIME) @Target(FIELD) public @interface SimpleInject { }
使用@SimpleInject 我们看两个简单的服务ServiceA和ServiceB,ServiceA依赖于ServiceB,它们的定义为:
public class ServiceA { @SimpleInject ServiceB b; public void callB(){ b.action(); } } public class ServiceB { public void action(){ System.out.println("I'm B"); } }
ServiceA使用@SimpleInject表达对ServiceB的依赖。 DI容器的类为SimpleContainer,提供一个方法:
public static <T> T getInstance(Class<T> cls)
应用程序使用该方法获取对象实例,而不是自己new,使用方法如下所示:
ServiceA a = SimpleContainer.getInstance(ServiceA.class); a.callB();
利用@SimpleInject SimpleContainer.getInstance会创建需要的对象,并配置依赖关系,其代码为:
public static <T> T getInstance(Class<T> cls) { try { T obj = cls.newInstance(); Field[] fields = cls.getDeclaredFields(); for (Field f : fields) { if (f.isAnnotationPresent(SimpleInject.class)) { if (!f.isAccessible()) { f.setAccessible(true); } Class<?> fieldCls = f.getType(); f.set(obj, getInstance(fieldCls)); } } return obj; } catch (Exception e) { throw new RuntimeException(e); } }
代码假定每个类型都有一个public默认构造方法,使用它创建对象,然后查看每个字段,如果有SimpleInject注解,就根据字段类型获取该类型的实例,并设置字段的值。
定义@SimpleSingleton
在上面的代码中,每次获取一个类型的对象,都会新创建一个对象,实际开发中,这可能不是期望的结果,期望的模式可能是单例,即每个类型只创建一个对象,该对象被所有访问的代码共享,怎么满足这种需求呢?我们增加一个注解@SimpleSingleton,用于修饰类,表示类型是单例,定义如下:
@Retention(RUNTIME) @Target(TYPE) public @interface SimpleSingleton { }
使用@SimpleSingleton
我们可以这样修饰ServiceB:
@SimpleSingleton public class ServiceB { public void action(){ System.out.println("I'm B"); } }
利用@SimpleSingleton
SimpleContainer也需要做修改,首先增加一个静态变量,缓存创建过的单例对象:
private static Map<Class<?>, Object> instances = new ConcurrentHashMap<>();
getInstance也需要做修改,如下所示:
public static <T> T getInstance(Class<T> cls) { try { boolean singleton = cls.isAnnotationPresent(SimpleSingleton.class); if (!singleton) { return createInstance(cls); } Object obj = instances.get(cls); if (obj != null) { return (T) obj; } synchronized (cls) { obj = instances.get(cls); if (obj == null) { obj = createInstance(cls); instances.put(cls, obj); } } return (T) obj; } catch (Exception e) { throw new RuntimeException(e); } }
首先检查类型是否是单例,如果不是,就直接调用createInstance创建对象。否则,检查缓存,如果有,直接返回,没有的话,调用createInstance创建对象,并放入缓存中。
createInstance与第一版的getInstance类似,代码为:
private static <T> T createInstance(Class<T> cls) throws Exception { T obj = cls.newInstance(); Field[] fields = cls.getDeclaredFields(); for (Field f : fields) { if (f.isAnnotationPresent(SimpleInject.class)) { if (!f.isAccessible()) { f.setAccessible(true); } Class<?> fieldCls = f.getType(); f.set(obj, getInstance(fieldCls)); } } return obj; }
小结 本节介绍了Java中的注解,包括注解的使用、自定义注解和应用示例。
注解提升了Java语言的表达能力,有效地实现了应用功能和底层功能的分离,框架/库的程序员可以专注于底层实现,借助反射实现通用功能,提供注解给应用程序员使用,应用程序员可以专注于应用功能,通过简单的声明式注解与框架/库进行协作。 下一节,我们来探讨Java中一种更为动态灵活的机制 - 动态代理。
(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic,位于包shuo.laoma.dynamic.c85下)