首先,在 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 是一类特殊的 Beanpublic 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。通过加上前缀
&
从工厂中取出对应的 BeanObject cherryFactoryBean = context.getBean("&cherryFactoryBean");
-
@Component
:component-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
)singleton
:单实例(默认),在Spring IOC容器启动的时候会调用方法创建对象然后纳入到IOC容器中,以后每次获取都是直接从IOC容器中获取(map.get()
);prototype
:多实例,IOC容器启动的时候并不会去创建对象,而是在每次获取的时候才会去调用方法创建对象;request
:一个请求对应一个实例;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
方法,包含两个入参:ConditionContext
:上下文信息;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
自动装配,先byType
再byName
,如果不能唯一自动装配,则需要@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 上下文中(通过AopContext
的ThreadLocal
实现)@Override public void saveUserTest(User user) { UserService userService = (UserService) AopContext.currentProxy(); userService.saveUser(user); }
总之是有种为了舍近求远又额外兜了一大圈的感觉,个人认为写事务就不要代入编程优雅方面的考虑了,没必要在方法单一职责上那么较真。