一、环境说明

  • Spring Boot 4.1.0,Java 17
  • RuoYi-Vue 3.9.2,前后端分离,后端启动模块为 ruoyi-admin
  • 两个例子分别取自:
    • 例子一:com.ruoyi.mall 商品模块(自己扩展的业务代码),注释式开发最典型的写法
    • 例子二:com.ruoyi.framework.config.CaptchaConfig 验证码模块(框架自带),配置类 + @Bean 的写法

IOC(控制反转)和 DI(依赖注入)其实是一件事的两面:对象的创建权从业务代码手里交给 Spring 容器,这叫 IOC;业务代码需要用什么对象,由容器在启动时塞进来,这叫 DI。判断一段代码有没有用 Spring,看它是不是还在自己 new 依赖对象就够了一半。

下面先看容器是怎么起来的,再看两个例子怎么用注解创建对象、注入对象。


二、Spring 环境的启动

2.1 启动类 RuoYiApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.ruoyi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class RuoYiApplication
{
public static void main(String[] args)
{
SpringApplication.run(RuoYiApplication.class, args);
System.out.println("(♥◠‿◠)ノ゙ 若依启动成功 ლ(´ڡ`ლ)゙");
}
}

@SpringBootApplication 是三个注解的组合,拆开看才能真正理解后面那些注解为什么生效:

组合注解 作用
@SpringBootConfiguration 标记这是一个配置类,内部就是 @Configuration
@EnableAutoConfiguration 按依赖自动装配,比如引了 mybatis-spring-boot-starter 就自动配 SqlSessionFactory
@ComponentScan 扫描启动类所在包及其所有子包,把 @Component、@Service、@Controller 等注解的类注册成 Bean

第三条是关键。启动类放在 com.ruoyi 根包下,所以 com.ruoyi.mall、com.ruoyi.framework、com.ruoyi.system 全在扫描范围内。例子一里商品 Service 只写了一个 @Service 就能被容器管理,靠的就是这个默认扫描规则。如果把启动类挪到 com.ruoyi.admin 这种更深的包,com.ruoyi.mall 就扫不到了,还得手动写 scanBasePackages。

SpringApplication.run() 做的事依次是:创建 ApplicationContext 容器 → 解析配置类 → 扫描并注册所有 BeanDefinition → 实例化所有非懒加载的单例 Bean 并完成依赖注入 → 启动内嵌 Tomcat。第二步到第四步完成后,容器里每个 Bean 的依赖字段都已经有值了,这才是”启动完成”。

exclude = DataSourceAutoConfiguration.class 是 RuoYi 的特殊处理:框架用 Druid 自己配置了多数据源(DruidConfig),所以要先关掉 Spring Boot 的数据源自动装配,不然启动时两个数据源打架,直接报错。

2.2 Mapper 接口为什么也能被注入

1
2
3
4
5
6
7
8
9
10
11
12
package com.ruoyi.framework.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.ruoyi.**.mapper")
public class ApplicationConfig
{
}

@MapperScan 告诉 MyBatis:com.ruoyi 下所有 mapper 包里的接口,都通过 MapperFactoryBean 生成动态代理对象注册进容器,Bean 名是接口名首字母小写。

这一点必须先说清楚,否则后面会看不懂:MallProductMapper 只是个 interface,一行实现都没有,凭什么能被注入?因为它注入进来的不是接口本身,而是 MyBatis 生成的代理对象,真正执行 SQL 的逻辑在代理里(读的是 MallProductMapper.xml)。


三、例子一:@Service 创建对象 + @Autowired 注入(商品模块)

3.1 用注解把自己写的类交给容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.ruoyi.mall.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
// ... 省略 import

@Service
public class MallProductServiceImpl implements IMallProductService
{
@Autowired
private MallProductMapper productMapper;

@Autowired
private MallCategoryMapper categoryMapper;

@Override
public List<MallProduct> selectProductList(MallProduct product)
{
return productMapper.selectProductList(product);
}
// ... 其余增删改查方法省略
}

@Service 的作用有三层:

  1. 语义标识:标明这是业务逻辑层。它本质上是 @Component 的派生注解,Spring 在容器层面把它们一视同仁,写 @Component 一样能跑,只是可读性差一些(同类的还有 @Controller 表示控制层、@Repository 表示持久层)。
  2. 注册 Bean:被 @ComponentScan 扫到之后,Spring 通过反射调用无参构造创建实例,以单例形式放进容器,Bean 名默认取类名首字母小写,即 mallProductServiceImpl。
  3. 支持别名:一个接口有多个实现时,可以写 @Service("xxx") 指定 Bean 名,注入时再按名字区分。

类声明为 implements IMallProductService 不是随手写的。容器里注册的 Bean 类型既包含 MallProductServiceImpl,也包含它实现的接口 IMallProductService,所以外部可以只依赖接口。以后要换成 MallProductServiceImplV2,Controller 一行都不用改。

3.2 注入对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
@RequestMapping("/shop")
public class ShopController extends BaseController
{
@Autowired
private IMallProductService productService;

@Autowired
private IMallCartService cartService;

@Anonymous
@GetMapping("/product/list")
public TableDataInfo productList(MallProduct product)
{
product.setStatus("0");
startPage();
List<MallProduct> list = productService.selectProductList(product);
return getDataTable(list);
}
// ... 其余接口省略
}

@RestController = @Controller + @ResponseBody,而 @Controller 又是 @Component 的派生注解。所以 ShopController 本身也是容器里的一个 Bean,它里面的 @Autowired 字段才会被 Spring 处理——如果这个类没被容器管理,字段上的 @Autowired 就是普通注解,不会起任何作用,运行时 productService 会是 null。

@Autowired 的作用:**按类型(byType)**从容器里找出匹配的 Bean 赋给字段。

  • 这里声明的类型是 IMallProductService,容器里这个类型的 Bean 只有 mallProductServiceImpl 一个,能唯一确定,注入成功。
  • 找不到时会抛 NoSuchBeanDefinitionException,因为 @Autowired 的 required 默认为 true,启动阶段直接失败,不会等到调用时才空指针。
  • 找到多个时会抛 NoUniqueBeanDefinitionException,这时得配合 @Qualifier("bean名") 指定。
  • 字段是 private 也不需要 setter,因为注入是反射完成的,由 AutowiredAnnotationBeanPostProcessor 在 Bean 实例化后统一处理。

3.3 完整调用链

1
2
3
4
5
6
7
8
9
浏览器 / 前端
→ GET /shop/product/list?categoryId=1&keyword=手机
→ ShopController.productList(MallProduct product) // @Autowired 拿到 Service
→ startPage() // PageHelper 把分页参数放进 ThreadLocal
→ productService.selectProductList(product) // 接口调用,实际执行的是实现类
→ MallProductServiceImpl.productMapper.selectProductList(product) // @Autowired 拿到 Mapper 代理
→ MallProductMapper 代理 → MallProductMapper.xml 的 <select id="selectProductList">
→ MySQL
→ getDataTable(list) → 包装成 {total, rows} 的 JSON 返回

这条链路上三次”拿到对象”都不是 new 出来的:

  • Controller 没有 new MallProductServiceImpl(),否则它又得自己去准备两个 Mapper,而 Mapper 是接口,根本 new 不出来;
  • Service 没有 new MallProductMapperImpl(),因为它不存在,代理是 MyBatis 在启动时生成的;
  • 如果不用 Spring,每一层都要自己管理 SqlSession、手动关闭连接,代码会长得没法看。

这就是 IOC/DI 的实际收益:创建和装配的活交给了容器,业务类只管用。


四、例子二:@Configuration + @Bean 创建对象 + @Resource 按名称注入(验证码模块)

4.1 为什么这里不能只用 @Service

验证码用的是 kaptcha 这个第三方 jar 里的 DefaultKaptcha。它的源码在别人包里,既没有 @Component,我们也没法进去加注解,扫描永远扫不到它。这类”第三方类”或者”必须先塞一堆配置才能创建的对象”,就得用 @Configuration + @Bean 手动注册。

而且项目里要两个配置不同的生成器:一个是字符验证码,一个是算术验证码,同一个类型要两个实例,用 @Bean 方法各创建一份最直接。

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
@Configuration
public class CaptchaConfig
{
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean()
{
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
properties.setProperty(KAPTCHA_BORDER, "yes");
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// ... 其余属性省略
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}

@Bean(name = "captchaProducerMath")
public DefaultKaptcha getKaptchaBeanMath()
{
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.framework.config.KaptchaTextCreator");
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
// ... 其余属性省略
defaultKaptcha.setConfig(new Config(properties));
return defaultKaptcha;
}
}

@Configuration:标记配置类,它本身也是 @Component 的派生,所以会被扫描、会被注册进容器。除此之外 Spring 还会用 CGLIB 给这个类生成一个代理子类,保证在配置类内部调用另一个 @Bean 方法时,返回的是容器里那个单例,而不是又执行一遍方法体 new 一个新对象。

@Bean:标在方法上,方法返回值就是要注册进容器的对象,Bean 名默认取方法名,可以用 name 属性改。方法体里 new 的对象、设置的属性,就是最终进入容器的那个实例。

对比例子一:@Service 是容器自己反射 new 出对象,@Bean 是我们自己 new 完交给容器。前者省事,后者灵活,能控制创建过程和参数。

4.2 注入对象

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
@RestController
public class CaptchaController
{
@Resource(name = "captchaProducer")
private Producer captchaProducer;

@Resource(name = "captchaProducerMath")
private Producer captchaProducerMath;

@Autowired
private RedisCache redisCache;

@Autowired
private ISysConfigService configService;

@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException
{
// ...
String captchaType = RuoYiConfig.getCaptchaType(); // 读 yml,这里是 math
if ("math".equals(captchaType))
{
String capText = captchaProducerMath.createText();
// ... 拆分题目和答案
image = captchaProducerMath.createImage(capStr);
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// ... 转成 base64 放进 AjaxResult 返回
}
}

@Resource 是 JSR-250 标准注解(jakarta.annotation.Resource),不是 Spring 自己的,Spring 同样支持它。它和 @Autowired 最大的区别是默认按名称匹配:先拿字段名去容器里找同名的 Bean,找不到再退化成按类型匹配。

这里必须写 name,原因很直接:容器里有 captchaProducer 和 captchaProducerMath 两个都是 Producer 类型的 Bean,如果改成 @Autowired private Producer captchaProducer;,Spring 按类型找到两个,无法判断给哪个,启动直接抛 NoUniqueBeanDefinitionException。同类型多实例的场景只有两种做法:@Resource(name = "...") 或者 @Autowired + @Qualifier("...")。

字段类型声明为 Producer 而不是 DefaultKaptcha,是因为 DefaultKaptcha 实现的接口就是 Producer,Controller 只用到 createText() 和 createImage() 两个方法,依赖接口就够了。@Bean 方法上返回的是具体类,注入时用接口接收,两者不冲突。

4.3 完整调用链

1
2
3
4
5
6
7
8
9
浏览器 / 前端登录页
→ GET /captchaImage
→ CaptchaController.getCode(...)
→ configService.selectCaptchaEnabled() // 查参数配置,@Autowired 注入
→ RuoYiConfig.getCaptchaType() // 读 application.yml 的 captchaType: math
→ captchaProducerMath.createText() // @Resource 按名注入的 Bean
→ captchaProducerMath.createImage(capStr)
→ redisCache.setCacheObject(uuid, code, ...) // @Autowired 注入的 Redis 工具类
→ AjaxResult{uuid, img(base64)} 返回前端

这个 Controller 里 4 个依赖全部是注入来的,没有一处 new:两个验证码生成器来自 @Bean,RedisCache 是 @Component,ISysConfigService 是 @Service。两种创建对象的方式在同一个类里同时出现,正好能对上。


五、两种写法对比

对比项 例子一(商品模块) 例子二(验证码模块)
创建对象的注解 @Service(派生自 @Component) @Configuration + @Bean
谁负责 new Spring 反射调用无参构造 方法体里自己 new,交返回值
注入对象的注解 @Autowired @Resource(name = "...")
匹配方式 按类型 byType 按名称 byName
适用场景 自己写的类,一个接口一个实现 第三方类、需要复杂初始化、同类型要多个实例

六、小结

  1. 创建对象:自己写的类加 @Component / @Service / @Controller / @Repository,靠启动类的 @ComponentScan 扫到;第三方类或者需要手动配参数的对象,用 @Configuration + @Bean 注册。Mapper 接口靠 @MapperScan 生成代理,同样在容器里。
  2. 注入对象:@Autowired 按类型注入,是 Spring 的注解;@Resource 按名称优先,是 JDK 标准注解。容器里同类型有多个 Bean 时,必须按名称指定,否则启动报错。
  3. 注解本身不干活,它们只是”声明”:告诉容器哪些类要创建、依赖往哪个字段塞。真正的创建和装配由 BeanFactory 在启动阶段完成,所以注解写错了,通常不用等到运行时——启动就直接失败。

广告
广告正在加载中...
暂不开放评论,如对本文有任何疑问或建议,请联系apple@mr-wu.top
All Tags