1. 基于注入管理Bean概念
Java 5 引入了注解(Annotation)这一特性,它允许程序员在源代码中插入元数据,这些元数据以标签形式存在,可以被编译器、类加载器或运行时环境所识别和处理。注解可以帮助开发者在不修改业务逻辑的前提下,向代码中添加额外的描述性信息,比如标记服务、组件、属性或其他需要特定处理的部分。
注解(Annotation)是Java编程语言中的一种强大特性,它允许开发者在源代码级别上附加一种类似于注释的元数据(meta-data)。这种元数据并不影响程序本身的执行逻辑,但它能携带关于代码元素(如类、方法、变量等)的附加信息,这些信息可以被编译器、开发工具或运行时环境所解读和利用。
注解以@符号开头,后面跟注解的名称,有时还会包含一些参数值。例如,在Java中,@Override就是一个内建的注解,它用来标记一个方法是重写了父类或接口中的方法,编译器会根据这个注解检查是否真的存在这样的重写关系,如果不存在,则会产生编译错误。
除了像@Override这类编译时就发挥作用的注解,还有很多注解在程序运行时才起作用。例如,在Spring框架中,@Component、@Service、@Repository和@Controller等注解用于标记类为Spring容器管理的组件,而@Autowired注解则指示Spring自动进行依赖注入。
Spring Framework 自从 2.5 版本开始增强了对注解功能的支持,其中一个重要应用就是通过注解实现自动装配(Auto-wiring),从而大大简化了基于 XML 的配置方式。
举个例子:
想象一下你正在经营一家咖啡店,每个员工都有不同的职责,如吧台员负责制作咖啡,收银员负责结账等。在传统的管理模式中,你需要详细地编写一份工作手册,说明谁做什么工作,如何与其他角色配合。
现在,我们把这家咖啡店比作一个Java应用程序,每个员工看作是一个类或者对象,他们的职责则是类的方法。在没有注解的情况下,你需要类似XML配置文件这样的“工作手册”,明确指出哪类对象应该扮演哪个角色(如Barista类是吧台员,Cashier类是收银员)以及他们如何协同工作(如吧台员需要一个磨豆机实例来进行工作)。
引入注解后,就像在员工的衣服上贴标签一样,可以直接在代码里标明:“我是吧台员” (@Component + @Role("Barista")) 或者 "我需要磨豆机" (@Autowired private CoffeeGrinder grinder)。这样一来,系统(Spring框架)在运行时就能自动读取这些标签(注解),并依据注解信息完成相应的工作分配和协调,无需再查看那份详细的“工作手册”。
这样,注解使得我们的代码更加简洁清晰,同时大大减少了配置工作量,提高了开发效率和可维护性。
以下是使用注解实现 Spring 自动装配的基本步骤:
- 引入依赖:
在构建项目时,确保包含 Spring 相关注解处理器的依赖项,如spring-context
或spring-boot-starter
,这将使 Spring 能够识别和处理注解。 - 开启组件扫描:
在 Spring 配置类或 XML 配置文件中启用组件扫描(Component Scan),这样 Spring 容器启动时会自动检测指定包及其子包下的类,寻找带有特定注解(如@Component
,@Service
,@Repository
,@Controller
)的类并将它们作为 Bean 进行注册。 - 使用注解定义 Bean:
在需要由 Spring 管理的类上使用上述注解,表明它们是 Spring 容器中的 Bean。例如,通过在业务类上标注@Service
注解,告诉 Spring 这是一个服务层的 Bean。 - 依赖注入:
利用注解进行依赖注入,如使用@Autowired
注解来指示 Spring 自动查找并注入相应类型的 Bean。例如,在一个类的字段、构造器或方法参数上使用@Autowired
,Spring 将负责找到符合条件的 Bean 实例并注入到对应位置。
总结:
通过注解技术,Spring 可以自动发现、实例化和组装对象,减少手动编写配置的工作量,提高了代码的可读性和维护性。
2. 搭建子模块Spring6-ioc-annotation
创建一个子模块--Spring6-ioc-annotation
ta的父工程是之前的Spring6,这里不细说~
因为父工程里已经添加了相关依赖
如:Junit测试单元、log4j2日志、spring-context之类
所以这里子模块Spring6-ioc-annotation的pom文件中不需要添加
在resource资源文件夹下创建spring配置文件、log4j2日志文件
log4j2.xml文件注意这两个本地日志存储位置,根据实际情况来
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<loggers>
<!--
level指定日志级别,从低到高的优先级:
TRACE < DEBUG < INFO < WARN < ERROR < FATAL
trace:追踪,是最低的日志级别,相当于追踪程序的执行
debug:调试,一般在开发中,都将其设置为最低的日志级别
info:信息,输出重要的信息,使用较多
warn:警告,输出警告的信息
error:错误,输出错误信息
fatal:严重错误
-->
<root level="DEBUG">
<appender-ref ref="spring6log"/>
<appender-ref ref="RollingFile"/>
<appender-ref ref="log"/>
</root>
</loggers>
<appenders>
<!--输出日志信息到控制台-->
<console name="spring6log" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
</console>
<!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
<File name="log" fileName="F:/Program/Spring6Log/test.log" append="false">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</File>
<!-- 这个会打印出所有的信息,
每次大小超过size,
则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,
作为存档-->
<RollingFile name="RollingFile" fileName="F:/Program/Spring6Log/app.log"
filePattern="log/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd 'at' HH:mm:ss z} %-5level %class{36} %L %M - %msg%xEx%n"/>
<SizeBasedTriggeringPolicy size="50MB"/>
<!-- DefaultRolloverStrategy属性如不设置,
则默认为最多同一文件夹下7个文件,这里设置了20 -->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
</appenders>
</configuration>
结构如图
3. 开启组件扫描
Spring 默认不使用注解装配 Bean,因此我们需要在 Spring 的 XML 配置中,通过 context:component-scan 元素开启 Spring Beans的自动扫描功能。开启此功能后,Spring 会自动从扫描指定的包(base-package 属性设置)及其子包下的所有类,如果类上使用了 @Component 注解,就将该类装配到容器中。
所以,我们需要配置bean.xml文件,来开启组件扫描
3.1. 配置bean.xml
bean.xml
新建时的初始样式
<?xml version="1.0" encoding="UTF-8"?>
<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">
</beans>
添加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/beans/contexts.xsd " >
</beans>
给个建议
在Spring的bean.xml
配置文件中添加新命名空间时,可以遵循以下步骤进行操作:
- 命名空间声明:
在XML文档头部,使用xmlns:prefix="http://www.springframework.org/schema/namespace"
格式声明新的命名空间,其中prefix
是一个自定义的、用于引用该命名空间的别名,而namespace
代表具体的Spring功能模块,例如“context”。
例如:
xmlns:context="http://www.springframework.org/schema/context"
- schemaLocation属性配置:
在xsi:schemaLocation
属性中,为新添加的命名空间指定对应的xsd文件位置,格式为http://www.springframework.org/schema/namespace location="xsd文件路径"
。
例如:
xsi:schemaLocation="...
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
综上所述,在bean.xml
中添加Spring 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">
<!-- 配置内容 -->
</beans>
通过这种方式,你可以逐步熟悉并掌握在Spring配置文件中引入不同功能模块所需的命名空间,提高理解和实践能力。
个人建议是以后多手动练练,当然你要是觉得懒也可以直接复制下面的,给个建议哈
<?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 ">
</beans>
3.2. 开启组件扫描
在使用 context:component-scan 元素开启自动扫描功能前,首先需要在 XML 配置的一级标签 <beans> 中添加 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-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--
开启组件扫描功能
参数:base-package 指定需要扫描的包路径
返回值:无
功能描述:通过指定包路径,自动发现和注册符合特定注解(如@Component、@Service、@Repository等)的类,完成依赖注入。
-->
<context:component-scan base-package="com.sakurapaid.spring6">
</context:component-scan>
</beans>
组件扫描的几种设定情况
在Spring框架中,为了让Spring能够识别并自动管理那些使用注解标记的Bean,我们需在Spring的XML配置文件中通过 <context:component-scan>
标签启用组件扫描功能。具体来说:
- 情况一:最基本的扫描方式
<!-- 扫描指定包下的所有组件,自动注册为Spring Bean -->
<context:component-scan base-package="com.sakurapaid.spring6">
</context:component-scan>
- 第一个配置扫描com.sakurapaid.spring6包下所有的组件,没有任何过滤条件,所以会注册包下所有被Spring管理的组件。
- 在这最基本的形式中,Spring会扫描指定的包"com.Sakurapaid.spring6"及其所有子包,查找标注了@Component、@Service、@Repository、@Controller等Spring注解的类,并将它们作为Bean注册到IoC容器中。
- 情况二:指定要排除的组件
<!-- 扫描指定包下除了注解为@Controller的组件外的所有组件 -->
<context:component-scan base-package="com.sakurapaid.spring6">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
- 第二个配置同样扫描com.sakurapaid.spring6包,但排除了所有被@Controller注解标记的组件。
- 在此情况下,除了基础扫描外,我们还指定了一个排除规则,即排除所有标注了@Controller注解的类。这意味着在扫描过程中,Spring会忽略掉所有被@Controller注解的类,不会将其作为Bean注册到IoC容器中。
- 情况三:仅扫描指定组件
<!-- 扫描指定包下仅包含注解为@Controller的组件 -->
<context:component-scan base-package="com.sakurapaid" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
- 第三个配置扫描com.sakurapaid包,但只包含被@Controller注解标记的组件,且通过设置use-default-filters="false"禁用了默认的过滤器,只保留了显式定义的包含过滤器。
- 在这一场景中,Spring默认的扫描行为被关闭(
use-default-filters="false"
),即不再扫描指定包及子包下的所有类。然后通过<context:include-filter>
标签设置了自定义的扫描规则,只包含标注了@Controller注解的类。这样,只有满足此条件的类才会被扫描并注册为Spring容器中的Bean。
3.3. 使用注解定义Bean
在Spring框架中,当我们开发一个应用程序时,我们会有很多类,比如处理业务逻辑的类、连接数据库的类以及处理用户请求的类等等。为了让Spring框架能够管理和使用这些类,我们需要将它们注册到Spring的容器(IoC容器)中。就像我们要把各种工具都放在工具箱里以便随时取用一样,Spring也需要一个地方存放和管理这些类,这个地方就是IoC容器
Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean:
注解 | 说明 |
@Component | 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。 |
@Repository | 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Service | 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Controller | 该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Component
:这个就像是一个通用标签,贴在任何一个你想让Spring管理的类上,告诉Spring:“嘿,这是一个重要的部分,你要关注它。”无论这个类是处理业务逻辑、数据库交互还是其他功能,只要贴上这个标签,Spring就会把它当作一个Bean(也就是一个可以在容器中被管理和使用的对象)来对待。@Repository
、@Service
和@Controller
:这三个注解其实是@Component
的细分和增强,它们分别针对不同的应用场景:
-
@Repository
:专门用于标注数据访问层(DAO层)的类,比如数据库操作类。当Spring看到这个注解时,就知道这个类是用来处理数据库相关工作的。@Service
:用于标注业务逻辑层(Service层)的类,这类类一般封装了复杂的业务处理逻辑。@Controller
:在Spring MVC环境下使用,标注的是控制器层的类,这类类主要负责接收用户的HTTP请求,执行相应的业务逻辑,并返回响应结果。
尽管它们各自有特定的用途和语境,但从功能上讲,它们和@Component
一样,都能使一个类成为Spring IoC容器中的Bean。这样做不仅简化了配置,同时也提高了代码的可读性和结构化程度。
举个代码栗子:
// 数据访问层(DAO层)
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
// 假设这是一个简单的方法,用于从数据库获取用户信息
public User getUserById(Long id) {
// 实现数据库查询逻辑
return new User(id, "username", "password");
}
}
// 业务逻辑层(Service层)
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository userRepository;
// 构造器注入 UserRepository
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 业务方法,调用DAO层方法完成用户信息获取
public User findUserById(Long id) {
return userRepository.getUserById(id);
}
}
// 控制层(Spring MVC中的Controller)
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class UserController {
private final UserService userService;
// 构造器注入 UserService
public UserController(UserService userService) {
this.userService = userService;
}
// 处理HTTP GET请求,根据用户ID获取用户信息
@GetMapping("/users/{id}")
@ResponseBody
public User getUser(@PathVariable Long id) {
return userService.findUserById(id);
}
}
在以上代码中:
UserRepository
类通过@Repository
注解被标识为Spring容器中的一个Bean,Spring知道它是处理数据库相关工作的类。UserService
类通过@Service
注解被标识为Spring容器中的一个Bean,它封装了业务逻辑,依赖于UserRepository
。UserController
类通过@Controller
注解被标识为Spring容器中的一个Bean,它在Spring MVC环境中处理HTTP请求,并通过调用UserService
完成业务逻辑,最终返回响应结果。
同时,由于我们在bean.xml
或其他Spring配置文件中启用了组件扫描(如之前讨论的<context:component-scan>
配置),Spring会自动发现并管理这些带有注解的类,无需手动在XML配置文件中逐一定义Bean。这样既简化了配置,也使得代码结构更为清晰和易于理解。
3.4. 实验一:@Autowired
@Autowired 是 Spring 框架提供的一种自动装配机制,它告诉 Spring 容器,某个 Bean(类实例)需要依赖其他的 Bean,并希望 Spring 能够自动找到并注入这些依赖。
查看源码:
package org.springframework.beans.factory.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}
第一部分:
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
在源码中的 @Target
注解列出了 @Autowired
可以使用的场景:
- 构造函数: 当你在一个类的构造函数上添加
@Autowired
时,Spring 会在创建这个类的实例时,根据构造函数参数的类型自动找寻并注入相应的 Bean。 - 方法: 如果你在某个方法上使用
@Autowired
,Spring 会在 Bean 初始化后调用此方法,并给其参数注入对应的 Bean。 - 方法参数: 在方法的参数上使用
@Autowired
,Spring 在调用该方法前会自动根据参数类型注入相应的 Bean。 - 类成员变量: 直接在类的成员变量(字段)上使用
@Autowired
,Spring 会自动将匹配类型的 Bean 注入到这个字段中。 - 自定义注解:
@Autowired
也可以应用在注解类型上,以便自定义具有类似自动装配功能的注解。
第二部分:
public @interface Autowired {
boolean required() default true;
}
@Autowired
注解有一个 required
属性,它的默认值是 true
,意味着:
- required=true:Spring 容器在处理自动装配时,必须找到一个与要注入的字段或方法参数类型匹配的 Bean。如果没有找到匹配的 Bean,Spring 会抛出异常,因为它认为这是必需的依赖。
- required=false:在这种情况下,Spring 仍然会尝试找到匹配的 Bean 来进行注入,但如果没有找到合适的 Bean,它不会抛出异常,而是允许注入的字段或参数保持未注入的状态。这就意味着这个依赖项不是强制必须的。
所以,简单来讲,@Autowired
是 Spring 帮助我们自动连接不同组件(Bean)的一种方式,而 required
参数则用来决定是否对这种依赖关系做强制要求。
3.4.1. 属性注入
这前提是要有spring配置文件开启组件扫描
<?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-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--
开启组件扫描,让Spring容器自动发现和装配标注了相应注解的Bean。
参数:
base-package:指定需要扫描的包路径,Spring会在此路径及其子包下查找组件。
-->
<context:component-scan base-package="com.sakurapaid.spring6"/>
</beans>
创建UserDao接口
package com.sakurapaid.spring6.autowired.dao;
public interface UserDao {
public void print();
}
创建UserDaoImpl实现
package com.sakurapaid.spring6.autowired.dao;
import org.springframework.stereotype.Repository;
@Repository // 标示一个数据库访问层的实现类
public class UserDaoImpl implements UserDao {
/**
* 打印信息,表示Dao层操作已经结束。
* 此方法没有参数。
* 也没有返回值。
*/
@Override
public void print() {
System.out.println("Dao层执行结束...");
}
}
创建UserService接口
package com.sakurapaid.spring6.autowired.service;
public interface UserService {
public void out();
}
创建UserServiceImpl实现类
package com.sakurapaid.spring6.autowired.service;
import com.sakurapaid.spring6.autowired.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // 标示一个Spring框架的服务组件
public class UserServiceImpl implements UserService {
@Autowired // 自动注入UserDao,以便于执行数据库操作
private UserDao userDao;
/**
* 执行数据库操作,并在操作完成后输出提示信息。
*/
@Override
public void out() {
userDao.print(); // 执行UserDao的print方法,通常用于打印数据库信息或其他操作
System.out.println("Service层执行结束..."); // 输出服务层执行结束的提示信息
}
}
创建UserController类
package com.sakurapaid.spring6.autowired.controller;
import com.sakurapaid.spring6.autowired.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller // 表示这是一个控制器类,用于处理用户相关的HTTP请求
public class UserController {
@Autowired // 自动注入UserService实例,以便在控制器中使用
private UserService userService;
/**
* 调用UserService中的out方法,然后在控制台打印结束信息。
* 这个方法没有参数和返回值,主要用于演示。
*/
public void out() {
userService.out(); // 调用UserService的out方法
System.out.println("Controller层执行结束...");
}
}
测试输出,代码结构如图
package com.sakurapaid.spring6.autowired;
import com.sakurapaid.spring6.autowired.controller.UserController;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* UserTest 类用于通过单元测试方法测试 UserController 类的功能。
*/
public class UserTest {
/**
* test 方法用于测试 UserController 类的实例是否能通过 Spring 上下文正确获取,并调用其方法。
* 该方法没有参数。
* 该方法没有返回值。
*/
@Test
public void test() {
// 创建一个 ClassPathXmlApplicationContext 实例,用来加载并使用 "bean.xml" 配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
// 通过上下文获取 UserController 类的实例
UserController userController = context.getBean(UserController.class);
// 调用 UserController 实例的 out 方法,进行测试
userController.out();
}
}
这是一个简单的Spring项目,其中包含三个主要部分:
- 服务层(Service Layer):
在UserServiceImpl
类中,有一个UserDao
类型的私有成员变量userDao
,并使用了@Autowired
注解。这意味着Spring框架会自动帮我们找到并设置合适的UserDao
实例给这个变量。 - 控制层(Controller Layer):
类似地,在UserController
类中,有一个UserService
类型的私有成员变量userService
,同样使用了@Autowired
注解。这样,Spring会自动把已创建好的UserService
实例(即UserServiceImpl
的一个实例)注入到这里。 - Spring容器初始化与注入:
当通过ClassPathXmlApplicationContext
加载Spring配置文件"bean.xml"时,Spring容器开始启动并管理这些Bean。它会识别出带有@Service
和@Controller
注解的类,并创建相应的实例。同时,对于带有@Autowired
注解的成员变量,Spring会根据类型自动寻找并注入相应的Bean。 - 测试过程:
在测试类UserTest
中,我们从Spring容器中获取UserController
实例,并调用其out()
方法。由于Spring已经完成了依赖注入,UserController
里的userService
已经有实际的UserService
实现,所以可以顺利调用其方法。而在UserService
的out()
方法里,又能顺利调用到已注入的UserDao
的方法。
总结来说,属性注入就是Spring框架帮助我们将相互依赖的对象关联起来的过程,无需我们在代码中手动创建和组装这些依赖关系,降低了耦合度,提高了程序的灵活性和可维护性。
3.4.2. set注入
把@Autowired注解用在对应的set方法上,而不是在成员属性上
修改UserServiceImpl类
package com.sakurapaid.spring6.autowired.service;
import com.sakurapaid.spring6.autowired.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // 标示一个Spring框架的服务组件
public class UserServiceImpl implements UserService {
/* @Autowired // 自动注入UserDao,以便于执行数据库操作
private UserDao userDao;*/
private UserDao userDao; // 用户数据访问对象
/**
* 通过自动装配设置UserDao对象。
* @param userDao 用户数据访问对象,用于进行用户数据的CRUD操作。
*/
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
/**
* 执行数据库操作,并在操作完成后输出提示信息。
*/
@Override
public void out() {
userDao.print(); // 执行UserDao的print方法,通常用于打印数据库信息或其他操作
System.out.println("Service层执行结束..."); // 输出服务层执行结束的提示信息
}
}
修改UserController类
package com.sakurapaid.spring6.autowired.controller;
import com.sakurapaid.spring6.autowired.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller // 表示这是一个控制器类,用于处理用户相关的HTTP请求
public class UserController {
/*@Autowired // 自动注入UserService实例,以便在控制器中使用
private UserService userService;*/
// UserService的引用,用于进行用户相关的操作
private UserService userService;
/**
* 通过@Autowired注解自动注入UserService实例。
*
* @param userService 要注入的UserService对象。
*/
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
/**
* 调用UserService中的out方法,然后在控制台打印结束信息。
* 这个方法没有参数和返回值,主要用于演示。
*/
public void out() {
userService.out(); // 调用UserService的out方法
System.out.println("Controller层执行结束...");
}
}
测试输出
注意:我把上一个代码测试的对象注释掉了,但重新运行测试代码能达到一样的效果,证明set起到了效果
package com.sakurapaid.spring6.autowired;
import com.sakurapaid.spring6.autowired.controller.UserController;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* UserTest 类用于通过单元测试方法测试 UserController 类的功能。
*/
public class UserTest {
/**
* test 方法用于测试 UserController 类的实例是否能通过 Spring 上下文正确获取,并调用其方法。
* 该方法没有参数。
* 该方法没有返回值。
*/
@Test
public void test() {
// 创建一个 ClassPathXmlApplicationContext 实例,用来加载并使用 "bean.xml" 配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
// 通过上下文获取 UserController 类的实例
UserController userController = context.getBean(UserController.class);
// 调用 UserController 实例的 out 方法,进行测试
userController.out();
}
}
将@Autowired
注解从成员变量移到了对应的setter方法上,这种做法称为setter注入。Spring框架依然利用依赖注入(Dependency Injection, DI)的机制来管理Bean之间的依赖关系。
下面是setter注入的原理过程:
- Spring容器初始化:
当创建ClassPathXmlApplicationContext
并加载"bean.xml"配置文件时,Spring容器开始初始化并管理所有被标记为@Service
、@Controller
等注解的Bean。 - 扫描Bean及其依赖:
Spring容器会识别出UserServiceImpl
和UserController
类上的注解,并准备创建这两个Bean的实例。同时,Spring会检查这些类中是否存在带@Autowired
注解的setter方法。 - 依赖注入:
对于UserServiceImpl
,Spring发现setUserDao
方法上有@Autowired
注解,就会从容器中查找类型匹配的UserDao
Bean,找到后调用setUserDao
方法,将UserDao
实例注入给UserServiceImpl
。
同理,对于UserController
,Spring会找到并调用setUserService
方法,将已创建的UserService
实例(也就是UserServiceImpl
实例)注入给UserController
。 - 测试阶段:
在测试类UserTest
中,我们从Spring容器获取UserController
实例。由于Spring已经在容器初始化阶段完成了对UserController
和UserService
的依赖注入,所以在调用userController.out()
时,userService
已经具备了完整的功能,可以正常调用UserServiceImpl
中的out()
方法,并进一步调用UserDao
的方法。
总的来说,无论是成员变量注入还是setter方法注入,Spring都遵循同样的依赖注入原则,即由容器负责管理Bean的生命周期和依赖关系,从而降低模块间的耦合度,提高系统的可测试性和可维护性。在本例中,通过setter方法注入的方式,仍然实现了相同的效果。
3.4.3. 构造方法注入
将@Autowired放在了构造方法上
修改UserServiceImpl类
package com.sakurapaid.spring6.autowired.service;
import com.sakurapaid.spring6.autowired.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // 标示一个Spring框架的服务组件
public class UserServiceImpl implements UserService {
/* @Autowired // 自动注入UserDao,以便于执行数据库操作
private UserDao userDao;*/
/*private UserDao userDao; // 用户数据访问对象
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}*/
private UserDao userDao;
@Autowired
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
/**
* 执行数据库操作,并在操作完成后输出提示信息。
*/
@Override
public void out() {
userDao.print(); // 执行UserDao的print方法,通常用于打印数据库信息或其他操作
System.out.println("Service层执行结束..."); // 输出服务层执行结束的提示信息
}
}
修改UserController类
package com.sakurapaid.spring6.autowired.controller;
import com.sakurapaid.spring6.autowired.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller // 表示这是一个控制器类,用于处理用户相关的HTTP请求
public class UserController {
/*@Autowired // 自动注入UserService实例,以便在控制器中使用
private UserService userService;*/
// UserService的引用,用于进行用户相关的操作
/*private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}*/
private UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
/**
* 调用UserService中的out方法,然后在控制台打印结束信息。
* 这个方法没有参数和返回值,主要用于演示。
*/
public void out() {
userService.out(); // 调用UserService的out方法
System.out.println("Controller层执行结束...");
}
}
测试输出
测试代码是一样的,这里就不重复了,直接放结果
构造方法注入是Spring框架依赖注入的另一种方式,它通过在类的构造函数上使用@Autowired
注解来完成依赖对象的注入。以下是构造方法注入的原理过程:
- Spring容器初始化:
当创建ClassPathXmlApplicationContext
并加载配置文件时,Spring容器开始扫描并管理所有的Bean。 - 识别构造器注入:
Spring容器在创建UserServiceImpl
Bean时,会发现其构造函数上有@Autowired
注解,这意味着Spring需要通过此构造函数来初始化UserServiceImpl
实例,并注入所需的UserDao
依赖。 - 注入依赖:
Spring会在IoC容器中查找类型匹配的UserDao
Bean,找到后将其实例作为参数传入UserServiceImpl
的构造函数,从而完成对UserDao
依赖的注入。
同样地,在创建UserController
Bean时,Spring发现其构造函数上有@Autowired
注解,此时Spring会查找并注入已经初始化好的UserService
Bean。 - 测试阶段:
测试类UserTest
从Spring容器获取UserController
实例时,由于Spring已在初始化阶段通过构造方法注入的方式完成了对UserController
和UserService
的依赖设置,所以调用userController.out()
时,userService
能够正确地执行业务逻辑。
总结来说,构造方法注入是在Bean实例化阶段通过构造函数一次性注入所有必需的依赖,相比于成员变量注入和setter方法注入,构造方法注入确保了对象在实例化后即可拥有完整的功能,增强了对象的即时可用性和一致性。在上述代码中,无论是在UserServiceImpl
还是UserController
类中,都是通过构造方法注入的方式来完成依赖注入的。
3.4.4. 形参上注入
@Autowired 也可以用在形参上
修改UserServiceImpl类
package com.sakurapaid.spring6.autowired.service;
import com.sakurapaid.spring6.autowired.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // 标示一个Spring框架的服务组件
public class UserServiceImpl implements UserService {
/* @Autowired // 自动注入UserDao,以便于执行数据库操作
private UserDao userDao;*/
/*private UserDao userDao; // 用户数据访问对象
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}*/
private UserDao userDao;
public UserServiceImpl(@Autowired UserDao userDao) {
this.userDao = userDao;
}
/**
* 执行数据库操作,并在操作完成后输出提示信息。
*/
@Override
public void out() {
userDao.print(); // 执行UserDao的print方法,通常用于打印数据库信息或其他操作
System.out.println("Service层执行结束..."); // 输出服务层执行结束的提示信息
}
}
修改UserController类
package com.sakurapaid.spring6.autowired.controller;
import com.sakurapaid.spring6.autowired.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller // 表示这是一个控制器类,用于处理用户相关的HTTP请求
public class UserController {
/*@Autowired // 自动注入UserService实例,以便在控制器中使用
private UserService userService;*/
// UserService的引用,用于进行用户相关的操作
/*private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}*/
private UserService userService;
public UserController(@Autowired UserService userService) {
this.userService = userService;
}
/**
* 调用UserService中的out方法,然后在控制台打印结束信息。
* 这个方法没有参数和返回值,主要用于演示。
*/
public void out() {
userService.out(); // 调用UserService的out方法
System.out.println("Controller层执行结束...");
}
}
测试输出
形参注入的优势在于,它可以在创建对象的同时确保依赖对象的有效性,有助于保持对象的完整性,并且可以通过构造函数强制要求必须提供所需依赖,增强了代码的清晰性和安全性。
这种方式下,@Autowired
注解直接放在了构造函数的参数上。以下是形参注入的原理过程:
- Spring容器初始化:
当Spring容器加载配置并准备创建UserController
Bean时,它会检测到类的构造函数中有带有@Autowired
注解的参数。 - 依赖查找与注入:
Spring会查找IoC容器中类型匹配的UserService
Bean。一旦找到匹配的Bean,Spring会将该Bean实例作为参数传递给UserController
的构造函数,进而完成对UserService
依赖的注入。 - 对象实例化:
通过构造函数注入依赖,Spring容器在创建UserController
实例的同时完成了所有必要的依赖初始化。因此,新创建的UserController
实例就已经具有了完整功能,可以直接调用UserService
的方法。 - 测试阶段:
在测试类中,从Spring容器获取到的UserController
实例,其内部的userService
成员变量已经被正确注入,因此可以成功执行out()
方法,依次调用UserService
和UserDao
的相关方法,并最终在控制台输出预期的结果。
3.4.5. 只有一个构造函数,无注解
当有参数的构造方法只有一个时,@Autowired注解可以省略
修改UserServiceImpl类
package com.sakurapaid.spring6.autowired.service;
import com.sakurapaid.spring6.autowired.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service // 标示一个Spring框架的服务组件
public class UserServiceImpl implements UserService {
/* @Autowired // 自动注入UserDao,以便于执行数据库操作
private UserDao userDao;*/
/*private UserDao userDao; // 用户数据访问对象
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}*/
/*private UserDao userDao;
public UserServiceImpl(@Autowired UserDao userDao) {
this.userDao = userDao;
}*/
private UserDao userDao;
public UserServiceImpl( UserDao userDao) {
this.userDao = userDao;
}
/**
* 执行数据库操作,并在操作完成后输出提示信息。
*/
@Override
public void out() {
userDao.print(); // 执行UserDao的print方法,通常用于打印数据库信息或其他操作
System.out.println("Service层执行结束..."); // 输出服务层执行结束的提示信息
}
}
修改UserController类
package com.sakurapaid.spring6.autowired.controller;
import com.sakurapaid.spring6.autowired.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller // 表示这是一个控制器类,用于处理用户相关的HTTP请求
public class UserController {
/*@Autowired // 自动注入UserService实例,以便在控制器中使用
private UserService userService;*/
// UserService的引用,用于进行用户相关的操作
/*private UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}*/
/*private UserService userService;
public UserController(@Autowired UserService userService) {
this.userService = userService;
}*/
private UserService userService;
public UserController( UserService userService) {
this.userService = userService;
}
/**
* 调用UserService中的out方法,然后在控制台打印结束信息。
* 这个方法没有参数和返回值,主要用于演示。
*/
public void out() {
userService.out(); // 调用UserService的out方法
System.out.println("Controller层执行结束...");
}
}
测试输出
上面的操作是移除了构造函数参数上的@Autowired
注解。尽管如此,Spring依然能够进行依赖注入,这是因为:
- Spring容器初始化:
当Spring容器加载配置并初始化Bean时,它会查找所有带有@Controller
等注解的类以创建Bean实例。 - 自动装配:
即使没有在构造函数参数上明确使用@Autowired
注解,Spring也能基于类型匹配进行自动装配。当Spring容器创建UserController
Bean时,它会发现UserController
构造函数需要一个UserService
类型的参数。此时,Spring会在IoC容器中寻找类型匹配的UserService
Bean。 - 依赖注入:
若Spring找到了唯一的UserService
Bean,则会将该Bean实例注入到UserController
的构造函数中,从而完成依赖注入。 - 对象实例化与测试:
构造函数注入依然有效,创建出来的UserController
实例已经包含了完整的UserService
依赖。因此,在测试阶段,可以从Spring容器获取到具有完全功能的UserController
实例,并能成功调用UserService
的out()
方法。
需要注意的是,虽然在本例中移除@Autowired
注解不影响依赖注入,但如果存在多个同类型候选Bean时,Spring无法确定具体要注入哪个Bean,这时就需要恢复使用@Autowired
注解配合其他策略(如@Qualifier
注解)来指定确切的Bean。在只有一个匹配Bean的情况下,默认的类型匹配足以完成注入任务。
说明:有多个构造方法时呢?大家可以测试(再添加一个无参构造函数),测试报错
3.4.6. @Autowired@Qualifier联合注解
添加dao层实现--UserDaoRedisImpl
package com.sakurapaid.spring6.autowired.dao;
import org.springframework.stereotype.Repository;
@Repository
public class UserDaoRedisImpl implements UserDao {
@Override
public void print() {
System.out.println("Redis Dao层执行结束");
}
}
此时测试输出会报错,报错提示很长我截取关键的
因为@Autowired 默认是按照ByType,按照类型进行装配
所以这里需要byName,根据名称进行装配了。
修改UserServiceImpl类
package com.sakurapaid.spring6.autowired.service;
import com.sakurapaid.spring6.autowired.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service // 标示一个Spring框架的服务组件
public class UserServiceImpl implements UserService {
/* @Autowired // 自动注入UserDao,以便于执行数据库操作
private UserDao userDao;*/
/*private UserDao userDao; // 用户数据访问对象
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}*/
/*private UserDao userDao;
public UserServiceImpl(@Autowired UserDao userDao) {
this.userDao = userDao;
}*/
@Autowired
//value值默认小写
@Qualifier(value = "userDaoRedisImpl")
private UserDao userDao;
/**
* 执行数据库操作,并在操作完成后输出提示信息。
*/
@Override
public void out() {
userDao.print(); // 执行UserDao的print方法,通常用于打印数据库信息或其他操作
System.out.println("Service层执行结束..."); // 输出服务层执行结束的提示信息
}
}
测试输出
通过@Autowired
和@Qualifier
注解组合使用,我们可以更精确地控制依赖注入的行为,使其不仅依据类型匹配,还能根据Bean的名称进行精准注入,解决了可能存在多个同类型候选Bean时的注入问题。在本例中,Spring会根据名称"userDaoRedisImpl"找到并注入对应的UserDao
实现类。
使用了@Autowired
结合@Qualifier
注解来进行按名称装配(ByName)的依赖注入。原理过程如下:
- Spring容器初始化:
当Spring容器加载配置并初始化Bean时,它会扫描带有@Service
等注解的类,准备创建UserServiceImpl
Bean。 - 按名称装配:
在UserServiceImpl
类中,UserDao
成员变量同时使用了@Autowired
和@Qualifier
注解。@Autowired
注解告诉Spring需要自动装配一个UserDao
类型的依赖,而@Qualifier(value = "userDaoRedisImpl")
则指定了具体的Bean名称,要求Spring根据名称去查找并注入对应的UserDao
实现类。 - 依赖查找与注入:
Spring容器在IoC容器中查找名称为"userDaoRedisImpl"的UserDao
Bean。如果找到,则将该Bean实例注入到UserServiceImpl
的userDao
属性上。 - 测试阶段:
当从Spring容器获取UserServiceImpl
实例时,其内部的userDao
属性已经被注入了指定名称的UserDao
实现类实例。因此,在调用out()
方法时,会调用到指定名称所对应的具体实现类的方法。
3.4.7. 总结
场景一:属性注入
- 在类的成员变量上直接使用
@Autowired
注解,Spring会自动查找并注入与该成员变量类型匹配的Bean。
@Autowired
private UserDao userDao;
场景二:set注入
- 在setter方法上使用
@Autowired
注解,Spring会在Bean实例化后调用该方法,注入匹配类型的Bean。
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
场景三:构造方法注入
- 在类的构造函数上使用
@Autowired
注解,Spring会在创建Bean实例时通过构造函数注入依赖。
@Autowired
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
场景四:形参上注入
- 这与场景三类似,只是注解直接放在构造函数的参数上,Spring会根据类型自动注入。
public UserController(@Autowired UserService userService) {
this.userService = userService;
}
场景五:只有一个构造函数,无注解
- 如果类中只有一个无参或有参构造函数,且无任何注解,Spring在某些情况下仍能通过类型匹配自动注入依赖(前提是IoC容器中只有一个匹配类型的Bean)。
场景六:@Autowired
注解和@Qualifier
注解联合
- 当存在多个同类型候选Bean时,单独使用
@Autowired
可能会导致不确定注入哪一个Bean。此时配合@Qualifier
注解,通过指定Bean的名称来精准注入。
@Autowired
@Qualifier(value = "userDaoRedisImpl")
private UserDao userDao;
通过以上场景,我们可以看出@Autowired
注解在不同位置和场景下的作用,主要是帮助Spring容器自动管理和注入Bean的依赖关系,实现低耦合、高内聚的设计目标。同时,结合@Qualifier
注解能够更精细地控制注入行为,解决多实例选择问题。
3.5. 实验二:@Resource注入
- @Resource注解 :
-
- 来源于Java EE规范(JSR-250),在Java EE环境中通常无需额外引入依赖就能使用,但在非Java EE环境如Java SE中,需添加如上所述的依赖包。
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
-
- 默认注入策略为按名称(byName)。若在注解中未指定
name
属性,它会尝试匹配字段名或setter方法名作为Bean的名称进行注入;若无法按名称找到匹配的Bean,则会尝试按类型(byType)进行注入。 - 通常应用于字段或setter方法上,例如:
- 默认注入策略为按名称(byName)。若在注解中未指定
@Resource(name = "userDaoRedisImpl")
private UserDao userDao;
- @Autowired注解 :
-
- 是Spring框架提供的注解,专门用于Spring环境下的依赖注入。
- 默认注入策略为按类型(byType),即Spring容器会查找并注入与被注解字段或方法参数类型相匹配的Bean。若存在多个相同类型的候选Bean,则需要配合
@Qualifier
注解明确指定Bean的名称。 - 可以应用于字段、setter方法、构造方法以及构造方法参数上,例如:
@Autowired
private UserService userService;
// 或者配合@Qualifier
@Autowired
@Qualifier("userServiceDBImpl")
private UserService userService;
简而言之,@Resource
和@Autowired
都能实现依赖注入,但默认的注入策略不同。@Resource
倾向于按名称注入,而@Autowired
倾向于按类型注入。在具体使用时,开发者可以根据项目的实际情况和需求来选择合适的注入方式。
3.5.1. 根据name注入
项目结构
修改UserDaoImpl类
package com.sakurapaid.spring6.resource.dao;
import org.springframework.stereotype.Repository;
@Repository("myUserDao") // 标示一个数据库访问层的实现类
public class UserDaoImpl implements UserDao {
/**
* 打印信息,表示Dao层操作已经结束。
* 此方法没有参数。
* 也没有返回值。
*/
@Override
public void print() {
System.out.println("Dao层执行结束...");
}
}
修改UserServiceImpl类
package com.sakurapaid.spring6.resource.service;
import com.sakurapaid.spring6.resource.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service("myService") // 标示一个Spring框架的服务组件
public class UserServiceImpl implements UserService {
@Resource(name = "myUserDao")
private UserDao userDao;
/**
* 执行数据库操作,并在操作完成后输出提示信息。
*/
@Override
public void out() {
userDao.print(); // 执行UserDao的print方法,通常用于打印数据库信息或其他操作
System.out.println("Service层执行结束..."); // 输出服务层执行结束的提示信息
}
}
测试输出
package com.sakurapaid.spring6.resource;
import com.sakurapaid.spring6.resource.controller.UserControllers;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 用户相关测试类
*/
public class UserTest {
/**
* 测试方法,用于验证 UserController 的功能。
* 该方法不接受参数,也不返回任何值。
*/
@Test
public void test() {
// 创建 Spring 应用上下文,加载配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
// 从上下文中获取 UserController 的实例
UserControllers userControllers = context.getBean(UserControllers.class, "myController");
// 调用 UserController 的方法进行测试
userControllers.out();
}
}
在这个例子中,我们看到UserServiceImpl
类中的UserDao
依赖是通过@Resource
注解并指定name="myUserDao"
进行注入的。
以下是根据名称注入的原理过程:
- Spring容器初始化:
当通过ClassPathXmlApplicationContext
创建Spring应用上下文并加载配置文件bean.xml
时,Spring开始扫描并注册所有符合Spring Bean定义规则的类。 - 识别资源注入:
Spring在初始化UserServiceImpl
Bean时,会发现UserDao
成员变量上使用了@Resource
注解,并指定了名称为myUserDao
。 - 依赖查找:
根据@Resource
注解中指定的名称myUserDao
,Spring容器会在已注册的Bean定义中查找名称匹配的Bean。若在bean.xml
或其他自动扫描的组件中有一个Bean定义的id或name为myUserDao
,并且它的类型与UserDao
一致或兼容,那么Spring将会找到这个Bean。 - 依赖注入:
找到匹配的Bean后,Spring容器将其实例注入到UserServiceImpl
的userDao
属性中,完成依赖注入。 - 测试阶段:
在测试类UserTest
中,我们创建了一个Spring应用上下文,并通过getBean
方法根据名称"myController"
获取UserControllers
实例。假设UserControllers
内部也通过@Resource
或@Autowired
注解正确注入了UserService
实例,那么当调用userControllers.out()
方法时,UserService
中的out
方法会被执行,其中包含从UserDao
注入的对象执行的数据库操作。
综上所述,根据名称注入的过程就是Spring容器根据注解中指定的名称在IoC容器中查找匹配的Bean实例,并将其注入到相应属性的过程。在这里,UserServiceImpl
中的UserDao
依赖就是通过名称myUserDao
成功注入的。
3.5.2. name未知注入
当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。
修改UserDaoImpl类
package com.sakurapaid.spring6.resource.dao;
import org.springframework.stereotype.Repository;
@Repository("myuserDao") // 标示一个数据库访问层的实现类
public class UserDaoImpl implements UserDao {
/**
* 打印信息,表示Dao层操作已经结束。
* 此方法没有参数。
* 也没有返回值。
*/
@Override
public void print() {
System.out.println("Dao层执行结束...");
}
}
修改UserServiceImpl类
这里并没有给@Resource进行name指定
package com.sakurapaid.spring6.resource.service;
import com.sakurapaid.spring6.resource.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service("myService") // 标示一个Spring框架的服务组件
public class UserServiceImpl implements UserService {
@Resource
private UserDao myuserDao;
/**
* 执行数据库操作,并在操作完成后输出提示信息。
*/
@Override
public void out() {
myuserDao.print(); // 执行UserDao的print方法,通常用于打印数据库信息或其他操作
System.out.println("Service层执行结束..."); // 输出服务层执行结束的提示信息
}
}
但测试输出一样能成功
当@Resource注解使用时没有指定name的时候,还是根据name进行查找,这个name是属性名。
3.5.3. 其他情况
如果上面两种情况都不是,没有指定name,而且根据属性名也找不到
那么就会根据类名ByType进行注入
修改UserControllers类
package com.sakurapaid.spring6.resource.controller;
import com.sakurapaid.spring6.resource.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;
@Controller("myController") // 表示这是一个控制器类,用于处理用户相关的HTTP请求
public class UserControllers {
//根据名字进行注入
/*@Resource(name = "myService")
private UserService userService;*/
//根据类型进行注入
@Resource
private UserService userService;
public UserControllers(UserService userService) {
this.userService = userService;
}
/**
* 调用UserService中的out方法,然后在控制台打印结束信息。
* 这个方法没有参数和返回值,主要用于演示。
*/
public void out() {
userService.out(); // 调用UserService的out方法
System.out.println("Controller层执行结束...");
}
}
修改UserServiceImpl类
package com.sakurapaid.spring6.resource.service;
import com.sakurapaid.spring6.resource.dao.UserDao;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service("myService") // 标示一个Spring框架的服务组件
public class UserServiceImpl implements UserService {
@Resource
private UserDao myuserDao;
/**
* 执行数据库操作,并在操作完成后输出提示信息。
*/
@Override
public void out() {
myuserDao.print(); // 执行UserDao的print方法,通常用于打印数据库信息或其他操作
System.out.println("Service层执行结束..."); // 输出服务层执行结束的提示信息
}
}
这里的@Resource没有指定name,并且属性名userService和myService也对不上
所以这里就根据UserService这个类进行注入
得到的结果是一样的
3.6. Spring全注解开发
全注解开发是指在Spring框架中,我们不再使用传统的XML配置文件来定义Bean和配置组件扫描等,而是通过编写Java配置类来替代。
比如以前写的spring配置文件
<?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-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--
开启组件扫描,让Spring容器自动发现和装配标注了相应注解的Bean。
参数:
base-package:指定需要扫描的包路径,Spring会在此路径及其子包下查找组件。
-->
<context:component-scan base-package="com.sakurapaid.spring6"/>
</beans>
现在用一个类文件完成,这是一样的效果
package com.sakurapaid.spring6.resource.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.sakurapaid.spring6.resource")
public class ResourceConfig {
}
测试输出
package com.sakurapaid.spring6.resource;
import com.sakurapaid.spring6.resource.config.ResourceConfig;
import com.sakurapaid.spring6.resource.controller.UserControllers;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* 用户相关测试类
*/
public class UserTest {
/**
* 测试方法,用于验证 UserController 的功能。
* 该方法不接受参数,也不返回任何值。
*/
@Test
public void test() {
// 创建 Spring 应用上下文,加载配置文件--用xml文件方式
//ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
// 创建 Spring 应用上下文,加载配置文件--用注解类方式
ApplicationContext context = new AnnotationConfigApplicationContext(ResourceConfig.class);
// 从上下文中获取 UserController 的实例
UserControllers userControllers = context.getBean(UserControllers.class);
// 调用 UserController 的方法进行测试
userControllers.out();
}
}
4. 原理--手写IOC
Spring框架的核心特性之一是它的依赖注入(Dependency Injection,DI),这是控制反转(Inversion of Control,IoC)的一种实现方式。在传统的编程模式中,对象通常主动创建并管理对其它对象的依赖。而在采用IoC原则的Spring框架中,对象不再自行创建所依赖的对象,而是由一个称为“容器”(Container)的外部实体负责创建和管理这些对象以及它们之间的依赖关系。
Spring框架通过Java反射机制实现了IoC。Java反射机制允许我们在运行时分析类和对象,并能动态地创建对象、访问私有属性、调用方法等,即使这些类的信息在编译期间并不完全可知。
4.1. Java反射
Java反射机制通俗解释:
Java反射机制就如同一部智能手机对APP的深度访问权限,让你在APP运行时,不仅能看到其界面和功能,还能深入探索和操控其内部结构和行为。
- 洞察类结构:在程序运行时,通过反射技术,你无需提前知晓每个类的具体细节,就能实时获取任何类的名称、属性列表(也就是字段/成员变量)、以及各种方法。
- 动态操作类元素:无需硬编码类名或方法名,可在运行时动态创建类的实例,并能调用类中定义的所有方法,甚至包括原本在编写代码阶段因访问权限限制而不能直接触达的私有方法和字段。
举例说明:
- 获取类信息:想象你有一个神秘的盒子,里面装着关于某个类的所有秘密配方(即类的静态信息)。通过
Class.forName()
这个咒语,你可以打开盒子,取出封装类信息的Class
对象。 - 制造对象实例:即使只知道类的一些基本描述,也可以通过反射的
newInstance()
方法像变魔术般创造出该类的实例。 - 操纵方法和字段:进一步地,你可以利用反射拿到类的特定方法(Method)和字段(Field)的“遥控器”,然后按需调用方法或更改字段的值。
在现实的应用场景中,像是Spring框架中的IoC(控制反转)就巧妙运用了反射机制,能够在运行时根据配置灵活地生成对象、注入依赖,并管理对象的整个生命周期。
结论:
Java反射机制突破了传统编程中的静态类型束缚,大大增强了程序的灵活性和适应性,尤其在构建大型框架和模块化系统时作用显著。然而,如同任何强大的工具,过度或不当使用反射可能影响程序执行效率,降低代码可读性和可维护性,因此,应当根据实际需求适度且明智地运用这一机制。
简单的用代码举例:
如何使用Java反射API来分析和操作自定义类的各种元素,包括获取Class对象、访问构造方法、获取和修改属性以及调用方法。
新建一个子模块
项目结构
自定义类 Car
我们有一个自定义类Car,它具有若干属性(name、age、color),构造方法(无参构造和带参数构造),getter/setter方法,以及一个私有方法run()和重写的toString()方法。
package com.sakurapaid.reflect;
public class Car {
private String name;
private int age;
private String color;
//私有方法
private void test() {
System.out.println("Car类的私有方法使用了...");
}
public Car() {
}
public Car(String name, int age, String color) {
this.name = name;
this.age = age;
this.color = color;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
* @return color
*/
public String getColor() {
return color;
}
/**
* 设置
* @param color
*/
public void setColor(String color) {
this.color = color;
}
public String toString() {
return "Car{name = " + name + ", age = " + age + ", color = " + color + "}";
}
}
编写测试类 TestCar
1. 获取Class对象
@Test
public void test01() throws Exception {
// 通过类名.class获取Class对象
Class<Car> clazz1 = Car.class;
// 通过对象的getClass()方法获取Class对象
Car carInstance = new Car();
Class<Car> clazz2 = carInstance.getClass();
// 通过Class.forName()方法,传入类的全限定名字符串获取Class对象
Class<Car> clazz3 = Class.forName("com.atguigu.reflect.Car");
// 使用默认无参构造函数创建Car对象实例
Car car = (Car) clazz3.getConstructor().newInstance();
System.out.println(car); // 输出新建Car对象的基本信息
}
这部分代码展示了获取Class对象的三种方式,Class对象是Java反射的核心,代表了类的类型信息。有了Class对象,就可以进一步获取类的构造方法、属性和方法等信息。
2. 获取并操作构造方法
@Test
public void test02() throws Exception {
Class<Car> clazz = Car.class;
// 获取Car类的所有构造方法(包括私有构造方法)
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
// 遍历构造方法并打印名称和参数个数
for (Constructor<?> c : constructors) {
System.out.println("方法名称:" + c.getName() + " 参数个数:" + c.getParameterCount());
}
// 获取带有三个参数的私有构造方法,并设置为可访问
Constructor<Car> c2 = clazz.getDeclaredConstructor(String.class, int.class, String.class);
c2.setAccessible(true);
// 使用该构造方法创建Car对象实例,并输出基本信息
Car car2 = (Car) c2.newInstance("捷达", 15, "白色");
System.out.println(car2);
}
这里展示了如何获取并操作类的构造方法,包括获取所有构造方法,设置私有构造方法为可访问,以及使用构造方法创建对象实例。
3. 获取并操作属性
@Test
public void test03() throws Exception {
Class<Car> clazz = Car.class;
Car car = (Car) clazz.getDeclaredConstructor().newInstance();
// 获取Car类的所有字段(包括私有字段)
Field[] fields = clazz.getDeclaredFields();
// 遍历字段,并查找名为"name"的字段,设置其为可访问,并修改值
for (Field field : fields) {
if (field.getName().equals("name")) {
field.setAccessible(true);
field.set(car, "五菱宏光");
}
System.out.println(field.getName()); // 输出字段名
}
// 输出修改后的Car对象基本信息
System.out.println(car);
}
这部分代码演示了如何获取类的所有属性(字段),找到特定字段并修改其值。
4. 获取并执行方法
@Test
public void test04() throws Exception {
Car car = new Car("奔驰", 10, "黑色");
Class<Car> clazz = car.getClass();
// 获取Car类的所有公共方法
Method[] methods = clazz.getMethods();
// 遍历公共方法,找到并执行toString()方法
for (Method m1 : methods) {
if (m1.getName().equals("toString")) {
String invoke = (String) m1.invoke(car);
// System.out.println("toString执行结果:" + invoke);
}
}
// 获取Car类的所有方法(包括私有方法)
Method[] methodsAll = clazz.getDeclaredMethods();
// 遍历所有方法,找到并执行私有方法run()
for (Method m : methodsAll) {
if (m.getName().equals("run")) {
m.setAccessible(true);
m.invoke(car);
}
}
}
最后,这段代码展示了如何获取类的公共方法和所有方法(包括私有方法),并执行对应的方法,如toString()
和run()
。需要注意的是,在调用私有方法前需调用setAccessible(true)
使方法可访问。
4.2. 实现Spring的IoC
这段过程有点难懂,建议搭配搭配视频一起看
链接传送门 --> 尚硅谷Spring6--P47-P51
我们知道,IoC(控制反转)和DI(依赖注入)是Spring里面核心的东西,那么,我们如何自己手写出这样的代码呢?下面我们就一步一步写出Spring框架最核心的部分。
4.2.1. 搭建子模块
4.2.2. 准备测试需要的bean
添加依赖
<dependencies>
<!--junit5测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
</dependencies>
创建UserDao接口
package com.atguigu.spring6.test.dao;
public interface UserDao {
public void print();
}
创建UserDaoImpl实现
package com.atguigu.spring6.test.dao.impl;
import com.atguigu.spring.dao.UserDao;
public class UserDaoImpl implements UserDao {
@Override
public void print() {
System.out.println("Dao层执行结束");
}
}
创建UserService接口
package com.atguigu.spring6.test.service;
public interface UserService {
public void out();
}
创建UserServiceImpl实现类
package com.atguigu.spring.test.service.impl;
import com.atguigu.spring.core.annotation.Bean;
import com.atguigu.spring.service.UserService;
@Bean
public class UserServiceImpl implements UserService {
// private UserDao userDao;
@Override
public void out() {
//userDao.print();
System.out.println("Service层执行结束");
}
}
4.2.3. 定义注解
我们通过注解的形式加载bean与实现依赖注入
bean注解
package com.atguigu.spring.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {
}
依赖注入注解
package com.atguigu.spring.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Di {
}
说明:上面两个注解可以随意取名
4.2.4. 定义bean容器接口
package com.atguigu.spring.core;
public interface ApplicationContext {
Object getBean(Class clazz);
}
4.2.5. 编写注解bean容器接口实现
AnnotationApplicationContext基于注解扫描bean
package com.atguigu.spring.core;
import java.util.HashMap;
public class AnnotationApplicationContext implements ApplicationContext {
//存储bean的容器
private HashMap<Class, Object> beanFactory = new HashMap<>();
@Override
public Object getBean(Class clazz) {
return beanFactory.get(clazz);
}
/**
* 根据包扫描加载bean
* @param basePackage
*/
public AnnotationApplicationContext(String basePackage) {
}
}
4.2.6. 编写扫描bean逻辑
我们通过构造方法传入包的base路径,扫描被@Bean注解的java对象,完整代码如下:
package com.atguigu.spring.core;
import com.atguigu.spring.core.annotation.Bean;
import java.io.File;
import java.util.HashMap;
public class AnnotationApplicationContext implements ApplicationContext {
//存储bean的容器
private HashMap<Class, Object> beanFactory = new HashMap<>();
private static String rootPath;
@Override
public Object getBean(Class clazz) {
return beanFactory.get(clazz);
}
/**
* 根据包扫描加载bean
* @param basePackage
*/
public AnnotationApplicationContext(String basePackage) {
try {
String packageDirName = basePackage.replaceAll("\\.", "\\\\");
Enumeration<URL> dirs =Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (dirs.hasMoreElements()) {
URL url = dirs.nextElement();
String filePath = URLDecoder.decode(url.getFile(),"utf-8");
rootPath = filePath.substring(0, filePath.length()-packageDirName.length());
loadBean(new File(filePath));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void loadBean(File fileParent) {
if (fileParent.isDirectory()) {
File[] childrenFiles = fileParent.listFiles();
if(childrenFiles == null || childrenFiles.length == 0){
return;
}
for (File child : childrenFiles) {
if (child.isDirectory()) {
//如果是个文件夹就继续调用该方法,使用了递归
loadBean(child);
} else {
//通过文件路径转变成全类名,第一步把绝对路径部分去掉
String pathWithClass = child.getAbsolutePath().substring(rootPath.length() - 1);
//选中class文件
if (pathWithClass.contains(".class")) {
// com.xinzhi.dao.UserDao
//去掉.class后缀,并且把 \ 替换成 .
String fullName = pathWithClass.replaceAll("\\\\", ".").replace(".class", "");
try {
Class<?> aClass = Class.forName(fullName);
//把非接口的类实例化放在map中
if(!aClass.isInterface()){
Bean annotation = aClass.getAnnotation(Bean.class);
if(annotation != null){
Object instance = aClass.newInstance();
//判断一下有没有接口
if(aClass.getInterfaces().length > 0) {
//如果有接口把接口的class当成key,实例对象当成value
System.out.println("正在加载【"+ aClass.getInterfaces()[0] +"】,实例对象是:" + instance.getClass().getName());
beanFactory.put(aClass.getInterfaces()[0], instance);
}else{
//如果有接口把自己的class当成key,实例对象当成value
System.out.println("正在加载【"+ aClass.getName() +"】,实例对象是:" + instance.getClass().getName());
beanFactory.put(aClass, instance);
}
}
}
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
}
}
}
}
4.2.7. java类标识Bean注解
@Bean
public class UserServiceImpl implements UserService
@Bean
public class UserDaoImpl implements UserDao
4.2.8. 测试Bean加载
package com.atguigu.spring;
import com.atguigu.spring.core.AnnotationApplicationContext;
import com.atguigu.spring.core.ApplicationContext;
import com.atguigu.spring.test.service.UserService;
import org.junit.jupiter.api.Test;
public class SpringIocTest {
@Test
public void testIoc() {
ApplicationContext applicationContext = new AnnotationApplicationContext("com.atguigu.spring.test");
UserService userService = (UserService)applicationContext.getBean(UserService.class);
userService.out();
System.out.println("run success");
}
}
控制台打印测试
4.2.9. 依赖注入
只要userDao.print();调用成功,说明就注入成功
package com.atguigu.spring.test.service.impl;
import com.atguigu.spring.core.annotation.Bean;
import com.atguigu.spring.core.annotation.Di;
import com.atguigu.spring.dao.UserDao;
import com.atguigu.spring.service.UserService;
@Bean
public class UserServiceImpl implements UserService {
@Di
private UserDao userDao;
@Override
public void out() {
userDao.print();
System.out.println("Service层执行结束");
}
}
执行第八步:报错了,说明当前userDao是个空对象
4.2.10. 依赖注入实现
package com.atguigu.spring.core;
import com.atguigu.spring.core.annotation.Bean;
import com.atguigu.spring.core.annotation.Di;
import java.io.File;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class AnnotationApplicationContext implements ApplicationContext {
//存储bean的容器
private HashMap<Class, Object> beanFactory = new HashMap<>();
private static String rootPath;
@Override
public Object getBean(Class clazz) {
return beanFactory.get(clazz);
}
/**
* 根据包扫描加载bean
* @param basePackage
*/
public AnnotationApplicationContext(String basePackage) {
try {
String packageDirName = basePackage.replaceAll("\\.", "\\\\");
Enumeration<URL> dirs =Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (dirs.hasMoreElements()) {
URL url = dirs.nextElement();
String filePath = URLDecoder.decode(url.getFile(),"utf-8");
rootPath = filePath.substring(0, filePath.length()-packageDirName.length());
loadBean(new File(filePath));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
//依赖注入
loadDi();
}
private void loadBean(File fileParent) {
if (fileParent.isDirectory()) {
File[] childrenFiles = fileParent.listFiles();
if(childrenFiles == null || childrenFiles.length == 0){
return;
}
for (File child : childrenFiles) {
if (child.isDirectory()) {
//如果是个文件夹就继续调用该方法,使用了递归
loadBean(child);
} else {
//通过文件路径转变成全类名,第一步把绝对路径部分去掉
String pathWithClass = child.getAbsolutePath().substring(rootPath.length() - 1);
//选中class文件
if (pathWithClass.contains(".class")) {
// com.xinzhi.dao.UserDao
//去掉.class后缀,并且把 \ 替换成 .
String fullName = pathWithClass.replaceAll("\\\\", ".").replace(".class", "");
try {
Class<?> aClass = Class.forName(fullName);
//把非接口的类实例化放在map中
if(!aClass.isInterface()){
Bean annotation = aClass.getAnnotation(Bean.class);
if(annotation != null){
Object instance = aClass.newInstance();
//判断一下有没有接口
if(aClass.getInterfaces().length > 0) {
//如果有接口把接口的class当成key,实例对象当成value
System.out.println("正在加载【"+ aClass.getInterfaces()[0] +"】,实例对象是:" + instance.getClass().getName());
beanFactory.put(aClass.getInterfaces()[0], instance);
}else{
//如果有接口把自己的class当成key,实例对象当成value
System.out.println("正在加载【"+ aClass.getName() +"】,实例对象是:" + instance.getClass().getName());
beanFactory.put(aClass, instance);
}
}
}
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
}
}
}
private void loadDi() {
for(Map.Entry<Class,Object> entry : beanFactory.entrySet()){
//就是咱们放在容器的对象
Object obj = entry.getValue();
Class<?> aClass = obj.getClass();
Field[] declaredFields = aClass.getDeclaredFields();
for (Field field : declaredFields){
Di annotation = field.getAnnotation(Di.class);
if( annotation != null ){
field.setAccessible(true);
try {
System.out.println("正在给【"+obj.getClass().getName()+"】属性【" + field.getName() + "】注入值【"+ beanFactory.get(field.getType()).getClass().getName() +"】");
field.set(obj,beanFactory.get(field.getType()));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
}
执行第八步:执行成功,依赖注入成功
标签:Autowired,--,Spring,Bean,UserService,import,IOC,public From: https://blog.csdn.net/Sakurapaid/article/details/136848952