IoC IoC
全称 Inversion of Control
,直译为控制反转。又被称为依赖注入(Dependency Injection
或 DI
)
传统应用模式下,组件的创建(实例化)、使用和销毁都由程序员维护,非常复杂。IoC 将组件的创建交给 IoC 容器,由容器管理组件的创建、销毁。
IoC 可以使用 XML 进行配置,但更推荐使用注解进行配置,因此只介绍后面一种。
初始代码 首先三个组件:User
UserService
和 MailService
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class User { public long id; public String email; public String password; public String name; public User (long id, String email, String password, String name) { this .id = id; this .email = email; this .password = password; this .name = name; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class UserService { private MailService mailService; private List<User> users = new ArrayList<>(Arrays.asList( new User(1 , "bob@example.com" , "password" , "Bob" ), new User(2 , "alice@example.com" , "password" , "Alice" ), new User(3 , "tom@example.com" , "password" , "Tom" ) )); public void setMailService (MailService mailService) { this .mailService = mailService; } public List<User> getUsers () { return users; } public User register (String email, String password, String name) { long maxUserId = 0 ; for (User user: users) { if (user.getEmail().equalsIgnoreCase(email)) { throw new RuntimeException("email exists" ); } maxUserId = Math.max(maxUserId, user.getId()); } User user = new User(maxUserId+1 , email, password, name); users.add(user); mailService.sendRegistrationMail(user); return user; } public User login (String email, String password) { for (User user: users) { if (user.getEmail().equalsIgnoreCase(email) && user.getPassword().equals(password)) { mailService.sendLoginMail(user); return user; } } throw new RuntimeException("login failed." ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class MailService { private ZoneId zoneId = ZoneId.systemDefault(); public void setZoneId (ZoneId zoneId) { this .zoneId = zoneId; } public String getTime () { return ZonedDateTime .now(this .zoneId) .format(DateTimeFormatter.ISO_ZONED_DATE_TIME); } public void sendRegistrationMail (User user) { System.err.printf("Welcome, %s!\n" , user.getName()); } public void sendLoginMail (User user) { System.err.printf("Hi, %s! You're logged in at %s\n" , user.getName(), getTime()); } }
装配 Bean 基础用法 第零步,Maven 安装依赖:
1 2 3 4 5 6 7 8 9 10 11 <properties > <spring.version > 5.2.3.RELEASE</spring.version > </properties > <dependencies > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > ${spring.version}</version > </dependency > </dependencies >
第一步:对上面三个类都标记上 @Component
:
1 2 3 4 @Component public class MailService {}
第二步,注意到 UserService
依赖 MailService
的一个实例,所以我们要在 UserService
定义 mailService
的地方标记上 @Autowired
,把两个东西连起来。
Idea 提示 Field injection is not recommended
廖雪峰提示 别去管 Idea 的警告。改成 method 注入,要多写好多行代码
1 2 3 4 5 6 @Component public class UserService { @Autowired private MailService mailService; }
第三步,在 main 函数中生成 ApplicationContext
,这个就是 IoC 容器实例,我们可以使用 getBean
获得上面绑定的任何一个组件。
此外,还需要加上 @ComponentScan
和 @Configuration
,表示:这个类搜索当前类所在的包;这个类可以被作为 AnnotationConfigApplicationContext
函数的配置参数。
注意这个类不能放在根目录(即 /src/main/java/
)下,否则会扫描所有的包;放在根目录下的任何一层均可。
1 2 3 4 5 6 7 8 9 10 @ComponentScan @Configuration public class Main { public static void main (String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(Main.class ) ; UserService userService = context.getBean(UserService.class ) ; User user = userService.login("bob@example.com" , "password" ); System.out.println(user.getName()); } }
修改 Bean 的作用域 对于Spring容器来说,当我们把一个Bean标记为@Component后,它就会自动为我们创建一个单例(Singleton),即容器初始化时创建Bean,容器关闭前销毁Bean。在容器运行期间,我们调用getBean(Class)获取到的Bean总是同一个实例。
还有一种Bean,我们每次调用getBean(Class),容器都返回一个新的实例,这种Bean称为Prototype(原型),它的生命周期显然和Singleton不同。声明一个Prototype的Bean时,需要添加一个额外的@Scope注解:
1 2 3 4 5 6 @Component @Scope (ConfigurableBeanFactory.SCOPE_PROTOTYPE)public class MailSession { }
注入 List @Autowired
不仅可以注入单个实例,还可以注入多个实例组成的 List。
定义验证接口和三个实现了这个接口的验证类,并对类标记 Component
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public interface Validator { void validate (String email, String password, String name) ; } @Component public class EmailValidator implements Validator { public void validate (String email, String password, String name) { if (!email.matches("^[a-z0-9]+\\@[a-z0-9]+\\.[a-z]{2,10}$" )) { throw new IllegalArgumentException("invalid email: " + email); } } } @Component public class PasswordValidator implements Validator { public void validate (String email, String password, String name) { if (!password.matches("^.{6,20}$" )) { throw new IllegalArgumentException("invalid password" ); } } } @Component public class NameValidator implements Validator { public void validate (String email, String password, String name) { if (name == null || name.isBlank() || name.length() > 20 ) { throw new IllegalArgumentException("invalid name: " + name); } } }
最后定义 Validators
作为入口进行验证:
1 2 3 4 5 6 7 8 9 10 11 12 @Component public class Validators { @Autowired List<Validator> validators; public void validate (String email, String password, String name) { for (var validator : this .validators) { validator.validate(email, password, name); } } }
可选注入 1 2 3 4 5 6 @Component public class MailService { @Autowired (required = false ) ZoneId zoneId = ZoneId.systemDefault(); }
创建第三方 Bean 如果一个 Bean 不在我们自己的 package 管理之内,例如 ZoneId
,如何创建它?
答案是我们自己在 @Configuration
类中编写一个 Java 方法创建并返回它,注意给方法标记一个 @Bean
注解:
1 2 3 4 5 6 7 8 9 @Configuration @ComponentScan public class AppConfig { @Bean ZoneId createZoneId() { return ZoneId .of ("Z" ); } }
Spring 对标记为 @Bean
的方法只调用一次,因此返回的 Bean 仍然是单例。
注入资源文件 在 Java 程序中,我们经常会读取配置文件、资源文件等。使用 Spring 容器时,我们也可以把“文件”注入进来,方便程序读取。
例如,AppService
需要读取 logo.txt
这个文件,通常情况下,我们需要写很多繁琐的代码,主要是为了定位文件,打开 InputStream
。
Spring 提供了一个 org.springframework.core.io.Resource
(注意不是 javax.annotation.Resource
),它可以使用 @Value
注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component public class AppService { @Value ("classpath:/logo.txt" ) private Resource resource; private String logo; @PostConstruct public void init () throws IOException { try (var reader = new BufferedReader( new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) { this .logo = reader.lines().collect(Collectors.joining("\n" )); } } }
上述工程结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 spring-ioc-resource ├── pom .xml └── src └── main ├── java │ └── com │ └── itranswarp │ └── learnjava │ ├── AppConfig .java │ └── AppService .java └── resources └── logo .txt
也可以直接指定文件的路径,例如:@Value("file:/path/to/logo.txt")
。但使用 classpath
是最简单的方式。
使用 Maven
的标准目录结构,所有资源文件放入 src/main/resources
即可。
注入配置文件 注入配置 - 廖雪峰的官方网站
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration @ComponentScan @PropertySource ("app.properties" ) public class AppConfig { @Value ("${app.zone:Z}" ) String zoneId; @Bean ZoneId createZoneId () { return ZoneId.of(zoneId); } }
上述代码读取 classpath 下的 app.properties
文件,然后找到 zone=X
,令 zoneId
为 X
。如果没有找到 zone
就设置为 Z
。
条件注入 Spring 容器可以条件注入。
@Profile("test")
表示在 "test"
时注入
@Profile("!test")
表示在不是 "test"
时注入
Spring 运行时可以指定多个 Profile
1 -Dspring.profiles.active =test,master
也可以判断编写判断类,然后在需要条件注入的 Bean 上加 @Conditional(OnSmtpEnvCondition.class)
。
最后更新时间:2021-08-30 16:46:32