上一章 详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第3章 剖析依赖装配参数
下一章 详解SSJ(Spring3.x mvc + Spring3.x Core + JPA2.x)轻量级集成开发—第5章 剖析Spring3.x AOP特性01
目录
一、Annotation特性;
二、元注解详解;
三、编写自定义注解;
四、编写注解解析器;
五、使用注解实现Bean自动装配;
六、Spring3.x常用注解讲解;
前言
上一章的内容笔者主要围绕依赖装配参数来进行讲解,其中包含了原始数据类型、引用类型(集合类型)的注入方式。如果对依赖装配或者IOC容器了解不是很透彻的朋友,笔者建议你先暂时不要阅读本篇博文,因为该篇博文已经属于简化配置的文章,当你对原始操作不熟悉的时候,阅读本文,你可能会毫无收获。
笔者在此感谢那些给笔者提过意见的朋友,当然不管是负面的还是正面的,笔者都感谢你们。有很多朋友告诉笔者,希望直接看到SSJ的整合过程,而不是细粒度的功能讲解。笔者对此确实很无奈,只能说抱歉,因为笔者不太希望写一些浪费笔者时间的博文,当然如果是因为阅读笔者的文章而耽误了你宝贵的时间,笔者在此表示深深的歉意。
一、Annotation特性
Annotation中文称作注解(以下简称注解),其实换句话说和注释差不多的意思。只不过为了区分注解拥有的功能性操作,从而中文翻译为注解。注解是JDK5.0引入的新特性,它适合应用于描述代码结构(类型、方法、属性)的一种元数据。注解采用键值对的方式对数据进行操作,但就原则性而言,注解并不会直接影响代码语义,但运行时的程序往往由于调用了注解的操作而被反向影响(类似第三方构件依赖)。
就目前而言,注解采用的是运行时方式动态加载运行,换句话来说就是采用运行时反射加载运行。来看看JDK1.5中为开发人员提供的常用注解:
注解名称 | 注解作用 | 作用域 |
@Override | 被描述的方法重写了超类方法 | Method |
@Deprecated | 用于描述已经过期的组件 | 暂无 |
@SuppressWarnings | 用于关闭编译器对组件成员的警告 | TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR, LOCAL_VARIABLE |
先来看看@Override,笔者相信该注解应该是大家最为熟知的,因为它经常时不时的出现在派生类重写超类的方法上。
@Override示例如下:
public class TestA { public void testA() { // ... } } class TestB extends TestA { @Override public void testA() { // TODO Auto-generated method stub super.testA(); } }
通过上述程序示例,相信大家应该对@Override有所印象的了吧。但是如果你的JDK版本低于5.0,那么你将无法使用该注解对你派生重写方法进行描述。@Deprecated用于描述过时组件,不知大家是否记得,当组件内部或者组件本身已经过时后,其上方存在一条横杠黑线?
@Deprecated示例如下:
如果你需要描述你的组件已过期时,你可以在其上方添加@Deprecated进行描述即可。这样一来被描述的组件就会沦为过时组件。@SuppressWarnings用于关闭编译器对组件成员的警告,在此笔者就不再继续演示了,有兴趣的朋友可以自己去尝试。
二、元注解详解
我们都知道元数据其实就是用于描述数据的数据,那么元注解也就是用于描述注解的一种特殊注解。Java目前一共给我们提供了4种常用的元注解,分别为:
1、@Retention:用于定义注解的保留策略;
2、@Target:定义注解的作用域范围;
3、@Documented:定义注解允许包含在JavaDoc中;
4、@Inherited:定义派生类型允许继承超类中的目标注解;
先来看看@Retention的用法,该注解用于定义注解的保留策略。什么是保留策略呢?也就是说如果我们通过@Retention描述指定注解后,该注解是以何种存储策略标记于指定数据源中。
我们可以使用RetentionPolicy枚举常量来定义@Retention的参数,@Retention的保留策略大致分为3种:
1、@Retention(RetentionPolicy.SOURCE) :该策略仅允许注解保存于源码中;
2、@Retention(RetentionPolicy.CLASS) :缺省保留策略,存在于源码和字节码中,但运行时无法获取;
3、@Retention(RetentionPolicy.RUNTIME) : 该策略允许存在于源码和字节码中,但运行时可以获取;
提及作用域,笔者相信大家都应该知道这个概念,因为作用域直接影响到了成员的可见性。如果你希望使用@Target来定义注解的作用域范围,那么你可以使用ElementType枚举常量来定义@Target的参数,@Target的作用域范围一共包含8种:
1、@Target(ElementType.TYPE) :作用域范围包含:接口、类、枚举、注解;
2、@Target(ElementType.FIELD) :作用域范围包含:字段、枚举的常量;
3、@Target(ElementType.METHOD) :作用域范围包含:方法;
4、@Target(ElementType.PARAMETER) :作用域范围包含:方法、参数;
5、@Target(ElementType.CONSTRUCTOR) :作用域范围包含:构造函数;
6、@Target(ElementType.LOCAL_VARIABLE):作用域范围包含:局部变量;
7、@Target(ElementType.ANNOTATION_TYPE):作用域范围包含:注解;
8、@Target(ElementType.PACKAGE):作用域范围包含:包;
提示:
1、@Target允许指定多个作用域范围,比如:
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
当然你也可以简写为:
import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import java.lang.annotation.Target; @Target({TYPE, FIELD, METHOD})
2、@Document定义了注解允许包含在JavaDoc中,而@Inherited则定义了派生类型允许继承超类中的目标注解。只不过这2个原注解并不需要定义注解参数,所以故不再继续演示。
三、编写自定义注解
在上一章节中,笔者为大家详细的讲解了有关注解的基本概念,那么接下来咱们就来看看如何编写自定义注解。就目前而言,许多第三方开源Framework基本上都在依靠使用注解达到简化代码,降低依赖的目的,那么笔者在此有必要为大家揭开注解内部的神秘面纱。
在很多年前,几乎所有的开发人员都希望将可配置型代码写进配置文件中,因为这样除了可以很好的解决代码耦合的问题,同样也可以解决系统每次更改属性都要反复部署的问题。但随着时间的推移,咱们的配置文件几乎越来越庞大,文件中的内容越来越冗长,为了很好解决配置文件的瘦身问题,越来越多的开源项目又开始将配置文件中的内容移至代码中,Spring其实就是其中一个。虽然这种做法在理论上看来无非是重量级的操作,但绝非是高侵入式的,因为很多开源项目并不是直接将第三方代码侵入进咱们的业务逻辑中,而只是以Annotation的方式进行嵌入。这种“侵入级别”而言对于开发人员来说并不是不能接受的,并且也是某些开源项目的目标,就像.net平台一样,始终都允许只用微软的东西,说不定某些开源项目拥有技术垄断的商业嫌疑(开个玩笑)。
我们都知道定义一个接口使用的是interface关键字,那么定义注解我们则采用的是@interface关键字。
使用@interface定义自定义注解:
public @interface MyService { //... }
笔者通过使用@interface关键字定义了一个标准的自定义注解。其实你完全可以把注解理解为一种特殊接口,因为注解和接口在一定程度上拥有太多的相似之处,如果你能很好的理解接口,那么编写自定义注解并不是一件难事。
回想一下原注解是用来干嘛的?没错,原注解是用于描述注解的一种特殊注解。那么接下来咱们就来看看如何对自定义注解进行元注解描述:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyService { //... }
在大部分情况下,我们编写的自定义注解往往是需要带参数的。那么如何在注解内部定义注解属性是接下来我们要学习的。千万别把定义注解属性想的过于复杂了,你就把它当做是常规属性进行定义即可。
定义自定义注解属性:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyService { public String name(); public int age(); public char sex(); }
在上述程序示例中,我们在自定义注解内部定义了3个注解属性。或许会有很多朋友会觉得奇怪,这是不是方法呢?可以抛出异常吗?可以指定实现重写吗?其实眨眼一看,这确实是类似一个抽象方法。但这个“抽象方法”却很特殊,因为它并不允许你抛出异常和指定实现重写。因为这个“抽象方法”的目的仅在于接收参数,然后根据不同的数据类型(注解并没有强制要求属性类型,你可以定义原始类型,同样也可以定义引用类型)存储于不同的内存区域中,最后持有即可,其实注解属性更像是和变量类似的媒介操作。
我们都知道缺省的构造函数会自动完成成员变量的初始化工作。但考虑到注解是基于运行时动态加载运行的,并且定义在注解内部的属性,既不是变量更不是纯粹的方法,该如何初始呢?在自定义注解中我们可以通过default关键字定义注解属性初始值。
使用default关键字定义注解属性初始值:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyService { public String name() default "admin"; public int age() default 20; public char sex() default '男'; }
上述程序示例中,笔者通过default关键字定义了注解成员的初始值。这和咱们平常使用的“=”符号类似,都可以理解为赋值操作。
提示:
注解内部缺省属性名称为value,当你在使用注解的时候,不想显式的声明属性名称,你则可以将属性名称缺省为value。
四、编写注解解析器
前面咱们学习了如何编写自定义注解。可能很多朋友在想,注解是定义好了,但怎么解析使用呢?别急,本章笔者将带领大家一起来编写注解解析器去解析咱们的自定义注解。之前笔者也说过,注解是通过运行时动态加载运行的,说白了就是基于反射解析运行。但是如果你本身并不了解反射机制,请先了解下反射原理及使用方式再来阅读本章。
如果你使用过Spring提供的@Autowired,那么接下来笔者将模拟一个通过注解注入的程序来示例注解解析器。在开始编写注解解析器之前,我们先来编写一个用于标注注入属性的自定义注解:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface MyService { //... }
上述自定义注解仅仅只是一个标记注解,注解内部并没有包含任何属性。并且该注解的作用域仅限于组件字段,也就是说接下来咱们要编写一个需要属性注入的POJO:
public class ClientTest { @MyService public String name; @MyService public int age; @MyService public char sex; /* 单例模式 */ private static ClientTest clientTest; static { clientTest = new ClientTest(); } public static ClientTest getClientTest() { return clientTest; } }
上述程序示例中,笔者定义了3个需要使用注解外部注入的属性。并且笔者将这个POJO定义为单例模式,这主要是为了方便笔者在编写注解解析器时对属性进行值注入。
当所有的准备工作都已经准备好后,咱们就要开始编写注解解析器了。解析器本身并不复杂,无非也就是反射技术的运用,而且反射机制对注解也提供了很好的支持。
编写自定义注解解析器:
public class AnnotationResolver { public static void resolver() { try { Field[] fields = Class.forName(ClientTest.class.getName()) .getFields(); for (Field field : fields) { /* 仅限注解标记成员 */ if (field.isAnnotationPresent(MyService.class)) { if (field.getName().equals("name")) ClientTest.getClientTest().name = "JohnGao"; if (field.getName().equals("age")) ClientTest.getClientTest().age = 27; if (field.getName().equals("sex")) ClientTest.getClientTest().sex = '男'; } } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
上述程序示例中,isAnnotationPresent()方法最为关键。使用该方法可以很方便的判断组件成员是否被指定注解标注。既然我们已经编写好注解解析器了,最后我们还需编写一个客户端来测试咱们的注解解析器是否能够成功解析自定义注解。
编写测试客户端:
/** * @param args */ public static void main(String[] args) { /* 使用注解解析器解析注解 */ AnnotationResolver.resolver(); /* 输出注入值 */ System.out.println(ClientTest.getClientTest().name); System.out.println(ClientTest.getClientTest().age); System.out.println(ClientTest.getClientTest().sex); }
通过上述程序示例,笔者相信注解解析器的使用大家应该都能理解了。不管是组件本身还是组件成员,我们都可以使用反射机制为我们提供的注解支持来进行相应的业务操作。
最后一个问题,我们如何通过注解解析器去获取注解成员?既然反射为咱们提供了良好的注解支持,这仍然不是一件难事,我们可以通过使用getAnnotation()方法即可获取到。
使用getAnnotation()方法获取注解成员之前,我们需要修改一下我们的自定义注解:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface MyService { public String name() default "JohnGao"; }
上述程序示例中,我们不再使用标记注解,而是在该注解内部定义了一个名称为name的注解成员。
修改注解解析器:
public class AnnotationResolver { public static void resolver() { try { Field field = Class.forName(ClientTest.class.getName()).getField( "name"); /* 反射获取注解实例 */ MyService myService = field.getAnnotation(MyService.class); System.out.println(myService.name()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
至此,关于如何编写自定义注解和注解解析器的讲解就到此结束。如果仍然有不清楚的朋友,笔者希望你通过邮件的形式告知笔者,笔者将在第一时间对其进行回复。
五、使用注解实现Bean自动装配
在本系列博文的第一、二章中,笔者详细的为大家讲解了Bean的依赖装配。在Spring2.5版本以前,使用配置文件的形式对Bean进行依赖装配几乎是唯一选择,但在Spring2.5版本以后,我们完全可以通过使用注解来简化原本繁琐的装配形式,这种形式我们称之为Bean的自动装配。
Spring是基于松耦合的设计,也就是说Spring并没有强制要求你一定要使用Bean的自动装配,你仍然可以在最新版本中保留基于配置文件形式的装配方式,或许这对于不太喜欢使用注解的开发人员而言是一种不错的选择。
使用自动装配形式可以使咱们的依赖装配过程更加简单,但也有不少开发人员觉得自动装配虽然简单,但却打乱了原本结构清晰的依赖关系。在笔者看来这源于注解本身,无论如何注解的流行是趋势,不管你是否愿意接受。
简单来说我们可以通过使用@Autowired、@Resource、@Inject等这3个注解来实现Bean的自动装配。在开始使用自动装配之前,我们先来看看基于配置文件的装配形式。
首先我们定义好需要被注入的Bean:
public class Test { private TestA testA; private TestB testB; /* 此处省略set和get方法 */ }
定义好Bean后,我们还需在IOC配置文件中进行Bean装配和依赖装配。
编写IOC配置文件:
<!-- 定义Test实例 --> <bean name="test" class="Test"> <!-- 投值注入 --> <property name="testA" ref="testA"/> <property name="testB" ref="testB"/> </bean> <!-- 定义TestA实例 --> <bean name="testA" class="TestA"/> <!-- 定义TestB实例 --> <bean name="testB" class="TestB"/>
从上述配置文件中我们可以看出,基于配置文件的Bean装配形式依赖关系非常明确和清晰。Test类型内部一共投值注入了2个参数,分别testA和testB。但你想想看,假设Test内部需要注入更多的参数时该怎么办?如果还是按照配置文件的形式进行Bean装配,那么配置文件将会变得极其冗长,维护性极低。或许有人会说那多用几个配置文件就可以啊,其实不论你用几个配置文件,你都无法掩盖配置文件内容的爆炸式增长。这种不利于扩展的做法到了Spring2.5及以上版本后,将完全得到了改善。我们首先来看如何使用@Autowired实现Bean的自动装配。
导入context的命名空间:
xmlns:context="http://www.springframework.org/schema/context" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
当成功导入命名空间后,接下来我们还需要在配置文件中声明自动装配:
<context:annotation-config />
<context:annotation-config />标签的作用是隐式的向IOC容器注入如下4个Bean类型:
1、AutowiredAnnotationBeanPostProcessor;
2、CommonAnnotationBeanPostProcessor;
3、PersistenceAnnotationBeanPostProcessor;
4、RequiredAnnotationBeanPostProcessor ;
如果我们想在程序中使用@Autowired,那么我们就必须显式的在IOC配置文件中声明:
<bean class="org.springframework.beans.factory.annotation. AutowiredAnnotationBeanPostProcessor"/>
如果我们想在程序中使用@Resource、@PostConstruct、@PreDestroy等注解,那么我们就必须显式的在IOC配置文件中声明:
<bean class="org.springframework.context.annotation. CommonAnnotationBeanPostProcessor"/>
如果我们想在程序中使用@PersistenceContext,那么我们就必须显式的在IOC配置文件中声明:
<bean class="org.springframework.orm.jpa.support. PersistenceAnnotationBeanPostProcessor"/>
如果我们想在程序中使用@Required,那么我们就必须显式的在IOC配置文件中声明:
<bean class="org.springframework.beans.factory.annotation. RequiredAnnotationBeanPostProcessor"/>
在实际开发过程中,已经很少有人按照上述做法显示对Bean进行声明。你完全可以使用<context:annotation-config/>标签进行自动隐式注入。
一旦在程序中使用Bean的自动装配后,Bean中再也不必为注入属性编写setter和getter方法。
修改被注入的Bean:
public class Test { @Autowired public TestA testA; @Autowired public TestB testB; }
既然是Bean的自动装配,那么肯定是简化编写。
修改IOC配置文件:
<!-- 声明注解自动装配 --> <context:annotation-config /> <!-- 定义Test实例 --> <bean name="test" class="Test"/> <!-- 定义TestA实例 --> <bean name="testA" class="TestA"/> <!-- 定义TestB实例 --> <bean name="testB" class="TestB"/>
原本定义在Test内部的依赖装配,现在可以完全移除了。这样配置文件将得到极大的瘦身,且维护性也更好,只不过对于不习惯使用注解的开发人员来说,依赖关系可能就显得不是那么清晰了,如果要查询依赖,你恐怕得跑到具体的代码中去寻找。
笔者有个建议,使用<context:annotation-config/>进行自动声明的朋友,你完全可以考虑使用:
<context:component-scan base-package="包.类" />
在很多时候我们一旦使用注解,往往会配置扫描包路径项 。并且<context:component-scan base-package="包.类" />标签仍然包含有和<context:annotation-config/>一样的自动隐式注入功能,所以一旦我们使用<context:component-scan base-package="包.类" />后,完全有必要移除<context:annotation-config/>。
提示:
使用<context:component-scan/>标签自动扫包时,你除了可以显示定义目标路径,还可以使用通配符“*”的方式指定局部或全局扫包路径。
使用通配符“*”扫描局部目标路径:
<context:component-scan base-package="org.johngao.*" />
使用通配符“*”扫描全局目标路径:
<context:component-scan base-package="*" />
@Autowired由Spring提供,该注解缺省是基于类型装配。并允许开发人员定义在字段上或者setter方法上,只不过一旦我们使用Bean的自动装配后,就完全没有必要在为需要注入的字段提供setter或者getter方法。
这里大家需要注意的是,由于@Autowired采用的是类型装配,一旦IOC配置文件中存在多个类型相同的Bean时,IOC容器则无法知晓到底装该配哪一个,并且会抛出异常。
修改需要注入的Bean:
public class Test { @Autowired public TestA testA; }
上述程序示例中,仅仅只是包含有一个需要注入的属性。再来看看IOC配置文件:
<!-- 声明注解自动装配 --> <context:annotation-config /> <!-- 定义Test实例 --> <bean name="test" class="Test" /> <!-- 定义TestA实例 --> <bean name="test1" class="TestA" /> <bean name="test2" class="TestA" />
由于考虑到@Autowired是基于类型装配,如果一旦按照笔者上述配置来执行,IOC容器将无法找到指定Bean,并且会抛出异常。
当然我们可以使用@Autowired和@Qualifier进行注解联用,这样一来@Autowired就变成名称装配,不会再因为定义的类型相同而触发异常(IOC容器规定Bean名称必须唯一)。
@Autowired和@Qualifier注解联用:
public class Test { @Autowired @Qualifier("test1") public TestA testA; }
@Resource由J2EE(JSR-250)规范提供,目的很简单就是取缔@Autowired。J2EE推荐使用@Resource是因为其更加优秀。@Resource不仅支持类型装配,还支持名称装配。@Resource缺省按照名称装配,只有在找不到指定Bean时才按照类型装配。
使用@Resource进行注入:
public class Test { @Resource public TestA testA; }
使用@Resource还可以显式的指定其被注入Bean的名称:
@Resource(name="名称")
提示:
当然你不指定Bean的名称也是可以的,缺省使用变量名称作为被注入Bean的名称。而且在实际开发过程中,没有多少人会有闲心去指定Bean名称。而且一旦显式的指定了Bean名称后,@Resource一旦找不到指定Bean,则不会再进行类型装配。
@Inject由J2EE(JSR-330)规范提供,缺省按照类型进行装配,但功能比@Autowired少,使用的人并不多,所以笔者也不建议你使用该注解。
当然,总的来说大家可以使用4种方式实现Bean的自动装配。
1、单独使用Autowired注解(基于类型进行自动装配);
2、使用Autowrite和Quelifier联用(基于指定名称进行自动装配);
3、使用Resource注解(基于Bean名称->类型或者指定名称进行自动装配);
4、使用Inject注解(基于类型进行装配);
六、Spring3.x常用注解讲解
Spring为开发人员提供了许多优秀的注解,但笔者本章并不打算讲解所有的注解。仅仅只是针对笔者经常使用到的一些常用注解进行讲解,如果你希望看到更全面的注解内容,请参考Spring帮助手册。
经过笔者总结,Spring中常用的注解一共有如下12种:
1、@Component:用于声明不好归类的组件;
2、@Controller:用于声明Action组件;
3、@Service:用于声明Service组件;
4、@Repository:用于声明Dao组件;
5、@RequestMapping:Action请求映射;
6、@Resource:自动注入(由JSR-250规范提供);
7、@Autowired:自动注入;
8、@Qualifier:允许和@Autowired联合使用;
9、@Inject自动注入(由JSR-330规范提供);
10、@Transactional:事物管理;
11、@ResponseBody:用于将内容或对象作为HTTP响应正文返回;
12、@Scope:定义Bean作用域;
上述12种是笔者总结的最为常用的注解。咱们先从@Component开始进行讲解。在上一章节中,笔者为大家演示了Bean的自动装配。虽然我们不需要在IOC配置文件中显式的进行依赖装配,但仍然需要在配置文件中声明Bean组件。有没有办法可以移除这些声明在配置文件中的Bean组件呢?答案是肯定的,你可以在程序中使用@Component移除定义在配置文件中的<bean/>标签。
使用@Component声明组件:
@Component public class Test { @Resource public TestA testA; }
一旦我们在组件上方使用@Component进行描述后,则再不必在配置文件中显式声明Bean组件。并且这样一来,我们的配置文件也会得到极致瘦身。你再也不必将时间花在这些令人繁琐的配置上,完全可以专注于你的业务中。
@Controller、@Service以及@Repository注解其实和@Component是一样的性质,都是用于声明组件。只不过相对@Component而言,组件描述得更加细粒度。如果你想描述Dao组件,你可以使用@Repository。如果你想描述Service组件,你可以使用@Service。如果你想描述Action,你可以使用@Controller。然而对于@Component而言,更大程度上来说是用于描述那些不好归类的组件。
@Component类型的组件都拥有一个可选参数,那便是value。该参数用于指定Bean的名称,一般情况下我们没有必要指定Bean的名称,因为@Component等类型的组件缺省会默认组件的名称做为Bean的名称进行查找。
@Resource、@Autowired、@Qualifier以及@Inject注解在上一章节已经做过讲解和演示,本章笔者将不再继续对其进行讲解,如果有不清楚的朋友,你可以返回上一章节进行阅读。
@Scope注解用于定义Bean的作用域,缺省情况下,Bean是单例的(singleton),当然你也可以通过修改Bean的作用域来达到满足你的需求。
使用@Scope定义Bean作用域:
@Component @Scope("prototype") public class Test { @Resource public TestA testA; }
最后关于@RequestMapping、@Transactional和@ResponseBody这3个注解,笔者并不打算在本章进行讲解。因为这3个注解涉及到后续章节的相关内容,所以敬请关注笔者的后续系列博文。
本章内容到此结束,由于时间仓库,本文或许有很多不尽人意的地方,希望各位能够理解和体谅。关于下一章的内容,笔者打算讲解JDK动态代理和AOP相关的内容。