创建并运行一个 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 对象,所以,项目不管以什么方式实现,牢记核心才是至上的。

目录
相关文章
|
30天前
|
XML JSON Java
使用IDEA+Maven搭建整合一个Struts2+Spring4+Hibernate4项目,混合使用传统Xml与@注解,返回JSP视图或JSON数据,快来给你的SSH老项目翻新一下吧
本文介绍了如何使用IntelliJ IDEA和Maven搭建一个整合了Struts2、Spring4、Hibernate4的J2EE项目,并配置了项目目录结构、web.xml、welcome.jsp以及多个JSP页面,用于刷新和学习传统的SSH框架。
30 0
使用IDEA+Maven搭建整合一个Struts2+Spring4+Hibernate4项目,混合使用传统Xml与@注解,返回JSP视图或JSON数据,快来给你的SSH老项目翻新一下吧
|
30天前
|
前端开发 JavaScript Java
spring boot+vue前后端项目的分离(我的第一个前后端分离项目)
该博客文章介绍了作者构建的第一个前后端分离项目,使用Spring Boot和Vue技术栈,详细说明了前端Vue项目的搭建、后端Spring Boot项目的构建过程,包括依赖配置、数据库连接、服务层、数据访问层以及解决跨域问题的配置,并展示了项目的测试结果。
spring boot+vue前后端项目的分离(我的第一个前后端分离项目)
|
1月前
|
IDE Java Shell
如何快速搭建一个 Spring Boot 项目?
本指南介绍如何通过Spring Initializr创建一个基本的Spring Boot Web项目。首先访问`start.spring.io`,选择Maven项目、Java语言、Spring Boot版本3.1.0、Java 17,并勾选Spring Web依赖。点击“Generate”下载项目模板。解压后,IDE打开项目并修改`DemoApplication.java`,添加REST控制器以实现一个简单的“Hello World!”服务。通过`@RestController`和`@GetMapping`注解定义Web端点,使用`@RequestParam`获取URL参数。
|
1月前
|
IDE Java Shell
如何快速搭建一个 Spring Boot 项目?
Spring Boot 可以用最少的配置来快速创建一个独立的、生产级的 Spring 应用程序。 本文介绍如何快速搭建一个 Spring Boot「Hello World!」项目。
|
30天前
|
前端开发 Java 测试技术
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作
|
30天前
|
Dubbo Java Nacos
【实战攻略】破解Dubbo+Nacos+Spring Boot 3 Native打包后运行异常的终极秘籍——从零开始彻底攻克那些让你头疼不已的技术难题!
【8月更文挑战第15天】Nacos作为微服务注册与配置中心受到欢迎,但使用Dubbo+Nacos+Spring Boot 3进行GraalVM native打包后常遇运行异常。本文剖析此问题及其解决策略:确认GraalVM版本兼容性;配置反射列表以支持必要类和方法;采用静态代理替代动态代理;检查并调整配置文件;禁用不支持的功能;利用日志和GraalVM诊断工具定位问题;根据诊断结果调整GraalVM配置。通过系统排查方法,能有效解决此类问题,确保服务稳定运行。
51 0
|
1月前
|
Java Windows Spring
Spring Boot CMD 运行日志输出中文乱码
Spring Boot CMD 运行日志输出中文乱码
22 0
|
1月前
|
设计模式 算法 Java
Spring Boot 项目怎么使用策略模式?
策略模式是一种设计模式,它允许在运行时选择不同的算法或行为。此模式通过定义一系列算法并将它们封装在独立的类中实现,这些类可以互相替换。这样可以根据不同情况动态选择最适合的算法。 在Spring框架中,可以通过依赖注入来实现策略模式。首先定义一个抽象策略类(接口或抽象类),然后创建具体策略类实现不同的算法。具体策略类通过`@Service`注解并在名称中指定特定的策略(如加法、减法等)。在上下文类(如Service类)中,通过`@Resource`注入策略对象的Map集合,根据需要选择并执行相应的策略。
|
Java 程序员 索引
Spring的三种创建方式和各种属性的注入(二)
Spring的三种创建方式和各种属性的注入(二)
117 0
Spring的三种创建方式和各种属性的注入(二)
|
24天前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决