本文最后更新于 2023-10-12,文章内容可能已经过时。
从SpringIOC到框架优化方式
1.目的:
从Spring框架的IOC,DI,SpringBoot的自动装配原理产生原因,作用,实现原理入手,浅析框架的简化开发作用,以及框架的设计思路.
2.流程:
2.1 引入:
使用框架的感受?(SpringBoot框架)
方便快捷.快速开发,调试,部署.
编程语言从汇编语言到高级语言,程序员开发的工具越来越简单.而现在的主流开发,尤其是后端开发,越来越依赖于框架.框架的作用有很多,其核心目的就是简化开发.使编程人员能够更加专注于业务逻辑而最大程度上忽略其他的东西.
如何简化开发?不同的语言的框架,有很多不同的实现方式.接下来我们主要关注Java语言.
Java语言的核心是什么?
面向对象程序设计,一切皆对象,在所有的程序中都会出现的一点就是类和对象的管理.同时这也是java程序所面临的最大的问题.
对象管理包括:
1.对象的产生
2.对象的使用
3.对象的销毁
我们先来看对象的销毁.
首先,回答一下c语言和c++的对象的销毁方式,
是由程序主动关闭.
而Java程序对象是如何销毁的?是由JVM自动进行垃圾回收的.这就是简化开发的角度上java比c和c++先进的地方,但是现在,对象的销毁问题解决了,对象的创建和使用问题怎么办呢?这是单纯地Java所不能解决的.
2.2 DI:
先来看两段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| Class Car(){
private Bottom bottom;
public Car(){
this.bottom = new Bottom();
}
}
Class Bottom(){
int a;
public Bottom(){
this.a = 1;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| Class Car(){
private Bottom bottom;
public Car(Bottom bottom){
this.bottom = Bottom bottom;
}
}
Class Bottom(){
int a;
public Bottom(){
this.a = 1;
}
}
|
这两种写法在程序的调用时候略有不同,第一种是在主程序中直接new一个car()对象就完成了。而第二种则需要先new一个bottom对象,然后再new一个car对象。这样看来,似乎第一种写法更加简练一些,可是,事实真的是如此吗?
让我们考虑一种情况:
当bottom发生修改时,比如a的值通过传入一个变量来确定的时候,这样对于第一种写法,所有的构造函数全部都需要修改,而对于第二种方式则只需要修改一个构造函数即可。
对于第一种设计来说,这样的设计基本是不可维护的,因为在实际工程中,有些类会有几千个底层,如果要一一修改,所耗费的成本太大了。
而且,在第一种写法中,创建car对象必然会创建一个新的bottom对象,纵使会被销毁,但是也在一定程度上增加了开销。
所以现在我们知道,采用第二种写法是比较合理的,即:
所有的new对象的过程都写在程序运行时,而不是再类的构造方法中。构造方法中统一更改为传入所需要的类的对象这种格式。
这就是依赖注入(Dependency Injection)。
依赖注入的主谓宾补充完整,就是将调用者所依赖的类实例对象注入到调用者类。而在这个例子中,car依赖于bottom,car开放了一个接口(这里其实是构造方法)使它的依赖bottom对象可以注入到自己的对象中来。提高了系统的可维护性和可扩展性。
2.3 IOC:
好了,在有了上面的基础之后,我们再来看我们的程序,现在它已经变成了大概这样:
1
2
3
4
5
6
7
8
9
10
11
12
| public static void main(String[] args) {
A a = new A();
//a发挥作用
B b = new B();
//b发挥作用
C c = new C():
//c发挥作用
......
A a = new A();
//a发挥作用
}
|
但是这样,仍然会出现很多问题.
如果a是某个接口的实现类,现在需要修改a的实现类,换成另一个类,这样就需要修改所有的new A的方法,这样仍然是十分复杂而不可维护的。
那么,能不能有一种机制来解决这个问题呢?这就体现出框架的作用了。
框架可以帮助我们创建对象.
只需要我们通过一些语法告诉框架我们需要什么样的对象,它的名字是什么,它的类在什么地方,程序就会自动的帮我们创建对象,我们只需要在需要这个对象的时候问框架要这个对象就可以了. 来看一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| package com.spring.demo;
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| <?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">
<bean id="person" class="com.spring.demo.Person">
<property name="name" value="zje"></property>
<property name="age" value="24"></property>
</bean>
</beans>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| package com.spring.demo;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TextIoc {
@Test
public void textUser()
{
//1.获取spring配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("Bean.xml");
//2.由配置文件返回对象
Person p = (Person)context.getBean("person");
System.out.println(p);
p.info();
}
}
|
这种机制主要由三个组成:
1.框架读取配置文件(也有可能是某些注解),获取类的有关信息.
2.根据类文件生成对象(bean)
3.在程序中通过(bean)方法获取对象.
这样在发生修改时,只需要修改配置文件中的相关信息,就可以实现修改,而不需要修改代码中的所有new 语句.
在这种框架下,我们再也不需要手动的创建对象,而是由框架帮我们创建对象,换句话说,我们把对象的控制权完全的交给了程序(框架)(以前是销毁权利,现在是全部的权利).
这就是控制反转(Inversion of Control).同样也是轻量级的Spring框架的核心。
不同的教程和文章对这两个词有很多种不同的解读,我认为,这样解读是最通俗易懂和符合常理的一种方式.
接下来我们主要关注Java语言,Java语言的核心是面向对象设计,在Java中,一切皆对象,在所有的程序中都会出现的一点就是类和对象的管理.同时这也是java程序所面临的最大的问题.
这样,我们就把java的对象创建和使用的问题,变成了bean的创建和获取的问题。
2.3.1 简单的手写一个IOC容器:
容器类:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
| package com;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class IOCContainer {
private Map<String, Object> beans; // 存储对象实例的 Map
//初始化
public IOCContainer() {
beans = new HashMap<>();
}
// 获取 bean 对象
public Object getBean(String name) {
return beans.get(name);
}
// 注册 bean 到容器中
private Object registerBean(String name,Class<?> clazz, Map<String, Object> properties) throws Exception {
try {
// 获取无参数的构造函数
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
Constructor<?> constructor = null;
for (Constructor<?> c : constructors) {
if (c.getParameterCount() == 0) {
constructor = c;
break;
}
}
if (constructor == null) {
throw new RuntimeException("No default constructor found for bean: " + clazz);
}
//生成实例
Object bean = constructor.newInstance();
// 获取变量属性,进行注入
for (Field field : clazz.getDeclaredFields()) {
if (properties.containsKey(field.getName())) {
field.setAccessible(true);
field.set(bean, properties.get(field.getName()));
}
}
//注册到bean容器中
beans.put(name,bean);
return bean;
} catch (Exception e) {
throw new RuntimeException("Failed to create bean: " + clazz, e);
}
}
public static void main(String[] args) throws Exception {
IOCContainer iocContainer = new IOCContainer();
// 注册 test到容器中
Map<String, Object> testProperties = new HashMap<>();
testProperties.put("name", "张三");
iocContainer.registerBean("test", Test.class, testProperties);
// 获取 test对象并调用方法
Test test = (Test) iocContainer.getBean("test");
test.Hello();
}
}
|
测试类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| package com;
public class Test {
private String name;
public void Hello(){
System.*out*.println("Hello!"+this.name);
}
public Test(){
}
}
|
2.4 @Autowried:
我们先来看bean的获取。
现在,我们的程序已经变成了这样:
1
2
3
4
5
6
7
8
9
10
11
| public static void main(String[] args) {
A a = (A) context.getBean("A");
//a发挥作用
B b = (B) context.getBean("A");
//b发挥作用
C c = (C) context.getBean("A"):
//c发挥作用
......
A a = (A) context.getBean("A");
//a发挥作用
}
|
1
2
3
4
5
6
7
8
9
10
| import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyApplication {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyBean myBean = (MyBean) context.getBean("myBean");
myBean.doSomething();
}
}
|
这样手动获取bean的方式仍然是非常繁琐和容易出错的,因为我们需要同时指定bean的类型和名称。
那么可不可以自动的获取bean呢?只要我创建一个变量就好了。
使用自动装配(@Autowired
注解)的好处包括:
简化代码:自动装配可以大大减少手动装配的代码,特别是在大型应用程序中,手动装配可能非常繁琐和易错。
更加可读性:通过使用自动装配,代码可以更加清晰和易于理解。不再需要在代码中指定显式的依赖注入,而是通过注解告诉Spring容器应该注入哪些依赖项。
更加灵活:使用自动装配可以使代码更加灵活,因为它可以在运行时自动解析依赖关系。这使得代码更加易于维护和测试,因为您不需要手动维护依赖关系。
可扩展性:使用自动装配可以使代码更加易于扩展。例如,如果您需要添加新的组件或服务,您可以使用自动装配来自动解析它们的依赖关系,而不需要手动修改现有代码。
更高的生产率:使用自动装配可以提高生产率,因为它可以使您的代码更加简洁、易于理解和易于维护。这意味着您可以更快地编写代码,并且更容易开发和维护复杂的应用程序。
这就是@Autowried注解,它的意思是自动注入,补充完整是自动把IOC中的bean注入到它所需要的地方去。请注意这里的自动注入和下文的自动装配,和上文的依赖注入都有所区别,注意区分。
1
2
3
4
5
6
7
8
9
10
11
12
13
| @Autowired
UserScoreRecordRepository userScoreRecordRepository;
@PostMapping("/addstrip")
public Result addScore(@RequestParam("name") String name,
@RequestBody() List<AddStrip> addStrips) {
return activityService.addScore(name, addStrips);
}
@PostMapping("/searchActivity")
@RequestParam("status") Integer status) {
return activityService.searchScore(status);
}
|
2.5 自动装配:
现在看来,似乎一切都非常完美……除了各种恶心的配置.
现在我们的开发过程是怎么样的呢? 新建项目,引入依赖,编写各种xml配置文件或者JavaConfig类,然后开始代码开发.
这是一个实际组件的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new
UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(false);
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
CorsFilter corsFilter = new CorsFilter(urlBasedCorsConfigurationSource);
return corsFilter;
}
}
|
但是,在实际的开发中,我们需要依赖的第三方组件是非常多的,比如tommat,mybatis等等,每当我引入这些依赖的时候,由于这协议来本身还依赖于其他的一些bean,我就需要写一大堆的配置才能完整配置,然后进行开发.
但是在很多时候,我们可以发现,在大多数情况下,所写出来的配置文件的细节都是差不多的,这时候就不禁引起我们思考了:能不能自动的导入这些配置文件呢?
这就是自动装配.简单来说,就是在约定大于配置的基础上,在第三方组件引入的时候,自动的带上一份配置文件,然后由框架自动的加载这些配置文件,自动的注入IOC容器生成bean.
这样就不需要我们为大多数组件手动的配置编写配置文件了。
这里就有同学要问了,那我在spingboot中为那些组件写的@bean注解是什么?不是已经有自动装配了吗?
但是在一些特定的情况下,仍然需要我们手动的配置文件.
需要使用自定义的第三方组件,而该组件的配置无法通过自动配置来完成。
需要使用一些较为复杂的配置,比如多数据源配置、分布式事务配置等。
需要对一些组件的配置进行细粒度的控制,比如缓存的 TTL、连接池的大小等。
需要对一些组件进行自定义扩展,比如自定义错误处理、自定义消息转换等。
就像在上文的跨域配置中,因为需要配置的东西特别多,而且在不同的应用场景下不太一样,所以仍然需要自己手动配置。
浅浅总结一下:
Spring Boot的自动装配和@Autowired
注解都是基于Spring框架的IoC容器实现的,不同的是它们自动化的实现方式略有不同。
在Spring Boot中,自动装配的核心是自动读取并配置各种组件和依赖项。Spring Boot会自动扫描应用程序的类路径,查找并读取各种配置文件(例如application.properties
或application.yml
),并根据这些配置信息自动配置和初始化各种组件和依赖项。这样,我们就不需要手动配置和初始化每个组件和依赖项,从而简化了应用程序的开发和部署。
而@Autowired
注解的自动则是指自动查找和注入符合类型要求的Bean对象。当我们在代码中使用@Autowired
注解时,Spring IoC容器会自动查找并创建符合类型要求的Bean对象,并将它们注入到我们的类中。这样,我们就不需要手动创建和配置每个Bean对象,从而简化了依赖注入的实现。
总之,Spring Boot的自动装配和@Autowired
注解都是基于Spring框架的IoC容器实现的,它们通过不同的自动化方式简化了应用程序的开发和部署。
2.6 代码生成:
(探讨部分)
现在,我们对框架的介绍已经进行到了实际开发的地步,日常开发中我们使用的spring boot基本上就是基于以上的原理和方法在进行简化开发的,但是框架的开发是没有尽头的,这不由得再次引发我们的思考,这就是最优秀的框架了吗?
事实上,虽然spring什么都能做,但是它最大的用处还是用来进行后端的那种crud开发,而这种开发一般都是遵循mvc模式,在这种情况下,有没有过这种疑惑?
来看一个service和controller:
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
| @Override
public Result createActivity(String year, Integer type, String name, String
explanation, String icon, String organization, String time, String site) {
try {
if(activityRepository.findActivityByName(name)!=null){
return Result.error(400,"活动名称已存在!");
}
Activity activity = new Activity();
activity.setYear(year);
activity.setActivityType(type);
activity.setAname(name);
activity.setExplanation(explanation);
activity.setIcon(icon);
activity.setOrganization(organization);
activity.setTime(time);
activity.setSite(site);
activity.setDeleteStatus(1);
activity.setStatus(0);
SimpleDateFormat tempDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String datetime = tempDate.format(new java.util.Date());
activity.setCreateTime(datetime);
activityRepository.save(activity);
return Result.success(200,"新建活动成功!");
}catch (Exception e){
return Result.error(500,"新建活动失败!");
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| @PostMapping("/addactivity")
@ApiOperation("创建活动")
public Result createActivity(@ApiParam("活动年份形如2023春,2023秋") @RequestParam("year") String year,
@ApiParam("活动加分类型,1表示加日常行为分,2表示加个性发展分,3表示加创新创业分,4表示加创新创业讲座学分,") @RequestParam("type") Integer type,
@ApiParam("活动名称") @RequestParam("name") String aname,
@ApiParam("活动备注") @RequestParam("explanation") String explanation,
@ApiParam("活动图片的url") @RequestParam("icon") String icon,
@ApiParam("活动所属社团") @RequestParam("organization") String organization,
@ApiParam("活动进行时间") @RequestParam("time") String time,
@ApiParam("活动进行地点") @RequestParam("site") String site){
return activityService.createActivity(year,type,aname,explanation,icon,organization,time,site);
}
|
在这个例子中,controller其实只做了两件事,调用了service的方法,这一点是非常重复的,完全属于重复劳动.为方法所属的参数加上了注释.而这些个注释最后会生成接口文档.
有趣的是,在实际的项目开发中,有很多时候是先有接口文档,然后后端程序员根据接口文档去编写代码.那么在这些的基础上,有没有什么更好地编写mvc模式的crud的方式呢?没有多余的东西,只有业务逻辑
代码生成:根据某种接口定义,自动的生成出了业务逻辑值外的其他所有代码,而一个接口的业务逻辑被抽象成一个方法,程序员只需要关注这个方法即可.
下面展示的是hertz的代码生成:
接口定义:
1
2
3
4
5
6
7
8
9
10
| struct RegisterRequest {
1: string username
2: string password
}
struct RegisterResponse {
1: string code
2: string msg
3: i64 userid
4: string token
}
|
生成代码:出了方法内部语句,其余都为框架自动生成.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| func (s *UserImpl) Register(ctx context.Context, req *api.RegisterRequest) (resp *api.RegisterResponse, err error) {
//建立数据库连接,数据库名:数据库密码
dsn := "root:123456@tcp(127.0.0.1:3306)/dbgotest"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
//处理错误
if err != nil {
//控制台打印错误日志
panic("数据库连接失败!")
}
var user User
db.First(&user, "name = ?", req.Username)
if db.RowsAffected != 0 {
resp = &api.RegisterResponse{Code: "1", Msg: "用户名已经存在!"}
return
}
//创建一条数据,传入一个对象
db.Create(&User{Username: req.Username, Password: req.Password, FollowCount: 0, FollowerCount: 0})
resp = &api.RegisterResponse{Code: "0", Msg: "用户注册成功!", Token: req.Username}
return
}
|
代码结构:
这未必是最好的实践方式,只能说,框架的开发和使用是永无止境的,没有最好的框架,只有最合适的框架。
3.参考资料:
1.《Inversion of Control Containers and the Dependency Injection pattern》
IOC领域的元老级文章,首次提出了DI的确切概念。
2.掘金:
https://juejin.cn/post/6844904161775976456#heading-7
https://juejin.cn/post/7215507413779611707
https://juejin.cn/post/6844903793637720071
https://juejin.cn/post/7162568709955911717
3.CSDN:
https://blog.csdn.net/weixin_54514751/article/details/126055383?ops_request_misc=&request_id=&biz_id=102&utm_term=springioc&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduweb~default-7-126055383.nonecase&spm=1018.2226.3001.4187
https://blog.csdn.net/weixin_52731337/article/details/119170446
https://blog.csdn.net/weixin_43826242/article/details/106005176?spm=1001.2014.3001.5506
https://blog.csdn.net/qq_24313635/article/details/109431324