Spring框架:利用 Java 注解配置依赖注入
1 Java 实例代码
以下展示一个简单的后端三层架构的 demo,Web 层简化为一个 main() 方法的调用。以下省略接口代码的展示,约定类名中包含 Impl
字样的类名去掉 Impl
后就是对应的接口。
假设一个后端服务涉及到对 MySQL 数据库的 CRUD 操作和相关方法调用。对应的业务代码和控制调用如下所示。UserDao 提供一个 save() 方法以打印各种成员属性来模拟具体的业务操作,顶层代码会逐层调用直至该方法。整体业务结构如下图所示:
1.1 Service & Dao 层
UserServiceImpl.java
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void save() {
userDao.save();
}
/*其他方法&成员*/
}
UserDaoImpl.java
public class UserDaoImpl implements UserDao {
// 数据库连接源
private DataSource dataSource;
public void save() {
System.out.println("instance of UserDaoImpl: 执行save()方法");
System.out.println("DataSource: " + dataSource);
}
/*其他方法&成员*/
}
1.2 Web 层
public class WebController {
public static void main(String[] args) {
UserService userService = new UserService();
userService.save();
}
}
2 基本常用注解
常用注解中包含了基本的对象创建、依赖注入、生命周期控制等配置。这些基本注解能够取代大部分的 XML 配置文本,但是部分配置仍无法被取代,比如,以数据库连接为代表的外部导入类无法使用基本注解配置。而且,仅仅使用基本注解时,需要在 XML 文档中配置组件扫描,代替普通 XML 配置引导 Spring 框架定位组件依赖。
2.1 组件扫描
配置组件扫描需要在 XML 文档中导入 context 名称空间,进而利用 context 配置组件扫描。
<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
">
<!--支撑注解配置的组件扫描,用以替代XML配置-->
<context:component-scan base-package="com.abe.service"/>
</beans>
一个重要的属性是 base-page,它定义了扫描的区域。比如上面的示例就是扫描了 service 包中定义的组件,同理可以扫描 dao 包、domain 包等位置。而且,组件扫描配置支持被其它 XML 文档导入,因此可以定义一个公共的根配置文档管理不同包的组件扫描配置。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
">
<!--导入MySQL连接源配置-->
<import resource="classpath:sql/app-sql.xml"/>
<!--导入dao包组件扫描-->
<import resource="classpath:dao/app-user-dao.xml"/>
<!--导入service包组件扫描-->
<import resource="classpath:service/app-user-service.xml"/>
</beans>
对应的 XML 依赖关系图如下:
当然,也可以直接在根配置文件中直接定义扫描整个工程:
<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:component-scan base-package="com.abe"/>
<!--导入MySQL连接源配置-->
<import resource="classpath:sql/app-sql.xml"/>
</beans>
个人认为扫描整个工程和扫描每个子包之间会存在效率上的差异,扫描整个工程的效率会低一些,毕竟是在整个工程中查找依赖。而如果在子 XML 文档中配置扫描子包,并以导入子XML文档的方式配置,相当于为每个子包添加了索引,所以能够提升扫描效率。以上只代表个人猜想,并未验证,有了解的大佬欢迎评论区或私信交流~
2.2 创建对象
替换XML:
<bean id="" class="">
2.2.1 @Component
将资源交给 spring 管理。相当于在 xml 中配置一个 bean。
属性:
- value:指定 bean 的 id,value 作为注解的主属性可省略。如果不指定 value 属性的值,默认 bean 的 id 是当前类的类名,首字母小写。
@Component(value = "userDao") // 注解配置bean
public class UserDaoImpl implements UserDao {
/*成员&方法*/
}
等价的 XML 配置:
<bean id="userDao" class="com.abe.dao.impl.UserDaoImpl"/>
2.2.2 @Controller、@Service、@Repository
这三个注解都是基于 @Component 的衍生注解,他们的作用及属性一模一样,只是提供了更加明确的注解语义:
- @Controller: 一般用于表现层(Web)的注解。
@Controller("webController") public class WebController extends HTTPServlet { /*成员&方法*/ }
- @Service: 一般用于业务层(Service)的注解。
@Service("userService") public class UserServiceImpl implements UserService { /*成员&方法*/ }
- @Repository: 一般用于持久层(Dao)的注解。
@Reporsitory("userDao") public class UserDaoImpl implements UserDao { /*成员&方法*/ }
2.2 bean 的作用范围与生命周期
2.2.1 @Scope
指定 bean 的作用范围。
替换 XML:
<bean id="" class="" scope="">
注解属性:
- value:指定范围的值。
- singleton:单例的,默认值
- prototype:多例的
- request:WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 request 域中.
- session:WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 session 域中.
- global session:WEB 项目中,应用在 Portlet 环境.如果没有 Portlet 环境那么globalSession 相当于 session.
@Repository(value = "userDao") // 声明这是一个DAO层的Bean对象
@Scope("prototype") // 指定作用范围为多例的
public class UserDaoImpl implements UserDao {
/*成员&方法*/
}
替换 XML 配置:
<bean id="userDao" class="com.abe.dao.impl.UserDaoImpl" scope="prototype"/>
2.2.2 @PostConstruct、@PreDestroy
用于指定初始化方法和销毁方法。
替换 XML:
<bean id="" class="" init-method="" destroy-method="" />
- @PostConstruct:标注该方法是此Bean对象的初始化方法,在对象实例化后执行
- @PreDestroy:标注该方法是此Bean对象的销毁方法,在对象销毁前执行
@Service("userService") // 声明这是一个Service层的Bean对象
@Scope("prototype") // 标注Bean的作用范围
public class UserServiceImpl implements UserService {
@Resource(name = "userDao") // @Resource 等价于 @Autowired + @Qualifier
private UserDao userDao;
@PostConstruct // 标注该方法是此Bean对象的初始化方法,在对象实例化后执行
public void init() {
System.out.println("Service对象初始化...");
}
@PreDestroy // 标注该方法是此Bean对象的销毁方法,在对象销毁前执行
public void destroy() {
System.out.println("Service对象被销毁...");
}
}
替换 XML:
<bean id="userDao" class="com.abe.dao.impl.UserDaoImpl" init-method="init" destroy-method="destroy"/>
2.3 依赖注入
替换XML:
<property name="" ref="">
<property name="" value="">
在使用注解配置 bean 对象或其它基本数据类型时,无需使用有参构造或者 setter 方法。
2.3.1 @Autowired
自动按照类型注入。当使用注解注入属性时, set 方法可以省略。它只能注入其他 bean 类型。当有多个类型匹配时,使用要注入的对象变量名称作为 bean 的 id,在 Spring 容器查找,找到则注入成功;找不到则报错。通常不建议使用。
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired // 按照数据类型从Spring容器中进行匹配
private UserDao userDao; // 其它 bean 对象
}
等价的 XML 配置:
<bean id="userService" class="com.abe.service.impl.UserServiceImpl">
<!--在代码中被 @AutoWired 注解替换-->
<property name="userDao" ref="userDao"/>
</bean>
2.3.2 @Qualifier
在自动按照类型注入的基础之上,再按照 Bean 的 id 注入。它在给字段注入时不能独立使用,必须和 @Autowire 一起使用;但是给方法参数注入时,可以独立使用。
@Service("userService") // 声明这是一个Service层的Bean对象(常用值:singleton单例、默认值,prototype多例)
public class UserServiceImpl implements UserService {
@Autowired // 按照数据类型从Spring容器中进行自动匹配
@Qualifier("userDao") // 进一步按照id属性值从容器中进行匹配。
private UserDao userDao;
}
等价 XML 配置:同 2.3.1
2.3.3 @Resource
直接按照 Bean 的 id 注入,只能注入其他 bean 类型,语义更加明确,推荐使用。
@Service("userService") // 声明这是一个Service层的Bean对象(常用值:singleton单例、默认值,prototype多例)
public class UserServiceImpl implements UserService {
@Resource(name = "userDao") // @Resource 等价于 @Autowired + @Qualifier
private UserDao userDao;
}
等价 XML 配置:同 2.3.1
2.3.4 @Value
注入基本数据类型和 String 类型数据,语义更加明确,推荐使用。
配置数据库连接:
@Repository("userDao")
public class UserDaoImpl implements UserDao {
@Value("com.mysql.jdbc.driver")
private String driver;
@Value("jdbc:mysql://localhost:3306/db1")
private String url;
@Value("root")
private String username;
@Value("******")
private String password;
/*其它成员&方法*/
}
替代 XML 配置:
<bean id="userDao" class="com.abe.dao.UserDaoImpl">
<!--被注解 @Value 替换-->
<property name="driverClassName" value="com.mysql.jdbc.driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/db1"/>
<property name="username" value="root"/>
<property name="password" value="******"/>
</bean>
2.4 完整的基本注解配置示例
2.4.1 Java 代码
WebController.java
@Controller("webController")
public class WebController {
public static void main(String[] args) {
// 通过加载XML配置文件获得IOC容器
ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
// 从容器中获取对象并使用
UserService userService = app.getBean(UserService.class);
userService.save();
}
}
UserServiceImpl.java
@Service("userService") // 声明这是一个Service层的Bean对象
@Scope("prototype") // 标注Bean的作用范围
public class UserServiceImpl implements UserService {
@Resource(name = "userDao") // @Resource 等价于 @Autowired + @Qualifier
private UserDao userDao;
public void save() {
userDao.save();
}
@PostConstruct // 标注该方法是此Bean对象的初始化方法,在对象实例化后执行
public void init() {
System.out.println("Service对象初始化...");
}
@PreDestroy // // 标注该方法是此Bean对象的销毁方法,在对象销毁前执行
public void destroy() {
System.out.println("Service对象被销毁...");
}
}
UserDaoImpl.java
@Repository("userDao") // 声明这是一个DAO层的Bean对象
@Scope("prototype")
public class UserDaoImpl implements UserDao {
@Value("${jdbc.driver}") // 利用@Value注解注入普通属性,例如:加载配置文件内容
private String driver;
@Resource(name = "druidDataSource")
private DataSource dataSource;
public void save() {
System.out.println("instance of UserDaoImpl: 执行save()方法");
System.out.println("SQL driver: " + driver);
System.out.println(dataSource);
}
}
2.4.2 XML 配置文档
applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
">
<import resource="classpath:sql/app-sql.xml"/>
<import resource="classpath:dao/app-user-dao.xml"/>
<import resource="classpath:service/app-user-service.xml"/>
</beans>
app-user-service.xml
<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
">
<!--支撑注解配置的组件扫描,用以替代XML配置-->
<context:component-scan base-package="com.abe.service"/>
</beans>
app-user-dao.xml
<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
">
<!--支撑注解配置的组件扫描,用以替代XML配置-->
<context:component-scan base-package="com.abe.dao"/>
</beans>
app-sql.xml
<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
">
<!--配置xmlns context属性头-->
<!--加载外部jdbc.properties文件-->
<context:property-placeholder location="classpath:sql/jdbc.properties"/>
<!--Spring配置druid数据源-->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
使用 Java 注解配置依赖注入,XML 文档中的配置被大大简化。
3 Spring 新注解
Spring 常用注解极大地减少了 XML 配置的工作量,那么有没有办法更进一步,完全使用 Java 代码实现全部依赖注入配置,彻底干掉 XML 文档呢?答案显然是有的,即 Spring 新注解。
观察 2.4.2 中的 XML 配置文档,里面剩下的功能包括:
- 使用 context 做组件扫描,其中涉及对 bean 对象的定位问题。新注解需要对 bean 对象的定位和扫描动作提供解决方法;
- 使用 import 实现多 XML 文件配置。完全使用 Java 代码(注解)配置时,不免想到利用到多配置代码来优化依赖注入内容的可维护性。新注解需要在多配置代码组合中提供解决方法;
- 第三方 bean 类及其外部配置文件的配置。向数据库源这类非自定义类,以及对于外部配置文件内容的导入等也需要新注解提供支持。
3.1 @Configration
用于指定当前类是一个 Spring 配置类, 当创建容器时会从该类上加载注解。这个类中不需要定义任何内容只是通过注解声明告知 Spring 该类是一个依赖配置类。
SpringConfiguration.java
@Configuration // 注解含义:标志该类是Spring核心配置类
public class SpringConfiguration {
/*无需定义任何内容*/
}
在获取 IOC 容器时,不必再导入 XML 文档,而是使用 AnnotationApplicationContext(带有@Configuration 注解的类.class)。
// 通过加载Spring配置类获得容器
ApplicationContext app = new AnnotationConfigApplicationContext(SpringConfiguration.class);
3.2 @ComponenScan
组件扫描替代方法,用于指定 Spring 在初始化容器时要扫描的包
SpringConfiguration.java
@Configuration // 注解含义:标志该类是Spring核心配置类
@ComponentScan("com.abe") // 注解配置组件扫描
public class SpringConfiguration {
/*无需定义任何内容*/
}
替代 XML:
<context:component-scan base-package="com.abe"/>
3.3 @Bean
该注解只能写在方法上,表明使用此方法创建一个对象,并且放入 Spring 容器。
属性:
- name:给当前 @Bean 注解方法创建的对象指定一个名称(即 bean 的 id)。
在 DataSourceConfiguration.java 中配置数据库连接源获取方法:
@Configuration
public class DataSourceConfiguration {
@Value("com.mysql.jdbc.Driver")
private String driver;
@Value("jdbc:mysql://localhost:3306/db1")
private String url;
@Value("root")
private String username;
@Value("******")
private String password;
/**
* 配置Druid数据库连接
* @return Druid数据库连接源
*/
@Bean("druidDataSource")
public DataSource getDruidDataSource() {
// 获取并配置数据库连接
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
3.4 @PropertySource
用于加载 .properties
文件中的配置。类似数据库连接源等这类配置信息最好是从外部的专门配置文件导入。@PropertySource 注解正是解决此问题。
DataSourceConfiguration.java
@Configuration
@PropertySource("classpath:sql/jdbc.properties") // 根据注解中的路径加载数据库连接配置文件
public class DataSourceConfiguration {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/**
* 配置Druid数据库连接
* @return Druid数据库连接源
*/
@Bean("druidDataSource")
public DataSource getDruidDataSource() {
// 获取并配置数据库连接
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
3.5 @Import
最后,前面专门为数据库连接定义了一套配置代码,利用 @Import 注解将其导入到主配置文件 SpringConfiguration.java 中。
@Configuration // 注解含义:标志该类是Spring核心配置类
@ComponentScan("com.abe") // 注解配置组件扫描
@Import({DataSourceConfiguration.class}) // 导入其它配置类
public class SpringConfiguration {
}
IntelliJ IDEA 同样支持对 Java 配置代码之间的关系可视化,甚至两套配置方案之间是兼容的:
加入新注解后,所有的 XML 配置文档被完全替换。
4 XML与注解的选择
注解的优势:配置简单,维护方便(我们找到类,就相当于找到了对应的配置)。
XML 的优势:修改时,不用改源码。不涉及重新编译和部署。
以下用一张表来概括各自的特点和优势:
XML文档配置 | Java注解配置 | |
---|---|---|
Bean定义 | <bean id="…" class="…"> | @Component / @Controller / @Service / @Repository |
Bean名称 | 通过 id 或 name 指定 | @Component / @Controller / @Service / @Repository("…") |
Bean注入 | <property>标签 / p名称空间 | @AutoWired + @Qulifier("…") / @Resource("…") |
作用范围 / 生命周期 | scope / init-method / destroy-method | @Scope / @PostConstruct / @PreDestroy |
其它方法注入 Bean | 无 | @Bean |
适用场景 | Bean 来自第三方 | Bean 由开发者自定义 |