返回
Featured image of post Spring - 常用注释

Spring - 常用注释

@Annotation.

首先,在 Spring 4.X 之后(不用 Spring Boot 的话)使用注释需要添加 aop 依赖。虽然不需要这么做了,还是有助于了解 Spring Boot 到底为我们做了什么。

<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>{springframework.version}</version>
</dependency>

而且需要在 XML 中添加约束并在 context 中配置扫描范围。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:context="http://www.springframework.org/schema/context"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

	<context:annotation-config/>

</beans>

配置扫描范围

<!--指定注解扫描包-->
<context:component-scan base-package="com.yourpackage"/>

接下来按类别整理一些最常用的注释。

Bean 的扫描


@ComponentScan

  • @ComponentScan:通过注释方式配置扫描范围,将其下的 @Component 组件(包括@Controller@Service@Repository)纳入 IOC 容器. 只能作用于配置类,且 Spring Boot 的入口类不能被纳入到扫描范围中.

    Spring Boot 默认的扫描范围是启动类所在包开始,当前包及子包下的所有文件

    @Configuration
    @ComponentScan("cc.mrbird.demo")
    public class WebConfig {
    
    }
    

    可以通过 excludeFilters 来排除一些组件的扫描,通过 @Filter 注释完成

    @Configuration
    @ComponentScan(value = "cc.mrbird.demo",
            excludeFilters = {
              			// 将注解为 Controller 和 Repository 的类排除
                    @Filter(type = FilterType.ANNOTATION,
                            classes = {Controller.class, Repository.class}),
              			// 排除所有 User 类(及子类、实现类)
                    @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = User.class)
            })
    public class WebConfig {
    
    }
    

    如上所示,可以跟据注释或直接指定排除相应类型(包括其子类、实现类)

    includeFilters的作用和excludeFilters相反,其指定的是哪些组件需要被扫描:

    @Configuration
    @ComponentScan(value = "cc.mrbird.demo",
            includeFilters = {
              			// 仅纳入注释为 Service 的类
                    @Filter(type = FilterType.ANNOTATION, classes = Service.class)
            }, useDefaultFilters = false)
    public class WebConfig {
    
    }
    

    通过实现 org.springframework.core.type.filter.TypeFilter接口可以自定义扫描策略,通过实现 match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) 方法,返回 true 说明匹配成功。

Bean 的注册


@Bean, @Component

  • @Bean:通过注解向 IOC 容器注册默认为方法名的 Bean,也可以通过 @Bean("{name}") 来重新命名

    @Configuration
    public class WebConfig {
        @Bean()
        public User user() {
            return new User("mrbirdy", 18);
        }
    }
    

    实现了 FactoryBean<T> 接口的 Bean 是一类特殊的 Bean

    public class CherryFactoryBean implements FactoryBean<Cherry> {
        @Override
        public Cherry getObject() {
            return new Cherry();
        }
    
        @Override
        public Class<?> getObjectType() {
            return Cherry.class;
        }
    
        @Override
        public boolean isSingleton() {
            return false;
        }
    }
    

    如果 isSingleton()false ,则每次会调用 getObject() 从中获取 Bean。

    通过加上前缀 & 从工厂中取出对应的 Bean

    Object cherryFactoryBean = context.getBean("&cherryFactoryBean");
    
  • @Componentcomponent-scan 指定的扫描路径下所有被@Controller@Service@Repository@Component注解标注的类都会被纳入IOC容器中

    @Component("user")
    // 相当于配置文件中 <bean id="user" class="当前注解的类"/>
    public class User {
       public String name = ...
    }
    

    说明该类被Spring管理。Component 类的有参构造方法会被默认用作依赖注入,所以相比在成员变量上加 @Autowire 来注入依赖,更合适的方法是通过构造方法注入。

    衍生注解:按照MVC三层架构分层

    • @Repository:用于DAO层,数据库操作
    • @Service:用于Service层,复杂逻辑
    • @Controller:用于Controller层,接收用户请求并调用Service层返回数据

    连同@Component,四个注解功能一样,都代表将某个类注册到Spring中

Bean 的加载


@Scope, @Lazy, @Conditional

  • @Scope:改变组件的作用域(默认 singleton

    1. singleton:单实例(默认),在Spring IOC容器启动的时候会调用方法创建对象然后纳入到IOC容器中,以后每次获取都是直接从IOC容器中获取(map.get());
    2. prototype:多实例,IOC容器启动的时候并不会去创建对象,而是在每次获取的时候才会去调用方法创建对象;
    3. request:一个请求对应一个实例;
    4. session:同一个session对应一个实例。
  • @Lazy:懒加载(针对 singleton

    懒加载的单例不会马上调用方法创建对象并注册,只有当第一次被使用时才会调用方法创建对象并加入容器中。

  • @Conditional:条件加载,类似于前面 @ComponentScan 中的 Filter

    使用@Conditional注解我们可以指定组件注册的条件,即满足特定条件才将组件纳入到 IOC 容器中。

    在使用该注解之前,我们需要创建一个类,实现Condition接口:

    public class MyCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return false;
        }
    }
    

    该接口包含一个matches方法,包含两个入参:

    1. ConditionContext:上下文信息;
    2. AnnotatedTypeMetadata:注解信息。

    简单完善一下这个实现类:

    public class MyCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            String osName = context.getEnvironment().getProperty("os.name");
            return osName != null && osName.contains("Windows");
        }
    }
    

    接着将这个条件添加到User Bean注册的地方:

    @Bean
    @Conditional(MyCondition.class)
    public User user() {
        return new User("mrbird", 18);
    }
    

    在Windows环境下,User这个组件将被成功注册,如果是别的操作系统,这个组件将不会被注册到IOC容器中。

属性注入


@Value, @ConfigurationProperties, @PropertySource

  • @Value: Property注入

    可以直接用在成员变量上,也可以用在Setter上

    需要注意的是 @value这种方式是不被推荐的,Spring 比较建议的是下面几种读取配置信息的方式。

  • @ConfigurationProperties: Properties 读取并与 bean 绑定

    LibraryProperties 类上加了 @Component 注解,我们可以像使用普通 bean 一样将其注入到类中使用。

    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.stereotype.Component;
    
    import java.util.List;
    
    @Component
    @ConfigurationProperties(prefix = "library")
    class LibraryProperties {
        private String location;
        private List<Book> books;
    
        static class Book {
            String name;
            String description;
        }
    }
    

    相应的配置文件内容

    library:
      location: 湖北武汉加油中国加油
      books:
        - name: 天才基本法
          description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。
        - name: 时间的秩序
          description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。
        - name: 了不起的我
          description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻?
    

    然后就可以通过 private final LibraryProperties library 注入 Property 对象了。

    题外话:InitializingBean 接口下的 afterPropertiesSet() 方法可以作为一个 Property 注入后的 AOP 使用,如下所示:

    @SpringBootApplication
    public class ReadConfigPropertiesApplication implements InitializingBean {
    
        private final LibraryProperties library;
    
        public ReadConfigPropertiesApplication(LibraryProperties library) {
            this.library = library;
        }
    
        public static void main(String[] args) {
            SpringApplication.run(ReadConfigPropertiesApplication.class, args);
        }
    
        @Override
        public void afterPropertiesSet() {
            System.out.println(library.getLocation());
            System.out.println(library.getBooks());    }
    }
    

    如果 Property类上不加 Component ,就需要在 SpringBootApplication 上加 @EnableConfigurationProperties 来注册 Bean ,如下所示:

    @SpringBootApplication
    @EnableConfigurationProperties(ProfileProperties.class)
    public class ReadConfigPropertiesApplication implements InitializingBean {
     private final ProfileProperties profileProperties;
    
     public ReadConfigPropertiesApplication(ProfileProperties profileProperties) {
         this.profileProperties = profileProperties;
     }
    
     public static void main(String[] args) {
         SpringApplication.run(ReadConfigPropertiesApplication.class, args);
     }
    
     @Override
     public void afterPropertiesSet() {
         System.out.println(profileProperties.toString());
     }
    }
    
  • @PropertySource: 有单独文件的 properties 可以通过 @PropertySource 来读取

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.stereotype.Component;
    
    @Component
    @PropertySource("classpath:website.properties")
    class WebSite {
        @Value("${url}")
        private String url;
    }
    

校验注解

这里可以参考 Spring Boot 指南


JSR 提供的校验注解:

  • @Null 被注释的元素必须为 null
  • @NotNull 被注释的元素必须不为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse 被注释的元素必须为 false
  • @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
  • @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
  • @Past 被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期
  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
public class Person {

    @NotNull(message = "classId 不能为空")
    private String classId;

    @Size(max = 33)
    @NotNull(message = "name 不能为空")
    private String name;

    @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围")
    @NotNull(message = "sex 不能为空")
    private String sex;

    @Email(message = "email 格式不正确")
    @NotNull(message = "email 不能为空")
    private String email;

}

Hibernate Validator 提供的校验注解

  • @NotBlank(message =) 验证字符串非 null,且长度必须大于 0
  • @Email 被注释的元素必须是电子邮箱地址
  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
  • @NotEmpty 被注释的字符串的必须非空
  • @Range(min=,max=,message=) 被注释的元素必须在合适的范围内

自动装配


@Autowired, @Resource

  • @Autowired

自动装配,先byTypebyName,如果不能唯一自动装配,则需要@Qualifier(value="xxx").

在成员变量上实现:

public class User {
   @Autowired
   private Cat cat;
   @Autowired
   private Dog dog;
   private String str;

   public Cat getCat() {
       return cat;
  }
   public Dog getDog() {
       return dog;
  }
   public String getStr() {
       return str;
  }
}

然后在XML中配置Bean

<context:annotation-config/>

<bean id="dog" class="com.kuang.pojo.Dog"/>
<bean id="cat" class="com.kuang.pojo.Cat"/>
<bean id="user" class="com.kuang.pojo.User"/>
  • @Qualifer():如果bean名字不为类的默认名字,则要加@Qualifer

    @Autowired
    @Qualifier(value = "cat2")
    private Cat cat;
    @Autowired
    @Qualifier(value = "dog2")
    private Dog dog;
    
  
</br>
  
- #### `@Resource`

  自动装配,先`byName`再`byType`,如果`name`属性指定,则只会按照名称进行装配.

  默认按照名称进行装配,名称可以通过name属性进行指定。如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配

  ```java
  public class User {
     //如果允许对象为null,设置required = false,默认为true
     @Resource(name = "cat2")
     private Cat cat;
     @Resource
     private Dog dog;
     private String str;
  }

事务


@Transactional

  • @Transactional

    在 Service 的实现类中使用,将方法标注为 SQL 事务.

    首先需要在入口类上加入 @EnableTransactionManagement 注解以开启事务:

    @EnableTransactionManagement
    @SpringBootApplication
    public class TransactionApplication {
    
        public static void main(String[] args) throws Exception {
            SpringApplication.run(TransactionApplication.class, args);
        }
    }
    

    然后再 @Service 中标注事务:

    @Service
    public class UserServiceImpl implements UserService {
    
        private final UserMapper userMapper;
    
        public UserServiceImpl(UserMapper userMapper) {
            this.userMapper = userMapper;
        }
    
        @Transactional
        @Override
        public void saveUser(User user) {
            userMapper.save(user);
            // 测试事务回滚
            if (!StringUtils.hasText(user.getUsername())) {
                throw new RuntimeException("username不能为空");
            }
        }
    }
    

    如果生效,当用户名为空(这里用的是 org.springframework.util 包下的 hasText() 方法,要求字符串不为 null 、长度大于0、不全为空),则会捕获到异常而进行回滚。

    @Transactional 同样利用的是 Spring 的 AOP 机制, 这里有两个坑.

    注意点一

    如果抛出的异常不是 RuntimeException 或者 Error ,也不是 @Transactional 注解指定的回滚异常类型则不会进行事务回滚

    所以在自定义需要回滚的异常时,要么继承 RuntimeException ,要么直接在注释上标出来:

    @Transactional(rollbackFor = Exception.class)
        @Override
        public void saveUser(User user) throws Exception {
            userMapper.save(user);
            // 测试事务回滚
            if (!StringUtils.hasText(user.getUsername())) {
                throw new Exception("username不能为空");
            }
        }
    

    注意点二

    如果我们在相同 Service 下的非事务方法中,对事务方法进行调用,事务同样不会生效。如下:

    @Service
    public class UserServiceImpl implements UserService {
    
        private final UserMapper userMapper;
    
        public UserServiceImpl(UserMapper userMapper) {
            this.userMapper = userMapper;
        }
    
        @Override
        public void saveUserTest(User user) {
            this.saveUser(user);
        }
    
        @Transactional
        @Override
        public void saveUser(User user) {
            userMapper.save(user);
            // 测试事务回滚
            if (!StringUtils.hasText(user.getUsername())) {
                throw new ParamInvalidException("username不能为空");
            }
        }
    }
    

    因为 Spring 事务控制通过 AOP 代理实现,通过代理目标对象来增强目标方法,而如果用 this 调用方法,this 绕过了代理类(实际上是代理类绕过原类,this 无视了代理类),直接用了类本身,从而没有触发事务。

    要让代理类重新生效有两种方法

    1、 从 IOC 中获取 Bean 后再调用:

    @Override
    public void saveUserTest(User user) {
        UserService userService = context.getBean(UserService.class);
        userService.saveUser(user);
    }
    

    2、 直接从 AOP 上下文取代理对象进行调用(需要引入 AOP Starter 依赖),且需要再在 SpringBoot 入口类中通过注解@EnableAspectJAutoProxy(exposeProxy = true)将当前代理对象暴露到 AOP 上下文中(通过 AopContextThreadLocal 实现)

    @Override
    public void saveUserTest(User user) {
        UserService userService = (UserService) AopContext.currentProxy();
        userService.saveUser(user);
    }
    

    总之是有种为了舍近求远又额外兜了一大圈的感觉,个人认为写事务就不要代入编程优雅方面的考虑了,没必要在方法单一职责上那么较真。

comments powered by Disqus