很抽象,我自己也不好理解,仅作为一个前端转后端的个人理解
一、先解释一个案例,以这个案例来分析“三层架构”
这里我先解释一下黑马程序员里的这个案例,兄弟们看视频的可以跳过这节课:Day05-08. 请求响应-响应-案例_哔哩哔哩_bilibili 看我这里的解说就可以,因为他这里把前端的文件塞到后端这里弄,还要解析XML文件来获取数据,完全没必要,没有任何地方会这样做了,直接看我的解释过一遍就行
首先他把一个写了我们要的数据的XML引入到spring boot项目中,我们可以理解类似为前端有时会在一个js文件里写一堆数据用于测试、模拟后端数据的(但实际后端应该是连接数据库,数据是来源于数据库的)
然后他的这个鬼XML文件里的数据里还不直接写明白“性别”是(男/女),而是写成(1/0),“职业”也是,非要写成(1/2/3),然后还得到获取数据的地方把(1/0)、(1/2/3)解析成(男/女)、(讲师/班主任/....),这不是脱裤子放屁,多此一举吗
于是,他又去请求页面,再解析获取到XML数据后,再去进行以上这些逻辑处理......我们暂且理解为vue里的<script></script>这部分的逻辑处理吧
最后再把这些数据发送请求
(我只是为了方便各位理解!!不是一个东西啊!!)
那么结合前端我们可以理解为有点类似“表单数据传送”,我们先获取表单的所有数据,然后进行逻辑处理:拆分出数据然后封装进一个对象,最后发送请求给后端
但是看看这一坨代码这么乱......
二、三层架构是啥
那么分析一下一坨代码,可以看出其实可以分成三大块:
【数据访问】:解析数据源,拿到数据
【逻辑处理】:把数据抽出来进行一些逻辑处理
【接收请求、响应数据】:把数据封装好给回前端
那么我们应该把这三块分开来写,这样的话,那一部分出问题可以直接找到这一部分查看问题
因此人们就规定应该分成这三块:
这三块要做的事:
-
Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
-
Service:业务逻辑层。处理具体的业务逻辑。
-
Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。
那么这个三层架构的执行流程是:
1、前端发起的请求,由Controller层接收(Controller响应数据给前端)
2、Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
3、Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
4、Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)
好,那么与之相反的,我们开发人员写代码的流程是:(切记!不要去管这里代码写了什么,过一遍流程就行!!!这里的代码不值得去花时间学习)
1、在【请求处理类】同级目录下创建一个【dao】目录
2、然后在【dao】目录下创建一个代表该获取的数据的接口(java里的接口,不是请求数据的那个接口)
然后创建这个接口的实现类(连同包含实现类的文件夹目录一块),implements实现这个类,然后把获取数据、解析数据的内容放到这里实现
记得把数据return出去
3、然后同样的流程【创建service目录】——>【创建代表数据的接口】——>【然后创建(连文件夹包一起)实现这个接口的实现类】——>【记得先创建并调用Dao的实现类对象,因为现在数据在Dao那】——>【然后service的实现类的内容就是处理数据的逻辑】
4、最后,创建【controller】目录,把一个【请求处理类】放到【controller目录】——>【然后现在完整、处理好的数据源在service实现类】——>【在方法外面创建service的实现类对象】——>【然后方法里调用service实现类对象,拿到数据】——>【然后通过请求return出去,响应给前端】
总结就是:
开发视角:数据源从外部到Dao——>然后我们要用service获取Dao的数据,然后处理——>然后Controller最后获取service处理好的数据,响应回前端
前端视角:发送请求给到Controller——>Controller找service要——>service找Dao要
三、分层解耦
1、理解耦合是什么?
耦合问题
首先需要了解软件开发涉及到的两个概念:内聚和耦合。
内聚:通俗讲就是各功能功能模块跟自身功能联系
比如【Controller】的功能是接受请求、实现请求接口、响应回前端,【service】的功能是处理数据、做逻辑处理,【Dao】功能就是解析数据
那么这三块分开,各自里面的内容是各自的功能实现,这就是【内聚高】,互不影响
那么如果不分开,这三个内容的代码全堆【Controller】那里,那么【Controller】本来功能只用实现请求、响应前端的,现在当黑奴,一人干几份活,这就是【内聚低】
耦合:衡量软件中各个层/模块之间的依赖、关联的程度。
比如【Controller】接收到前端请求后,找数据需要通过调用【service】来获取数据,而【service】找数据也得靠调用【Dao】来获取数据,这种联系就叫【耦合】
当他们是三个部门,当少了一方部门、或者其中一个部门要改名字啥的,会牵连到三个部门,这就是【耦合高】
但是软件工程开发要求【内聚低耦合高】
那么就要分层解耦
2、分层解耦是什么?
那么现在我们要解耦,该怎么办?
我们思考一下【Controller】调用的是什么?是【service的实现类】。
那么【Controller】调用的时候其实不管你是叫【service_A】还是【service_B】,只要是implement实现了【service】这个接口的实现类,【Controller】都需要
那么就可以有这么一个外部【容器】,他装有实现了【service】接口的【实现类】,当【Controller】需要的时候就去【容器】找有没有,有的话就拿去用
(我们理解为“华为公司”需要招聘员工,不管你是清华学子、还是北大学子,只要你是实现、集合了“985/211重本院校”的学生,他都要。那么我们就可以设一个“人才市场”,里面有各个[实现、集合了“985/211重本院校”的学生],华为公司需要谁,就去人才市场要)
这里还有几个概念要知道:
控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。(通俗理解给自己签订卖身契,卖到人口市场,随时被人交易)
依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。(通俗理解为买家跟人口市场绑定了,有符合要求的就自动买了)
IOC容器中创建、管理的对象,称之为:bean对象 (理解为人口市场的奴隶hh)
四、IOC控制反转 + DI注入依赖
1、首先先把【Controller】【service】【Dao】之间的联系删掉
(理解为华为公司取消了跟某些高校的强制合作,不要固定那几个高校推荐内推的学生,选择让人才市场给自己分配员工)
2、然后开始【控制反转】
给【service】和【Dao】加上容器注解【@Component】
加上它,就意味着要将当前类“交给”IOC容器管理,成为IOC容器里的一个bean对象
(可以理解为高材生把自己的简历投放到人才市场了,由人才市场保管自己的简历)
3、然后开始【依赖注入】
要将【Controller】绑定上【依赖注入】,加上注解【@Autowired】
加上【@Autowired】就意味着运行时,IOC容器会提供该类型的bean对象,并赋值给该变量
(理解为华为公司把自己的HR丢人才市场,任由HR发挥,只要符合要求就直接招进公司)
4、那如果要更换bean对象怎么办?
加入我有一个【service】实现类【A】,我现在不要用它了,我要改成【B】怎么办?
把【A】的【@Component】注释掉,在【B】上面加上【@Component】就行了......就这么简单
五、Bean另外的衍生注解
注解 | 说明 | 位置 |
@Component | 声明bean的基础注解 | 不属于以下三类时,用此注解 |
@Controller | @Component 的衍生注解 | 标注在控制器类上 |
@Service | @Component的衍生注解 | 标注在业务类上 |
@Repository | @Component的衍生注解 | 标注在数据访问类上 (由于与mybatis整合,用的少) |
是啥个意思呢?
意思是【控制反转】有的时候不一定要用【@Component】,其实【Controller】、【Service】、【Dao】的【控制反转】可以分别用【@Controller】、【@Service】、【@Repository】来注解
然后因为其实【@Service】、【@Repository】的源代码里其实是包含了【@Component】的,所以用【@Service】、【@Repository】就等于用【@Component】
但是spring boot的web开发里,【Controller】控制器不能用【Component】,不过它除了注解【@Controller】,我们之前讲过【@RestController】也包含了【@Controller】,所以用【@ReastController】就可以了
六、主包(主目录)外面的包的Bean对象扫描不到
问题:使用前面学习的四个注解声明的bean,一定会生效吗?
答案:不一定。(原因:bean想要生效,还需要被组件扫描,比如你主包外面的bean对象就不会生效)
那就要用到组件扫描:【@ComponentScan】
但是【@ComponentScan】并不是乱用的,首先回到我们的【启动类】
我们知道【@SpringBootApplication】是【启动类】注解,但其实他还是包含了【@ComponentScan】的组件扫描注解,它可以扫描【当前包以及其自包里的所有的Bean】
但是要是Bean在主包的外面怎么办?
那这是才只能手动写【@ComponentScan】来导入要扫描的“包(目录)”,并且还得带上原本当前这个“包(目录)”,因为会覆盖下面的【@SpringBootApplication】
具体语法是:
@ComponentScan( { "外部含Bean的那个包" , "当前包" } )
但其实不建议这样,一般情况推荐全部Bean都放到启动类的包下,方便全局扫描
七、DI注入依赖选择一个Bean来注入
我们前面说过,如果想要注入依赖的时候,容器里有两个符合条件的实现类,只选择其中一个的话,就把另一个实现类的【@Component】注释掉
但是如果我容器里有成千上万个符合条件的【实现类Bean】怎么办?难道我要全部一个一个去注释吗?
比如下面这个例子,我有两个都写了【@Sevice】(怕忘了,再提醒一下这个也等于@Component)的【service实现类Bean】
然后此时再运行时就会报错,因为超过两个同类型的实现类Bean
这是因为注入依赖里的注解【@Autowired】,它是根据类型来自动获取容器里所有的符合的实现类Bean的,那么容器里有两个就拿两个,有十个就拿十个,而一旦超过了一个Bean就会报错
那么解决办法有三个
1、(不太推荐)@Primary:在要希望生效的那个Bean上添加这个注解
2、(可以)@Qualifier:在要注入依赖的地方、@Autowired前面指定你要哪一个Bean
3、(非常推荐)@Resouce:直接在注入依赖的地方,去掉@Autowired,替换成这个并指定要哪个Bean
1、@Primary是在要希望生效的那个Bean上添加这个注解,不推荐是因为跟注释【@Component】其实是半斤八两的啊.......你还得去一个一个找到这个Bean,然后再Bean的代码里改
2、@Qualifier就是在要注入依赖的地方、@Autowired前面指定你要哪一个Bean,这样确实很方便,直接指明要谁,美中不足就是,我感觉跟@Autowired有点重复,两在一块总感觉有点多余
另外解释为什么【实现类EmpServiceA】在@Qualifier这要写成【empServiceA】,因为我前面懒,省略了讲:Bean对象的名字其实是【实现类】名字的首字母小写......而不是它原本的实现类的那个名字
3、@Resouce为甚推荐?因为它是直接在注入依赖的地方,去掉@Autowired,替换成@Resouce,并写上指定要哪个Bean的名字就行
为什么它可以直接不要【@Autowired】,因为【@Resouce】是根据名字来注入Bean的!!
另外【@Primary】跟【@Qualifier】是spring boot框架提供的,而【@Resouce】是Java的JDK提供的