创建并运行一个 Spring项目(下)

简介: 创建并运行一个 Spring项目(下)

第二个 Spring 项目



在上面的第一个 Spring 项目中,我们是通过将 bean 对象通过配置文件注册到 spring 中,之后再从配置文件中取出对象来,这一部分需要我们手动去输入 " id " 和 " class " 属性。


而在我们即将实现的第二个 Spring 项目中,我们是完全通过注解的方式进行了,这样一来,就可以更简单的存储对象和读取对象了。


如果说第一个 Spring 项目是一个手动挡的汽车,那么第二个 Spring 项目就是一个自动挡的汽车,因为实际上,一个注解就只有一行代码而已,通过注解这样的方式,很多工作并不需要我们自己动手去做,而是交给 spring 框架去做。


1. 搭建项目环境


(1) 创建一个 maven 项目

(2) 添加 spring 框架支持 ( spring-context + spring-beans )

(3) 配置国内源

(4) 创一个启动类并添加 main 方法

(5) 配置 " spring-config.xml " 文件

前 4 步,在第一个 spring 项目中,已经体现出来了。现在,我们来实现第 5 步。


我们在 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=" ">
    </content:component-scan>
</beans>


其中有一个 " base-package " 这样的属性十分重要!它表明了我们所有存放到 spring 中的 bean 的根路径。


如下图所示,只要我们涉及 spring 的类,就必须放在当前的 " beans " 包下,或者重新创建一个包 " abc " ,把类放在 " abc " 包下,但是 " abc " 包也应该在 " beans " 包下。也就是说," beans " 目录可以是 spring 类的父级目录,也可以是 " 爷爷目录 ",依然可以是 " 祖先目录 " …但不能是子孙目录。


2fc622a0060646e79a676185a2d2de04.png


2. 将 bean 对象存储到 spring 容器中


要想简单地将对象存储在 Spring 中,有如下两种注解类型可以实现。

必须明确,两种注解都是 spring 框架提供的,也就是从我们之前从 maven 仓库引入的依赖所提供的。两种注解实际上在底层就是对应着各自的 " .class " 文件。


(1) 类注解:


@Controller、@Service、@Repository、@Component、@Configuration.


(2) 方法注解:


@Bean.


(1) 类注解


创建一个 UserController 类,作为 bean 对象。


8bb48612de6740ff9ffacf6e8e48f3ba.png


在 UserController 类的上方,添加一个 " @Controller " 注解。


@Controller
public class UserController {
    public void hello() {
        System.out.println("你好,Controller");
    }
}


在启动类中,我们通过 " getBean " 方法进行测试,结果发现并没有问题。


public class Run {
    public static void main(String[] args) {
        // 1. 得到 spring 上下文对象
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        // 2. 根据上下文对象提供的方法获取到 bean
        UserController userController = context.getBean("userController", UserController.class);
        // 3. 使用
        userController.hello();
    }
}


7183c255e0bd46999656ae21e1ea5367.png


注意


注意1 五个类注解之间的联系


1. 在上面的程序中,我只测试了一个 " @Controller " 注解,如果我们使用其他四个类注解,在当前的打印功能上,它们其实都是一样的,我就不额外展示了。


但是我们应该着重理解这五个类注解,到底都是什么意思?能用来干什么?它们之间有什么区别和联系?


如下图所示,在一个企业项目中,最少会有下面的四层,或许会比四层多,但一定不会比下面的四层少。这四层,我们利用这四个注解来进行解释。


08caa9db2796439990e11e1a2eb4dc68.png

@Configuration:配置层


配置层用于存放当前项目的所有配置,其实在我们日常学习的时候,感受不到配置文件的分层问题,因为,平时我们可能不做项目,就将一个项目的所有文件放在一个目录下。而在实际的工作开发中,一个项目,可能有成千上万个文件,多数情况下,这些目录并不仅仅只有你的代码,也有你同事的代码。而配置层就可以通过一些配置类,来管理这些文件。当我们需要对当前项目的一些配置进行修改、维护的时候,只需要利用这一层来实现即可。


@Controller:控制层


控制层主要用作前端参数校验。比方说,现在我们通过前端登录一个网站,那么我们至少需要输入账号和密码这两个参数供后端验证,那么此时,就可以利用控制层来实现登录逻辑,验证成功后,才能到达下一层 " @Service ",验证失败,直接返回错误给前端用户。


举个例子:这就和当下疫情一样,当你的健康码是绿码的时候,你才能通行,否则,就可能需要马上去做核酸…那么控制层的作用就体现出来了,它就像安检关卡一样。


@Service:服务层


服务层主要实现了数据的组装和接口调用。


@Repository:数据持久层 ( DAO 层 )


数据持久层直接与数据库打交道,在这一层,可以实现数据库的增删查改。


@Component:上面四个注解的 " 父亲 "


查看上面四个注解的源码,就可以发现,它们四个都有 " @Component ",说明它们本身就是基于 " @Component " 实现的,换句话说,它们四个就是 " @Component " 的 " 孩子 "。


cd84f2d6300940e8a71fb05d47b06919.png


总结:一个 spring 项目为什么要分层呢?


举个例子,当我们去某家公司面试,公司会有 Java 面试的地方、前端面试的地方、C++ 面试的地方,这些地方我们可以理解为【配置层】分配好的区域。假设我们去面试 Java 后端工程师,首先,我们需要通过安检,出示我们的健康码,如果是绿码才能正常进入公司,此时这个安检就可以理解为【控制层】。当我们通过了安检,可能就会有服务人员告诉你,Java 面试在某某楼、某某房间,你需要通过他给你的提示路线,才能找到具体的面试地点,此时服务人员就可以理解为【服务层】。只有你到达了最终目的地,面试官才能面试你,那么最后一步,我们才能视为【持久层】。


再回到上面的问题,在实际项目中,我们不可能通过一次执行,就直接对数据库进行操作,这不符合权限、也不安全,更不科学。


注意2 BeanName 的命名规则


打开下面的一个类文件,查看底层类的源码,滑动到页面最后,打开 " decapitalize " 方法,再看其源码。


9ca8ef8e7c15400481f490243711654e.png


这一段源码核心地方在于下面的红框部分,如果第一个字母和第二个字母都是大写,那么直接返回;否则,就会将第一个字母变成小写,再返回。 现在,我们使用 " getBean " 方法,传入的参数就有依据了。


此外,值得注意的是,这是 JDK 提供的标准。


a6ed67beedbd41348746a74f2bcdda7f.png


在下面,对源码进行测试,最终发现与我们理解的是一致的。


public class test {
    public static void main(String[] args) {
        String str1 = "UserController";
        String str2 = "APIController";
        System.out.println(Introspector.decapitalize(str1));
        System.out.println(Introspector.decapitalize(str2));
    }
}


a826b79e229b4e60a83cb0f5af76cd38.png


综上所述,我们在今后使用 " getBean " 方法的时候,就只应该注意我们自己定义类的前两个字母即可,如果前两个字母都是大写,传入的就是原类名;反之,就将第一个字母变成小写即可。


UserController userController = context.getBean("userController", UserController.class);
APIController apiController = context.getBean("APIController", APIController.class);


(2) 方法注解


@Controller
public class UserBean {
    @Bean
    public User user1() {
        User user = new User();
        user.id = 1;
        user.name = "露丝";
        return user;
    }
    @Bean(name = {"user", "userinfo"})
    public User user2() {
        User user = new User();
        user.id = 2;
        user.name = "杰克";
        return user;
    }
}


注意


(1) " @Bean " 需要和五大类注解配合使用,才能生效。

(2) " @Bean " 只能放在方法上面,将当前方法返回的对象,存储到 spring 容器中。


(3) 使用 " @Bean " 存储到 spring 容器后,再从容器中取出来 bean 对象时,对传入 " getBean " 方法的参数,也有讲究。如果当前没有重命名,就传入 " @Bean " 注释的方法名;如果对 " @Bean " 重命名了,那么就只能使用新的名字,原来的方法名失效。重命名的规则很简单,在 " @Bean " 后面加上 name 数组即可,可以重新使用多个名字。


对上面的程序进行验证:


User user = context.getBean("user1", User.class); // true
User user = context.getBean("user2", User.class); // false
User user = context.getBean("user", User.class); // true
User user = context.getBean("userinfo", User.class); // true


3. 对象装配来获取 bean 对象


以往我们是通过 new 一个对象的传统写法,来从一个类中拿到外部类的对象。现在,我们使用对象装配的方式将一个类的对象放到另一个类中。对象装配也叫做对象注入,实际上就是将 bean 对象取出来放到某个类中,接着就可以在这个类中,直接使用 bean 对象了。


对象注入的实现方法有下面三种:


(1) 属性注入

(2) 构造方法注入

(3) Setter 注入


(1) 属性注入 bean 对象 ( 字段注入 )


我们往 " UserController " 类中,注入 " UserService " 类的对象,之后,我们就可以通过 " UserController " 类拿到 " UserService " 类的字段、方法等数据了。下面的 Run 启动类,只是用来测试一下,对象装配是否成功。


2ff65761a8ea41c29e0578dd3f067fa8.png


注意


以往我们是通过 new 一个对象的传统写法,来从一个类中拿到外部类的对象。


UserService userService = new UserService();


现在,我们是通过属性注入的方式,拿到外部类的 bean 对象。

但使用属性注入的前提是,我们需要保证被注入的对象是一个 bean 对象,也就是说,它得包含五大类注解才行。

@Autowired
private UserService userService;


这就像:一个容器里面装了一个大盒子,这个大盒子又装了一个小盒子。

当我们从 Spring 容器中,将 UserController 这个大盒子取出来的时候,UserService 这个小盒子,也会被取出来。


45e27689d2d94d50ad9eb394a7957e5d.png


(2) 构造方法注入 bean 对象


我们往 " UserController2 " 类中,注入 " UserService " 类的对象,之后,我们就可以通过 " UserController2 " 类拿到 " UserService " 类的字段、方法等数据了。下面的 Run 启动类,只是用来测试一下,对象装配是否成功。


267bf58f9e504f7dbc5d3ed12b22e51e.png


注意


注意1


下面这行代码,并不是属性注入,因为它上面并没有 " @Autowired " 这样的注解,所以很明显,它就是是一个值为 null 的一个字段而已。


private UserService userService;


接着,由于构造方法在外部 new 的时候,第一时间就能够使用,也就是说,构造方法的优先级很高。所以,通过构造方法注入,就是将原先值为 null 的 userService 字段,赋值了新的对象,这就好像激活了 UserService 类一样。


@Autowired
public UserController2(UserService userService) {
    this.userService = userService;
}


注意2


利用构造方法注入 bean 对象的时候,如果被注入的类出现了多个构造方法,我们只能使用一个构造方法来进行 bean 对象的注入,也就是只将其中一个构造方法设置为注解 " @Autowired "。


(3) Setter 注入 bean 对象


我们往 " UserController3 " 类中,注入 " UserService " 类的对象,之后,我们就可以通过 " UserController3 " 类拿到 " UserService " 类的字段、方法等数据了。下面的 Run 启动类,只是用来测试一下,对象装配是否成功。


6232b189ecff49df921610aea85b5fe4.png


注意


Setter 方法注入和构造方法注入的思想基本相同,这里我就不展开介绍了。但是它们之间也略有不同,因为构造方法可能根据参数不同,所以会有多个构造方法,然而,Setter 方法则不同,它只有唯一一个,它只针对于某个字段进行设置。


经典面试题1


对象注入的方式有哪些?

属性注入、构造方法注入、Setter 注入三者的区别?


答:


① 属性注入:写法最简单,但通用性较差,它只能用于 IOC 容器,如果是用于非 IOC 容器下,就会出现空指针异常。


② 构造方法注入:通用性好,此外,由于构造方法是一个类的优先级最高的成员,所以使用构造方法注入,就能够确保注入对象不会出差错,这也是现阶段官方推荐的写法。但是,由于构造方法可能根据参数不同,所以会有多个构造方法,如果多个构造方法用到了同样的对象注入,那么程序就会显得比较冗余。


③ Setter 注入:官方早期的推荐写法,但通用性依旧较构造方法差,因为在 Java 中的 Setter 方法,放在其他语言之中,可能就行不通了。


④ 官方推荐的写法是一种理论,我们在实际开发中,只要符合对应场景,用到最多的还是属性注入,因为理论和实践还是不一样的,实践以高效、实用为主。


经典面试题2


在进行对象注入时,除了可以使用 " @Autowired " 注解之外,我们还可以使用 " @Resource " 进行注入。


对象注入可以使用的哪两种注解 / 哪两种关键字?


① 出身不同:" @Autowired " 来自于 Spring 框架,而 " @Resource " 来自于 JDK.


② 用法不同:注解 " @Autowired " 支持属性注入、构造方法注入 和 Setter 注入,然而,注解 " @Resource " 不支持构造方法注入。


③ 支持的参数不同:" @Autowired " 只支持 required 参数设置,而 " @Resource " 支持更多的参数设置,比如 name、type 设置。


同一个类型的对象注入多次的问题

创建一个 User 类,作为待存储的类。


public class User{
    public int id;
    public String name;
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}


在 UserBean 类下,通过注解 " @Bean " ,将 User 类的对象往 spring 容器中注入了两次,一个 beanName 为 " user1 ",一个 beanName 为 " user2 ".


@Controller
public class UserBean {
    @Bean
    public User user1() {
        User user = new User();
        user.id = 1;
        user.name = "露丝";
        return user;
    }
    @Bean
    public User user2() {
        User user = new User();
        user.id = 2;
        user.name = "杰克";
        return user;
    }
}


创建一个 UserController4 类,进行 User 类的对象注入。


@Controller
public class UserController4 {
    @Autowired
    private User user;
    public void hello() {
        System.out.println("你好," + user.name);
    }
}


利用 Run 启动类,进行测试。


public class Run {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        UserController4 userController4 = context.getBean("userController4", UserController4.class);
        userController4.hello();
    }
}


通过 Run 类测试,我们发现,如果我们直接注入一个 beanName 为 " user " 的一个对象,就会出现如下错误。这其实很好理解,因为当初存入的对象,一个名为 " user1 " ,一个名为 " user2 ",现在,我们却让 spring 容器为我们找一个名称不匹配的对象,自然是找不到的,因为容器中 User 类的对象不唯一。


efadf51573f84e6290ac1fc710065467.png


备注: 如果在 Spring 容器中,同一个类型的对象只有一个,那么,就算我们对象注入的名字与那唯一对象的名字不匹配,也不影响 spring 能够找出来。然而,同一个类型,有多个对象,就算 spring 再智能,它也不知道你究竟想要哪个了。


解决方案


综上所述,我们就知道了,往 spring 中存入 " bean 对象 " 的时候,没有发生问题,但是,从 spring 中取出 " bean 对象 " 的时候,却发生了问题。那么,我们只需要修改【对象注入】的弊端即可。


方案1 精确描述 beanName

如果你想要 user1,属性注入的时候就直接注入 user1;

同样地,如果你想要 user2,就直接注入 user2.


@Autowired
private User user1;
public void hello() {
    System.out.println("你好," + user1.name);
}


测试结果:


dc54b0c8eefa45b5a3fea59896304e16.png


@Autowired
private User user2;
public void hello() {
    System.out.println("你好," + user2.name);
}

6748eaffae394a4882a5f907fb6ea950.png


方案2 通过 " @Resource " 设置 【name 参数】 来重命名


之前,我们提到 " @Autowired " 和 " @Resource ",发现 " @Resource " 在提供的参数方面,更胜一筹。所以我们直接利用其 【name 参数】进行重命名设置即可,这样,就相当于告诉了 spring 容器,你到底取的是哪个对象了。


@Resource(name = "user1")
private User user;
public void hello() {
    System.out.println("你好," + user.name);
}
// 你好,露丝
@Resource(name = "user2")
private User user;
public void hello() {
    System.out.println("你好," + user.name);
}
// 你好,杰克


方案3 通过 " @Autowired " + " @Qualifier " 的方式来限定名称


实际上, " @Qualifier " 注解只有一个参数,就是 value,可写可不写。但我们最好还是加上 value,因为这样更准确,此外,如果有一天,spring 框架升级了 " @Qualifier " 注解,让它不止一个参数,那么我们也能很好地预防意外。


@Autowired
@Qualifier(value = "user1")
private User user;
public void hello() {
    System.out.println("你好," + user.name);
}
// 你好,露丝
@Autowired
@Qualifier(value = "user2")
private User user;
public void hello() {
    System.out.println("你好," + user.name);
}
// 你好,杰克


总结


之前,我们说第一个 spring 项目需要依赖 " xml " 配置文件,来进行存储和取出 bean 对象的,这就像开手动挡的车一样。之后,在第二个 spring 项目中,我们又通过注解的方式,来进行存储和读取 bean 对象,这时候,就像开自动挡的车子一样了。


而实际上,这就是一个随着时代发展,技术升级的过程,后面还有更现代的框架可供使用。但是,我们需要记住一点,在当前的 spring 框架中,核心功能就是存储和读取 bean 对象,所以,项目不管以什么方式实现,牢记核心才是至上的。

目录
相关文章
|
23天前
|
设计模式 前端开发 Java
Spring MVC——项目创建和建立请求连接
MVC是一种软件架构设计模式,将应用分为模型、视图和控制器三部分。Spring MVC是基于MVC模式的Web框架,通过`@RequestMapping`等注解实现URL路由映射,支持GET和POST请求,并可传递参数。创建Spring MVC项目与Spring Boot类似,使用`@RestController`注解标记控制器类。
31 1
Spring MVC——项目创建和建立请求连接
|
23天前
|
Java 关系型数据库 MySQL
Maven——创建 Spring Boot项目
Maven 是一个项目管理工具,通过配置 `pom.xml` 文件自动获取所需的 jar 包,简化了项目的构建和管理过程。其核心功能包括项目构建和依赖管理,支持创建、编译、测试、打包和发布项目。Maven 仓库分为本地仓库和远程仓库,远程仓库包括中央仓库、私服和其他公共库。此外,文档还介绍了如何创建第一个 SpringBoot 项目并实现简单的 HTTP 请求响应。
100 1
Maven——创建 Spring Boot项目
|
26天前
|
Java 关系型数据库 MySQL
如何使用 maven 创建一个 Spring Boot项目
Maven 是一个强大的项目管理工具,通过配置 `pom.xml` 文件自动获取所需的 jar 包,提高开发效率。其核心功能包括项目构建和依赖管理。项目构建支持编译、测试、打包和发布等流程,而依赖管理则通过中央仓库、本地仓库和私有服务器获取和管理项目依赖。示例中展示了如何创建第一个 SpringBoot 项目并实现简单接口。
21 1
如何使用 maven 创建一个 Spring Boot项目
|
1月前
|
Java 应用服务中间件 Android开发
Eclipse创建Spring项目
本文介绍了在Eclipse中创建Spring项目的步骤,包括如何配置Tomcat服务器、创建项目、部署项目到Tomcat以及添加Spring框架所需的JAR包。
48 1
Eclipse创建Spring项目
|
23天前
|
Java Apache Maven
Java/Spring项目的包开头为什么是com?
本文介绍了 Maven 项目的初始结构,并详细解释了 Java 包命名惯例中的域名反转规则。通过域名反转(如 `com.example`),可以确保包名的唯一性,避免命名冲突,提高代码的可读性和逻辑分层。文章还讨论了域名反转的好处,包括避免命名冲突、全球唯一性、提高代码可读性和逻辑分层。最后,作者提出了一个关于包名的问题,引发读者思考。
Java/Spring项目的包开头为什么是com?
|
1月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
52 2
|
1月前
|
XML Java 应用服务中间件
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
137 2
|
4月前
|
Java 测试技术 数据库
Spring Boot中的项目属性配置
本节课主要讲解了 Spring Boot 中如何在业务代码中读取相关配置,包括单一配置和多个配置项,在微服务中,这种情况非常常见,往往会有很多其他微服务需要调用,所以封装一个配置类来接收这些配置是个很好的处理方式。除此之外,例如数据库相关的连接参数等等,也可以放到一个配置类中,其他遇到类似的场景,都可以这么处理。最后介绍了开发环境和生产环境配置的快速切换方式,省去了项目部署时,诸多配置信息的修改。
|
Java 关系型数据库 MySQL
Spring Boot项目属性配置
Spring Boot项目属性配置
189 0
|
Java 数据库
【SpringBoot】项目属性配置
【SpringBoot】项目属性配置
130 0