Spring 更简单的读取和存储对象


在 Spring 中要想更简单的存储和读取对象 , 核心是 使用注解 , 所以我们需要通过 Spring 中相关注解 , 来存储和读取 Bean 对象.

1.存储 Bean 对象

之前我们存储 Bean 时 , 需要在 spring-config.xml 中添加一行注释才行:

image-20230504192911762

而现在我们只需一个注解就可以替代之前要写一行配置 , 不过在存储对象之前 , 我们先要来点准备工作.

1. 前置工作: 配置扫描路径

要想将对象成功的存储到 Spring 中 , 我们需要配置一些存储对象的扫描包路径 , 只有被配置的包下的所有类 , 添加了注解才能被正确的识别并保存到 Spring 中.

在 spring-config.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"
       xmlns:content="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 https://www.springframework.org/schema/context/spring-context.xsd">
       <content:component-scan base-package="com.spring.demo"></content:component-scan>
</beans>

其中标红的一行为注册扫描的包:

image-20230504193010859

也就是说即使添加了注解 , 如果不是在配置的扫描包下的类对象 , 是不能被存储到 Spring 中的 , 体现了 Spring 框架在追求方法的同时 , 更追求性能.


2.添加注解存储 Bean 对象

想要将对象存储在 Spring 中 , 有两种注解类型可以实现:

  1. 类注解: @Controller , @Service , @Repository , @Component , @Configuration
  2. 方法注解: @Bean
1. @Controller (控制器存储)

验证用户请求的数据正确性(安保系统)

代码示例:

创建 StudentController 类 , 并添加 @Controller 注解

@Controller //将当前类存储到 Spring 中
public class StudentController {
    public void sayHi(){
        System.out.println("do student controller sayhi()");
    }
}
2. @Service (服务层)

编排和调度具体执行方法(客服中心)

代码示例:

创建 StudentController2类 , 并添加 @Service 注解

@Service
public class StudentController2 {
    public void sayHi(){
        System.out.println("do StudentController2");
    }
}
3. @Repostory(数据持久层)

和数据交互 , 操作数据库 (调用 用户表和日志表) (执行者)

代码示例:

创建 StudentController3类 , 并添加 @Reporstory 注解

@Repository
public class StudentController5 {
    public void sayHi(){
        System.out.println("do StudentController5");
    }
}
4. @Configuration(配置层)

配置项 , 项目中的一些配置.

代码示例:

创建 StudentController4 类 , 并添加 @Configuration注解

@Configuration
public class StudentController4 {
    public void sayHi(){
        System.out.println("do StudentController4");
    }
}
5.@Component(组件)

组件. 工具类

代码示例:

创建 StudentController2 类 , 并添加 @Component 注解

@Component
public class StudentController3 {
    public void sayHi(){
        System.out.println("do StudentController3");
    }
}
6 . 启动类测试:

从容器中取 Bean 对象 , 如果我们在配置文件中有注册标签 , 那么 getBean() 中就可以添加 id 和 class 两个参数 , 确保在容器中找到 Bean. 可是此时我们把配置文件中的标签改为了 component-scan 包路径下的扫描 , 这样就没法通过 id 来访问包了 , 但 Spring 中约定可以 “当类名为大驼峰命名时 , id 为小驼峰. 当类名前两个字符都是大写时 , id 为原类名” , 这个规定后续会在剖析源码中讲解.

public class App {
    public static void main(String[] args) {
        //1.获取 Spring 对象
        ApplicationContext context =
                new ClassPathXmlApplicationContext("spring-config.xml");
        //小驼峰
        StudentController studentController  =
                context.getBean("studentController", StudentController.class);
       //前两个字符小写
        SController sController =
                context.getBean("SController", SController.class);

结果表名五种注解修饰类的 , 调用其方法都可以正确执行 , 且执行结果一致. 那么这五种类究竟有什么区别呢?

public class App {
    public static void main(String[] args) {
        //1.获取 Spring 对象
        ApplicationContext context =
                new ClassPathXmlApplicationContext("spring-config.xml");
        //2.得到 Bean 对象
//        StudentController studentController  =
//                context.getBean("studentController", StudentController.class);
//        SController sController =
//                context.getBean("SController", SController.class);
//        StudentController3 studentController3 =
//                context.getBean("studentController3", StudentController3.class);
//        StudentController4 studentController4 =
//                context.getBean("studentController4", StudentController4.class);
        StudentController5 studentController5 =
                context.getBean("studentController5", StudentController5.class);

        //3. 使用 Bean 对象
        studentController5.sayHi();
    }
}

3. 常见问题

1. 和 component-scan 可以同时存在吗?

创建 UserService 类

public class UserService {
    public void sayHi(){
        System.out.println("do UserService");
    }
}

在 spring 配置文件中同时添加这两种: (bean 的路径与 component 的包路径不一样)

img/image-20230411082622660.png  0 → 100644

结果显示可以执行 , 说明 可以作为额外补充添加一些 , 不适合放在 component-scan 包路径下的类.

public class App {
    public static void main(String[] args) {
        //1.获取 Spring 对象
        ApplicationContext context =
                new ClassPathXmlApplicationContext("spring-config.xml");
        //2.得到 Bean 对象
        UserService userService =
               context.getBean("userService", UserService.class);

        //3. 使用 Bean 对象
        userService.sayHi();
    }
}
2. 五大类注解可以不在 component-scan 包下吗?
public class App {
    public static void main(String[] args) {
        //1.获取 Spring 对象
        ApplicationContext context =
                new ClassPathXmlApplicationContext("spring-config.xml");
        //2.得到 Bean 对象
        StudentService service =
                context.getBean("studentService", StudentService.class);

        //3. 使用 Bean 对象
        service.sayHi();
    }
}

结果显然不可以 , 未找到该类. 因此五大类注解必须在 component-scan 包下

img/image-20230411083549583.png  0 → 100644

3. 在 component-scan 的路径包下,不加五大类注解可以存储到 Spring 中吗?
//@Controller //将当前类存储到 Spring 中
public class StudentController {
    public void sayHi(){
        System.out.println("do student controller sayhi()");
    }
}

结果依旧是找不到bean 对象.

img/image-20230411084011440.png  0 → 100644

4. 在 component-scan 下的子包类中 , 添加注解可以存储到 Spring 中吗?

在 component-scan 包路径下创建子包 “java” , 该包中创建类 UserController

img/image-20230411084303945.png  0 → 100644

@Controller
public class UserController {
    public void sayHi(){
        System.out.println("do UserController");
    }
}

结果显示可以正常执行 , 说明在 component-scan 下的所有子包下的类只要添加了五大类注解 , 同样能存储到 Spring 中.

5.不同包下的同名类 , 可以通过注解读取吗?

不同包下创建两个相同的 UserController ,

image-20230411173613794

@Controller
public class UserController {
    public void sayHi(){
        System.out.println("do UserController -> com.spring.demo.java");
    }
}
@Controller
public class UserController {
    public void sayHi(){
        System.out.println("do UserController -> com.spring.demo");
    }
}

报错结果为"Bean 对象定义冲突" , 那么如何解决该问题呢?

image-20230411174024867

通过查看 Controller 的源码 ,我们可以给重名的类传一个别名.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

@Controller() 中传一个别名参数(字符串类型)

@Controller("UserController2")
public class UserController {
    public void sayHi(){
        System.out.println("do UserController -> com.spring.demo.java");
    }
}

4. 为什么需要五大类注解

通过上述代码 , 我们发现五大类注解都可以做到将当前类存储到 Spring 中 , 那么为什么需要五大类注解?

1.剖析源码

查看五大类注解的源码之后 , 可以发现一个共同点那就是 , 其他的四类注解都是继承自 @Component , 可以认为 @Controller @Service @Repository @Configuration 都是 @Component 的子类 , 都是针对 @Component 的扩展.

例如: 不同的省市甚至是县区为什么要有自己单独的车牌号? 如果只区分省不是更方面吗? 其实这样做的目的就是可以更直观的标识一辆车的归属地.

为什么需要这么多的注解 , 原因就是让程序员看到注解能够望文生义 , 清楚的知道当前的类的作用.

  • @Controller: 控制层 , 验证参数正确性 , 与前端交互.
  • @Service: 服务层 , 编排和调度程序执行.
  • @Repository: 数据持久层 , 直接操作数据库.
  • @Configuration: 存放配置信息.
2. JavaEE 标准分层(至少三层)
  1. 控制层
  2. 服务层
  3. 数据持久层

3. 阿里巴巴 java 开发手册中标准分层:


5. Bean 命名规则

通过上面示例 , 我们可以看出 , 通常我们 Bean 使用的都是标准的大驼峰命名 , 而读取时首字母小写就可以读取 , 特殊情况是 , 当前两个字符都是大写字母 ,那么就用原字符串读取。

那么为什么会有这样的规则呢? 我们可以查看源码 , 在全局搜索中找到注解名字生成。

最终我们找到了生成名称的源代码 , 发现与我们之前的结论一致:

public static String decapitalize(String name) {
    if (name == null || name.length() == 0) {
        return name;
    }
    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
                    Character.isUpperCase(name.charAt(0))){
        return name;
    }
    char[] chars = name.toCharArray();
    chars[0] = Character.toLowerCase(chars[0]);
    return new String(chars);
}

我们可以测试一下 Introspector 方法:

public class BeanNameTest {
    public static void main(String[] args) {
        String className = "UserClass";
        String className2 = "UClass";
        System.out.println("UserClass ->" + Introspector.decapitalize(className));
        System.out.println("UClass ->" + Introspector.decapitalize(className2));
    }
}

发现结果与推断一致:


6. @Bean 方法注解

@Bean 注解就是将当前方法的返回对象 , 存储到 Spring 中.

类注解是添加到某个类上的 , 而方法注解是放到某个方法上的.

1. 实体类是什么?

通俗来讲 , 实体类就是一个有 Get 和 Set 方法的类 , 通常和数据持久层联系在一起. 因此实体类就是一个载体 , 通常和一张数据库表联系起来 , 实体类中的字段和数据库表中的属性一一对应.

2. 实体类的命名规则
  • 基本对象(数据库中的一张表); 表名: Userinfo

  • 扩展对象: UserinfoVO(view object)

3. 使用方法注解将 Bean 存储到 Spring

创建一个实体类:

public class User {
    private Integer userId;//属性=字段
    private String username;
    private String password;
    private Integer age;

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

创建一个 UserBeans 类 , 用于将实体类对象 , 存入 Spring 中.

public class UserBeans {
    @Bean
    public User user1(){
        User user = new User();
        user.setUserId(1);
        user.setUsername("张三");
        user.setPassword("123456");
        user.setAge(18);
        return user;
    }
}

启动类中 , 按照之前 Bean 命名规则 , 获取 Bean 对象 , 并使用其方法.

public class App {
    public static void main(String[] args) {
        //1.获取 Spring 容器
        ApplicationContext context =
                new ClassPathXmlApplicationContext("spring-config.xml");
        //2.得到 Bean 对象
        User user = context.getBean( "user", User.class);
        System.out.println(user.getUserId());
    }
}

执行结果:

报错原因:

  1. @Bean 命名规则和五大类的命名规则不同. @Bean 命名规则默认情况下 , 存储对象的名称 = 方法名.
  2. @Bean 注解必须配合五大类注解一起使用(基于 Spring 对性能的追求 , 快速定位到类).

只有将以上两个原因解决才能防止报错.

4. @Bean 重命名:

源码中我们可以看出 , 可以给方法起多个名字 , 而且参数无论是 name 还是 value , 都是可以的 , 但 @Bean 方法中更建议 name.

public @interface Bean {
    @AliasFor("name")
    String[] value() default {};

    @AliasFor("value")
    String[] name() default {};
@Bean 重命名之后 , 默认的使用方法名获取对象的方式就不能使用了.
@Controller
public class UserBeans {
    @Bean({"User", "U1"})
    public User user1(){
        User user = new User();
        user.setUserId(1);
        user.setUsername("张三");
        user.setPassword("123456");
        user.setAge(18);
        return user;
    }
}
public class App {
    public static void main(String[] args) {
        //1.获取 Spring 容器
        ApplicationContext context =
                new ClassPathXmlApplicationContext("spring-config.xml");
        //2.得到 Bean 对象
        User user = context.getBean( "user1", User.class);
        System.out.println(user.getUserId());
    }
}

image-20230412201111049

Spring 容器中允许将同一个类型的对象 , 在容器中存放多份.
@Controller
public class UserBeans {
    @Bean({"User", "U1"})
    public User user1(){
        User user = new User();
        user.setUserId(1);
        user.setUsername("张三");
        user.setPassword("123456");
        user.setAge(18);
        return user;
    }
    public User user2(){
        User user = new User();
        user.setUserId(1);
        user.setUsername("李四");
        user.setPassword("123456");
        user.setAge(18);
        return user;
    }
}
同名同类型方法在不同类中可以获取到吗?

类似于 HashMap 如果存储相同的值 , 后来值的会将之前的值覆盖.

同名方法也会被后面的方法覆盖 , 但 @Order() 可以控制注入的顺序 , 值越小优先级越高.

明显李四会先执行.

@Controller
@Order(20)
public class UserBeans {
    @Bean({"User", "U1"})
    public User user1(){
        User user = new User();
        user.setUserId(1);
        user.setUsername("张三");
        user.setPassword("123456");
        user.setAge(18);
        return user;
    }
}

@Controller
@Order(2)
public class UserBeans2 {
    public class UserBeans {
        @Bean({"User", "U1"})
        public User user1() {
            User user = new User();
            user.setUserId(1);
            user.setUsername("李四");
            user.setPassword("123456");
            user.setAge(18);
            return user;
        }
    }
}


2. 获取 Bean 对象(对象注入)

对象注入: 更简单的读取 Bean (从 Spring 容器中读取某个对象 , 放到当前类中)

Spring 中常见的注入方式:

  1. 属性注入(Field Injection)
  2. Setter 注入(Setter Injection)
  3. 构造方法注入(Constructor Injection)

1. 属性注入

属性注入因其简单的特性 , 是日常开发中使用最多的一种方式.

@Autowired 注解 , 相当于是从 Spring 容器中读取到对象 , 交给当前的变量. 不必像启动类那样 , 先得到 Spring 容器 , 再从容器中获取 Bean 对象.

@Controller
public class UserController {
    @Autowired //注入对象 (更简单的从 spring 容器中读取到对象)
    private UserService userService;

    public void sayHi(){
        System.out.println("do UserController -> com.spring.demo");

        userService.sayHi();
    }
}

Tips: 不可以在启动类中使用 @Autowired 获取对象 , 因为 main 方法属于静态方法 , 静态类加载顺序早于 Spring.

优点: 很明显就是简单

缺点:

  1. 没法注入 final 修饰的对象.(JavaSE 语法限制)
  2. 兼容性不强 , 只适用于 IoC 容器 , 非 IoC 项目直接抛 NULLPOINTEXCEPTION.
  3. 有风险 , 因为写法简单 , 所以可能同时注入多个对象 , 会出现违反单一设计原则的可能性.

2. Setter 注入

@Controller
public class UserController {
    private UserService userService;
    @Autowired //注入对象 (更简单的从 spring 容器中读取到对象)
    public void setUserService(UserService userService) {//Spring 赋值
        this.userService = userService;
    }

    public void sayHi() {
        System.out.println("do UserController -> com.spring.demo");

        userService.sayHi();
    }
}

优点: 每次只传一个对象 , 符合单一设计原则.

缺点:

  1. 无法注入一个 final 对象.
  2. 使用 Setter 注入的对象 , 可能会被修改.
@Controller
public class UserController {
    @Autowired //注入对象 (更简单的从 spring 容器中读取到对象)
    private UserService userService;

    public void setUserService(UserService userService) {//Spring 赋值
        this.userService = userService;
    }

    public void sayHi() {
        System.out.println("do UserController -> com.spring.demo");
        UserController controller = new UserController();
      //故意修改成 null
        controller.setUserService(null);
        userService.sayHi();
    }
}

3. 构造方法注入(Spring 官方推荐写法)

@Controller
public class UserController {
    private UserService userService;
    @Autowired //注入对象 (更简单的从 spring 容器中读取到对象)
    public UserController(UserService userService) {
        this.userService = userService;
    }

    public void sayHi() {
        System.out.println("do UserController -> com.spring.demo");

        userService.sayHi();
    }
}

构造方法注入 如果只有一条构造方法 不写 @Autowired 照样可以执行. 但如果一个类中有多个构造方法 @Autowried 不可省略.

优点:

  1. 可注入 final 对象 (Java 中规定 , 在 Java 中 , 被 final 修饰的对象 , 必须满足二者之一 , 要么直接赋值 , 要么在构造方法中赋值)
  2. 注入对象不会被修改.(构造方法只能在类加载时执行一次)
  3. 构造方法注入可以保证注入对象完全被初始化.(构造方法在对象创建之前就已执行完毕 , 因此被注入对象在使用前会完全初始化)
  4. 通用性和兼容性更强. (即使不在容器中也能注入)

综上:

依赖注入常见方式有三种 , 属性注入 , Setter 注入 , 构造方法注入. 其中属性注入最简单高效 , 但可移植性不强. Spring 官方推荐 构造方法注入 , 它可以注入不可变对象 , 且可移植性更强. 如果想注入可变对象 , 应使用 Setter 注入.

4. @Resource: 另一种注入方式

通过源码观察@Resource 和 @Autowired 二者区别:

1.@Autowired 来自 Spring 框架 , @Resource 来自 jdk

import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Resource;

2.@Resource 支持多种参数.

@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {
    /**
     * The JNDI name of the resource.  For field annotations,
     * the default is the field name.  For method annotations,
     * the default is the JavaBeans property name corresponding
     * to the method.  For class annotations, there is no default
     * and this must be specified.
     */
    String name() default "";

    /**
     * The name of the resource that the reference points to. It can
     * link to any compatible resource using the global JNDI names.
     *
     * @since Common Annotations 1.1
     */

    String lookup() default "";

    /**
     * The Java type of the resource.  For field annotations,
     * the default is the type of the field.  For method annotations,
     * the default is the type of the JavaBeans property.
     * For class annotations, there is no default and this must be
     * specified.
     */
    Class<?> type() default java.lang.Object.class;

    /**
     * The two possible authentication types for a resource.
     */
    enum AuthenticationType {
            CONTAINER,
            APPLICATION
    }

@Autowired 支持参数很少.

public @interface Autowired {
    boolean required() default true;
}

3.@Resource 是 jdk 提供的一种注解 , 通过代码测试发现其不可用于构造方法注入.

在这里插入图片描述

因此 , @Autowired 支持更多的注入类型 , @Resource 支持更多的参数类型 , 二者能力五五开.

综上二者区别如下:

  1. 来源不同.
  2. 支持参数种类不同.
  3. 注入的支持类型不同.

Tips: 在 Spring 容器中找 Bean 有两种方式:

  1. 根据类型查找
  2. 根据名称查询

@Autowired 先根据类型去找 , 再根据名称查找.

@Resource 先根据名称去查 , 后根据类名去查.

  • 11
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Node_Hao

您的支持是我创作的不懈动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值