Spring Boot 学习之路 -- Service 层
前言
- 最近因为业务需要,被拉去研究后端的项目,代码框架基于 Spring Boot,对我来说完全小白,需要重新学习研究…
- 出于个人习惯,会以 Blog 文章的方式做一些记录,文章内容基本来源于「 Spring Boot 从入门到精通(明日科技) 」一书,做了一些整理,更易于个人理解和回顾查找,所以大家如果希望更系统性的学习,可以阅读此书(比较适合我这种新手)。
一、Service 层与 @Service 注解
在实际开发中,Service 层主要负责业务模块的逻辑应用设计。在设计 Service 层的过程中,首先设计接口,然后设计接口的实现类。通常情况下,Service 层用于封装项目中一些通用的业务逻辑,这么做的好处是有利于业务逻辑的独立性和重复利用性。因此,为了处理一个 Spring Boot 项目中的业务逻辑,Service 层是不可或缺的。
Spring Boot 中的 Service 层是业务逻辑层,其作用是处理业务需求,封装业务方法,执行 Dao 层中用于访问、处理数据的操作。Service 层通常由一个接口和这个接口的实现类组成。其中,Service 层的接口可以在 Controller 层中被调用,用于实现数据的传递和处理;Service 层的实现类须使用 @Service 注解予以标注。
说明:
Dao 层介于 Service 层和数据库之间,用于访问、操作数据库中的数据。Dao 层通常由 Dao 接口、Dao 实现类和 Dao 工厂类这 3 个部分组成。在 Dao 接口中,定义了一系列用于访问、操作数据库中数据的方法。在 Dao 实现类中,实现了 Dao 接口中的方法。Dao 工厂类的作用是返回一个 Dao 实现类的对象。
Controller 层的作用是通过调用 Service 层的接口,控制各个业务模块的业务流程。Controller 层通过解析用户通过 URL 地址发送的请求,调用不同的 Service 层的接口以处理这个请求,把处理结果返回给客户端。
在 Spring Boot 中,把被 @Service 注解标注的类称作「服务类」。@Service 注解属于 Component 组件,可以被 Spring Boot 的组件扫描器扫描到。当启动 Spring Boot 项目时,服务类的对象会被自动地创建,并被注册成 Bean。
二、Service 层的实现过程
大多数的 Spring Boot 项目采用接口模式实现 Service 层。那么,在实际开发中,如何实现 Service 层呢?如下图所示,Service 层的实现过程如下。
- 定义一个 Service 层的接口,在这个接口中定义用于传递和处理数据的方法。例如,定义一个 Service 层的接口 ProductService,代码如下:
public interface ProductService {
... // 省略用于传递和处理数据的方法
}
- 定义一个 Service 层的接口的实现类,使用 @Service 注解予以标注。这个实现类的作用有两个:一个是实现 Service 层的接口中的业务方法;另一个是执行 Dao 层中用于访问、处理数据的操作。
例如,使用 @Service 注解标注实现 ProductService 接口的 ProductServiceImpl 类,代码如下:
public class ProductServiceImpl implements ProductService {
... // 省略用于实现接口的业务方法和用于执行访问、处理数据的操作的代码
}
- 在服务类的对象被自动地创建并被注册成 Bean 之后,其他 Component 组件即可直接注入这个 Bean。
三、同时存在多个实现类的情况
在上一节中,通过简单的示例只演示了一个 Service 层的接口存在一个实现类的情况。但是在实际开发中,一个 Service 层的接口可能会针对多种业务场景而存在多个实现类。
本节将介绍如何处理 Spring Boot 中的 “一个 Service 层的接口同时存在多个实现类” 的情况。
3.1 按照实现类的名称映射服务类的对象
使用 @Service 注解标注一个 Service 层的接口的实现类,这个实现类被称作服务类,这个实现类的对象被称作服务类的对象。服务类的对象会被自动地创建,并被注册成 Bean。
综上所述,Bean 的名称就是实现类的名称。需要注意的是,实现类的名称的首字母要大写,Bean 的名称的首字母是小写的。
例如,使用 @Service 注解标注实现 Service 接口的 ServiceImpl 类,代码如下:
@Service
public class ServiceImpl implements Service { }
在上述代码中,实现类的名称是 ServiceImpl。因为 Bean 的名称就是实现类的名称,所以 Bean 的名称是 serviceImpl。因此,上述代码就等同于如下的用于注册 Bean 的代码:
@Bean("serviceImpl")
public Service createBean() {
return new ServiceImpl();
}
这样,其他 Component 组件即可通过指定 Bean 的名称的方式注入与服务类的对象对应的 Bean。代码如下:
@Autowired
Service serviceImpl;
上述代码等同于如下的代码:
@Autowired
@Qualifier("serviceImpl")
Service impl;
掌握了以上内容后,下面编写一个实例来演示如何按照实现类的名称映射服务类的对象的方式来处理 “一个 Service 层的接口同时存在多个实现类” 的情况。
- 首先,创建 TranslateService 翻译服务接口,接口中只定义一个翻译方法。代码如下:
public interface TranslateService {
String translate(String word);
}
然后,新建一个 Impl 包,并在包下创建 English2ChineseImpl 英译汉类,并实现 TranslateService 接口,同时使用 @Service 注解标注此类。在实现的翻译方法中,如果用户传入的单词是“Good morning”(不区分大小写),则返回中文“早上好”;如果传入其他内容,则返回“我还没有学会这个短句,你可以举例说明吗?”的提示信息。
English2ChineseImpl 类的代码如下:
@Service
public class English2ChineseImpl implements TranslateService {
@Override
public String translate(String word) {
if ("Good morning".equalsIgnoreCase(word)) {
return "Good morning -> 早上好";
}
return "我还没有学会这个短句,你可以举例说明嘛?";
}
}
接着,在 Impl 包下创建 French2ChineseImpl 法译汉类,并实现 TranslateService 接口,同时使用 @Service 标注此类。在实现的翻译方法中,如果用户传入的单词是 “bonjour”(不区分大小写),则返回中文“早上好”;如果传入其他内容,则返回“我还没有学会这个短句,你可以举例说明吗?”的提示信息。
French2ChineseImpl 类的代码如下:
@Service
public class French2ChineseImpl implements TranslateService {
@Override
public String translate(String word) {
if ("bonjour".equals(word)) {
return "bonjour -> 早上好";
}
return "我还没有学会这个短句,你可以举例说明吗?";
}
}
最后,创建 TranslateController 控制器类,分别创建两个服务类的对象,并分别按照两个实现类的名称(但首字母小写)映射这两个服务类的对象,使用 @Autowired 注解自动注入这两个服务类的对象。如果客户端访问的是 “/english” 地址,就将发来的参数交由负责英译汉的服务处理;如果访问的是 “/french” 地址,就将发来的参数交由负责法译汉的服务处理。
TranslateController类的代码如下:
@RestController
public class TranslateController {
@Autowired
@Qualifier("english2ChineseImpl")
TranslateService english2ChineseImpl; // 英译汉服务
@Qualifier("french2ChineseImpl")
@Autowired
TranslateService french2EnglishImpl; // 汉译英服务
@RequestMapping("/english")
public String english(String word) {
return english2ChineseImpl.translate(word);
}
@RequestMapping("/french")
public String french(String word) {
return french2EnglishImpl.translate(word);
}
}
使用 Postman 模拟用户通过 URL 地址发送的请求。访问 http://127.0.0.1:8080/english 地址,并添加 word 参数,参数值为 Good morning。发送请求后,即可看到下图结果,服务器将 Good morning 翻译成了“早上好”。
3.2 按照 @Service 的 value 属性映射服务类的对象
在 @Service 注解中,只包含一个 value 属性。value 属性是 @Service 注解的默认属性,它的两种语法格式如下:
@Service("id") //在语法格式中省略了“value = ”
@Service(value = "id") //在语法格式中没有省略“value = ”
为 value 属性赋值后,就相当于在创建与服务类的对象对应的 Bean 时确定了 Bean 的名称。因此,上述的语法格式等同于如下的用于注册 Bean 的代码:
@Bean("id")
public Service createBean() {
return new ServiceImpl();
}
这样,其他 Component 组件即可通过指定 Bean 的名称的方式注入与服务类的对象对应的 Bean。代码如下:
@Autowired
Service id;
上述代码等同于如下的代码:
@Autowired
@Qualifier("id")
Service impl;
掌握了以上内容后,下面编写一个实例演示如何按照 @Service 的 value 属性映射服务类的对象的方式处理 “一个Service层的接口同时存在多个实现类” 的情况。
- 首先,创建 TranscriptsService 考试成绩服务接口,接口中只定义一个排序方法,参数为 List 类型的对象。
TranscriptsService 接口的代码如下:
public interface TranscriptService {
void sort(List<Double> score);
}
- 创建 ASCTranscriptsServiceImpl 升序排列成绩类,并实现 TranslateService 接口,同时使用 @Service 注解标注此类。在实现的排序方法中,调用 Collections 类的 sort() 方法按照升序重新排列列表中的成绩。
ASCTranscriptsServiceImpl 类的代码如下:
@Service("asc")
public class ASCTTranscriptsServiceImpl implements TranscriptService {
@Override
public void sort(List<Double> score) {
Collections.sort(score); // 对 List 升序排序,默认排序规则
}
}
- 接着,再创建 DESCTranscriptsServiceImpl 降序排列成绩类,并实现 TranslateService 接口,同时使用 @Service 注解标注此类。
DESCTranscriptsServiceImpl 类的代码如下:
@Service("desc")
public class DESCTranscriptsServiceImpl implements TranscriptService {
@Override
public void sort(List<Double> score) {
score.sort(Comparator.reverseOrder());
}
}
- 最后,创建 TranscriptsController 控制器类,分别创建两个用于排序的服务类的对象。其中,用于负责升序排列的服务类的对象使用 @Qualifier(“asc”) 注解予以标注,以表示注入的是名称为 asc 的Bean;负责降序排列的服务类的对象直接被命名为“desc”,@Autowired 注解会自动寻找名称为 “desc” 并且类型相同的 Bean 予以注入。如果前端向 “/asc” 地址发送成绩数据,则按照升序排列列表中的成绩;如果前端向 “/desc” 地址发送成绩数据,则按照降序排列列表中的成绩。
TranscriptsController 类的代码如下:
@RestController
public class TranscriptsController {
@Autowired
@Qualifier("asc")
TranscriptService asc;
@Autowired
TranscriptService desc;
@RequestMapping("/asc")
public String asc(Double class1, Double class2, Double class3) {
return getString(class1, class2, class3, asc);
}
@RequestMapping("/desc")
public String desc(Double class1, Double class2, Double class3) {
return getString(class1, class2, class3, desc);
}
private String getString(Double class1, Double class2, Double class3, TranscriptService asc) {
List<Double> list = new ArrayList<>();
list.add(class1);
list.add(class2);
list.add(class3);
asc.sort(list);
StringBuilder sb = new StringBuilder();
list.forEach(e -> sb.append(e).append(" "));
return sb.toString();
}
}
使用 Postman 模拟用户通过 URL 地址发送的请求。访问 http://127.0.0.1:8080/asc 地址,先添加 class1、class2 和 class3 这 3 个参数并为其赋值,再发送请求,而后返回的结果将以升序的方式予以排列:
四、不采用接口模式的服务类
在实际开发中,一些功能非常简单的服务可以不采用接口模式,直接创建服务类并用 @Service 注解予以标注即可。下面编写一个实例来演示如何使用不采用接口模式的服务类。
- 创建 VerifyService 校验服务类,并用 @Service 注解予以标注。在这个服务类中,提供了一个方法,用来校验指定字符串是不是由 2~4 个中文字符组成的。
VerifyService 类的代码如下:
@Service
public class VerifyService {
public boolean chineseName(String name) {
String match = "^[\\u4e00-\\u9fa5]{2,4}$";
if (name != null) {
return name.matches(match);
}
return false;
}
}
- 创建 VerifyController 控制器类,创建 VerifyService 服务类的对象,并使用 @Autowired 注解自动注入与服务类的对象对应的 Bean。当客户端发来一个名称时,通过 VerifyService 服务类的对象调用校验方法,判断该名称是否为有效的中文名称,并返回校验结果。
VerifyController 类的代码如下:
@RestController
public class VerifyController {
@Autowired
VerifyService verify;
@RequestMapping("/verify/name")
public String verifyName(String name) {
if (verify.chineseName(name)) {
return "中文名称检验通过";
}
return "这不是一个有效的中文名称";
}
}
使用 Postman 模拟用户通过 URL 地址发送的请求。访问 http://127.0.0.1:8080/verify/name 地址,并添加 name 参数,name 参数的值为 “David”。因为 David 都是英文字母,所以发送请求后会看到下图结果:
将 name 参数的值修改为“李四”后,再次发送请求: