Spring Boot 2+Thymeleaf企业应用实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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时,大概知道其表示的意思即可。