4.1 Spring的常用注解
Spring框架主要包括IoC与AOP,这两大功能都可以使用注解进行配置,本节就以这两大功能为基础,讲解Spring的基本注解。新建一个Maven项目,在pom.xml中加入“spring-boot-starter-web”依赖,这个项目将用于测试。
注意:本节将会直接使用Spring Boot的环境来测试Spring的注解,省去了使用编码的方式来创建Spring容器,这些工作将由Spring Boot完成。
4.1.1 bean定义
使用XML的方式配置Spring,需要告诉Spring容器XML的位置,然后在XML中使用<bean>元素来定义bean。
查找XML文档与配置<bean>元素这两个工作同样可以使用注解来完成。使用@Component、@Service或者@Configuration注解来修饰一个Java类,这些Java类会被Spring自动检测并注册到容器中,在类里面使用@Bean注解修饰的方法,会被作为一个bean存放到Spring容器中。代码清单4-1即使用了@Configuration和@Bean注解。
代码清单4-1:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\bean\MyBean.java codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\bean\BeanApp.java
public class MyBean { } @Configuration public class MyConfig { @Bean public MyBean getMyBean() { return new MyBean(); } @Bean("bean2") public MyBean myBean2() { return new MyBean(); } }
在MyConfig类中使用了两个@Bean注解。在使用@Bean注解时,如果没有传入bean的名称,默认会使用方法名作为bean的名称。接下来编写Spring Boot的启动类和控制器,并在容器中读取这两个bean,请见代码清单4-2。
代码清单4-2:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\bean\BeanApp.java
@SpringBootApplication @RestController public class BeanApp { public static void main(String[] args) { SpringApplication.run(BeanApp.class, args); } @Autowired ApplicationContext ctx; /** * 输出容器中全部的bean * @return */ @GetMapping("/print") public String printBeans() { // 根据类型获取bean的名称 String[] names = ctx.getBeanNamesForType(MyBean.class); for(String name : names) { System.out.println(name); } return ""; } }
代码清单4-1将启动类和控制器放到一起,在控制器中会注入ApplicationContext的实例,printBean方法会通过该实例,获取MyBean类型的bean名称并输出到控制台。运行代码清单4-1,在浏览器中访问:http://localhost:8080/print,控制台输出如下:
getMyBean bean2
根据结果可知,Spring容器中存在两个MyBean类型的bean。本例在修饰MyConfig时使用了@Configuration注解,这个注解实际上已经具有了@Component的功能,一般情况下我们直接使用@Configuration注解即可。
我们可以换一个角度来理解@Configuration与@Bean这两个注解,使用@Configuration注解修饰的类,我们可以把它看作一个Spring的XML配置文件,而在类里面使用@Bean修饰的方法,则可以被看作XML文件中的一个<bean>元素。还有一个细节需要注意,使用@Configuration注解修饰的类,本身也会作为一个bean被注册。
4.1.2 依赖注入
想要在一个bean中使用另一个bean,则可以直接在使用者一端,将被调用者的实例注入,例如下面的XML,在beanA中注入beanB的实例:
<bean id=”beanA” class=”xxx”> <property name=”beanB” ref=”beanB”/> </bean>
使用注解同样可以实现实例的注入,最常用的是@Resource及@Autowired这两个注解。其中@Resource注解是JSR-250规范定义的注解,该注解默认会根据名称进行注入,而@Autowired注解默认会根据类型进行注入。代码清单4-3使用了这两个注解实现依赖注入。
代码清单4-3:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\inject\InjectBean.java codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\inject\InjectConfig.java
public class InjectBean { private String id; // 省略setter和getter方法 } @Configuration public class InjectConfig { @Bean public InjectBean myBean1() { return new InjectBean("1"); } @Bean public InjectBean myBean2() { return new InjectBean("2"); } }
在代码清单4-3中定义了两个名称不同的InjectBean类型的bean,这两个bean都会设置不同的id属性,它们将会被注入到控制器中,请见代码清单4-4。
代码清单4-4:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\inject\InjectApp.java
@SpringBootApplication @RestController public class InjectApp { public static void main(String[] args) { SpringApplication.run(InjectApp.class, args); } // 使用@Resource注入 @Resource(name = "myBean1") InjectBean myBean1; // 使用@Autowired注入 @Autowired InjectBean myBean2; @GetMapping("/inject") public String inject() { System.out.println(myBean1.getId()); System.out.println(myBean2.getId()); return ""; } }
将启动类和控制器写到同一个类中,往这个类中注入两个定义好的bean,分别使用@Resource与@Autowired注解实现注入。运行启动类,在浏览器中输入:http://localhost:8080/inject,即可以在控制台中看到结果。
可能有细心的朋友会发现,@Autowired是根据类型注入的,而在上面的例子中,Spring容器里面有两个InjectBean类型的bean,为什么@Autowired会拿到相应的bean呢?实际上,@Autowired默认会根据类型来获取bean,如果存在多个bean,则会根据属性名来查找,而前面例子中的属性名为myBean2,因此最终会找到相应的bean。如果我们在这个例子的基础上,将myBean2修改为myBean3,则在启动时,会得到以下错误信息:
Field myBean3 in org.crazyit.boot.c4.inject.InjectApp required a single bean, but 2 were found: - myBean1: defined by method 'myBean1' in class path resource [org/crazyit/boot/c4/inject/InjectConfig.class] - myBean2: defined by method 'myBean2' in class path resource [org/crazyit/boot/c4/inject/InjectConfig.class]
以上错误信息大致的意思是,InjectApp的myBean3属性在容器中找到两个bean,而Spring并不知道应该往控制器注入哪个,所以就报出以上的错误信息。@Resource注解也会有同样的问题,如果使用@Resource时根据名称无法查找到bean,则会根据类型进行注入,如果找到多个相同类型的bean,则会报出以下的异常信息:
Error creating bean with name 'injectApp': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.crazyit.boot.c4.inject.InjectBean' available: expected single matching bean but found 2: myBean1, myBean2
以上的示例使用了@Resource与@Autowired进行依赖注入,注入方式为设值注入。除了这种设值注入外,还可以使用构造注入,向控制器的构造器中注入bean,如以下代码片断:
InjectBean myBean; @Autowired public InjectApp(InjectBean myBean2) { this.myBean = myBean2; }
构造注入效果与设值注入类似,在此不再赘述。
注意:@Resource注解不能修饰构造器,需要使用@Autowired注解来实现构造注入。
4.1.3 使用Primary注解
根据上一节可知,不管是@Resource还是@Autowired注解,如果根据类型来注入,而且容器中存在多个同类型的bean时,就会抛出异常,因为此时Spring并不知道将哪个bean注入,针对这个问题,可以使用@Primary注解,请见代码清单4-5。
代码清单4-5:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\primary\PrimaryConfig.java
@Configuration public class PrimaryConfig { @Bean @Primary public PrimaryBean bean1() { return new PrimaryBean("1"); } @Bean public PrimaryBean bean2() { return new PrimaryBean("2"); } }
在容器中配置了两个bean,其中bean1使用了@Primary注解,也就是在Spring容器中会存在两个类型均为PrimaryBean的bean,如果在控制器或者其他组件中,根据类型注入PrimaryBean,则会注入bean1,请见代码清单4-6。
代码清单4-6:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\primary\PrimaryApp.java
@Autowired PrimaryBean bean; @GetMapping("/primary") public String primary() { System.out.println(bean.getId()); return ""; }
在代码清单4-6中使用了@Autowired注解进行注入,由于bean1使用了@Primary注解,因此只会注入bean1的实例。如果代码清单4-5没有使用@Primary注解,则会在启动时抛出错误信息,原因在前面小节讲过:根据类型找到两个bean, Spring并不知道应该注入哪个。
运行PrimaryApp.java,使用浏览器访问:http://localhost:8080/primary,则可以看到控制台输出的结果。
4.1.4 Scope注解
在配置bean时,可以指定bean的作用域(scope),一般的bean可以被配置为单态(singleton)或者非单态(prototype)的。配置为singleton的话,Spring的bean工厂都会只返回同一个bean实例,而配置为prototype,则每次都会创建一个新的实例。代码清单4-7定义了两个有不同作用域的bean。
代码清单4-7:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\scope\ScopeConfig.java
@Configuration public class ScopeConfig { @Bean @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) public ScopeBean bean1() { return new ScopeBean(); } @Bean @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) public ScopeBean bean2() { return new ScopeBean(); } }
在ScopeConfig类里面定义了两个bean,分别是prototype与singleton。在控制器中使用ApplicationContext来获取这两个bean,并且将它们输出到控制台,请见代码清单4-8。
代码清单4-8:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\scope\ScopeApp.java
@Autowired ApplicationContext ctx; @GetMapping("/hello") public String hello() { System.out.println("非单态的bean:" + ctx.getBean("bean1") + ", 单态的bean:" + ctx.getBean("bean2")); return "hello"; }
启动服务器,在浏览器中访问:http://localhost:8080/hello,刷新多次查看结果,可以看到非单态的bean每次都返回一个新的实例。
单态与非单态的配置较为简单,但是有一点需要注意,如果在一个单态的bean里面注入一个非单态的bean,则这个单态的bean所维护的非单态bean实例,将不会被刷新。举个简单的例子,Spring MVC的控制器是单态的,如果往控制器里面注入一个非单态的bean,例如以下代码片断:
@Controller public class TestController { // 注入一个非单态的bean @Autowired private ScopeBean bean1; @GetMapping("/hello2") public String hello() { System.out.println(bean1); return "hello"; } }
在以上代码片断中,多次调用hello方法打印的ScopeBean都会是同一个实例,因为控制器在初始化时,就已经被注入了一个bean,而且一直维护着同一个实例。Spring的官方文档不建议使用这种方式进行依赖注入,建议使用Spring的“方法注入”来解决bean实例无法刷新的问题。
@Scope注解的value属性,除了可以取值为“singleton”和“prototype”外,还可以设置为“request”“session”“application”和“websocket”,这些值都需要在Web环境中使用,在此不展开讨论。
4.1.5 方法注入
如前面小节所讲的,在一个单态的bean中注入一个非单态的bean,会导致非单态bean的实例无法刷新的问题,解决这个问题有多种方法,以下提供两种简单的解决方法:第一,在需要注入的一方(单态的bean),直接使用ApplicationContext,每次调用非单态的bean,都由Spring容器返回;第二,使用Spring的方法注入。第一种方法,请见代码清单4-9。
代码清单4-9:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\mi\MiController.java
@RestController public class MiController { @Autowired private ApplicationContext ctx; private MiBeanA getMiBean() { return (MiBeanA)ctx.getBean("bean1"); } }
代码中的MiController是一个单态的bean,在它的里面需要使用一个非单态的bean,类型为MiBeanA。在上面的代码清单中,并没有直接注入MiBeanA的实例,而是注入了ApplicationContext,当需要使用MiBeanA时,再通过ApplicationContext来返回。这种方式会导致我们的代码与ApplicationContext耦合,并不推荐使用这种方式。
第二种方法,可使用Lookup Method Injection(方法注入)来解决。在使用实例的一端,添加一个获取非单态bean的抽象方法,请见代码清单4-10。
代码清单4-10:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\mi\MiController.java
@RestController public abstract class MiController { @GetMapping("/mi") public String mi() { System.out.println(createMiBean()); return ""; } @Lookup("bean1") public abstract MiBeanA createMiBean(); }
在该代码清单中使用了@Lookup注解来修饰一个抽象方法,这个方法会返回MiBeanA的实例。运行启动类(本例为org.crazyit.boot.c4.mi.MiApp),在浏览器中访问:http://localhost:8080/mi,多次刷新,可以看到MiController每次都会得到不同的MiBeanA实例。实际上,Spring容器会使用CGLIB库,帮我们动态生成一个MiController的子类,并且会实现createMiBean方法。在实际应用中,一个单态的bean使用一个非单态的bean,这种情况并不多见,如果遇到可以考虑以上介绍的两种方法。
4.1.6 AOP注解
AOP是Spring的重要功能之一,很多时候,AOP会被应用在数据库事务、业务日志记录、权限控制等功能中。在Spring Boot项目中使用AspectJ的注解来实现AOP功能,需要为pom.xml加入以下依赖:
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
新建业务类与代理类,请见代码清单4-11。
代码清单4-11:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\aop\SaleServiceImpl.java codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\aop\ProxyService.java
@Component public class SaleServiceImpl { public void saleService() { System.out.println("要代理的销售业务方法"); } } @Aspect @Component public class ProxyService { @Before("execution(* org.crazyit.boot.c4.aop.*ServiceImpl.*(..))") public void before() { System.out.println("业务方法调用前执行"); } @After("execution(* org.crazyit.boot.c4.aop.*ServiceImpl.*(..))") public void after() { System.out.println("业务方法调用后执行"); } }
在代码清单4-11中,要注意代理类,其使用@Aspect注解进行修饰,使用@Before与@After来设置通知(Advice),并为其配置了相应的切点(Pointcut)。代码清单4-11,简单来说,就是在业务方法调用前执行before方法,在业务方法调用后执行after方法。新建Spring Boot的启动类与控制器,本例的启动类与控制器为同一个类,请见代码清单4-12。
代码清单4-12:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\aop\ProxyApp.java
@SpringBootApplication
@RestController
public class ProxyApp {
public static void main(String[] args) {
new SpringApplicationBuilder(ProxyApp.class).properties(
"spring.aop.proxy-target-class=true").run(args);
}
@Autowired
SaleServiceImpl saleService;
@GetMapping("/sale")
public String saleService() {
saleService.saleService();
System.out.println("SaleServiceImpl的class: " + saleService.getClass());
return "";
}
}
执行ProxyApp类的main方法后,打开浏览器访问以下地址:http://localhost:8080/sale,控制台输出如下:
业务方法调用前执行 要代理的销售业务方法 业务方法调用后执行 SaleServiceImpl的class: class org.crazyit.boot.c4.aop. SaleServiceImpl$$EnhancerBySpringCGLIB$$a6f12b8f
根据结果可知,我们的业务方法已经被代理,因此在方法调用前和调用后,都会执行通知的方法,输出的代理类为经过CGLIB处理的类。
在Spring Boot 2.0中,默认情况下,不管是代理接口还是类,都使用CGLIB代理。我们可以将spring.aop.proxy-target-class配置为false,这样在代理接口时,会使用JDK动态代理。
在启动类中将spring.aop.proxy-target-class设置为false后,在浏览器中访问:http://localhost:8080/mer,则可以在控制台中看到,生成的代理类类似于com.sun.proxy.$Proxy61。Spring AOP功能强大,这里所述仅是其冰山一角,限于篇幅,更深入的AOP功能不再赘述。
4.1.7 ComponentScan注解
ComponentScan注解主要用于检测使用@Component修饰的组件,并把它们注册到Spring容器中。除了直接使用@Component修饰的组件外,还有间接使用@Component的组件(如@Service、@Repository、@Controller,或自定义注解),都会被检测到。在使用Spring Boot时,我们一般很少直接接触ComponentScan注解,但实际上每个应用都使用它,它只是被放到@SpringBootApplication里面了。在使用ComponentScan注解时,要注意过滤器,例如,代码清单4-13即使用了ComponentScan的过滤器。
代码清单4-13:codes\04\4.1\spring-ants\src\main\java\org\crazyit\boot\c4\scan\ScanApp.java
@SpringBootConfiguration @EnableAutoConfiguration @RestController @ComponentScan(basePackages = "org.crazyit.boot.c4.scan", excludeFilters = { @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = MyComponent.class) }) public class ScanApp { public static void main(String[] args) { SpringApplication.run(ScanApp.class, args); } @Autowired private ApplicationContext ctx; /** * 调用将会报错:NoSuchBeanDefinitionException:No bean named'myComponent'available * @return */ @GetMapping("/scan") public String scan() { System.out.println(ctx.getBean("myComponent")); return ""; } }
ScanApp类使用了@ComponentScan修饰,设置了excludeFilters,因此在扫描组件时,将会过滤掉MyComponent类,从而容器在启动后,再向Spring容器请求“myComponent”的bean时,将会抛出异常。启动服务器(ScanApp),在浏览器中访问:http://localhost:8080/scan,可以看到控制台输出的异常信息。
对于ComponentScan注解的过滤器,可以指定多个类型,甚至可以使用自定义的过滤器,但由于在实际环境中应用不多,因此不再赘述,读者在见到ComponentScan时,大概知道其表示的意思即可。