Spring Service功能作用详细讲解
居然天上楼 人气:01. Spring项目中的核心组成部分
项目的核心组成部分图解:
2. Spring项目中的Service
2.1 Service的功能作用
Service是项目中用于处理业务逻辑的,因为每种数据在做某种操作时,应该都有某些规则:
- 例如用户尝试登录时,涉及的规则可能包含:用户名对应的用户信息必须存在、提交的密码必须与数据库中存储的密码是匹配的……
- 例如用户尝试修改密码时,涉及的规则可能包含:当前用户账号必须存在且处于正常状态、提交的原密码必须与数据库中存储的密码是匹配的……
- 例如用户尝试注册时,涉及的规则可能包含:提交的用户名必须在数据库不存在,提交的手机号码必须在数据库中不存在,提交的电子邮箱必须在数据库中不存在……
这些规则是用于保障数据的有效性、安全性的,使得数据可以随着我们设定的规则而产生或发生变化!
在项目中,关于Service的开发,通常是先定义接口,再定义类实现此接口,接口名通常使用“数据类型Service”这样格式的名称,而实现类通常是在接口名的基础上再添加Impl
后缀。
在《阿里巴巴Java开发手册》中的规约:
【强制】对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部 的实现类用 Impl 的后缀与接口区别。
2.2 Service的实现
则在项目的根包下创建service.IAlbumService
接口:
public interface IAlbumService {}
然后,在根包下创建service.impl.AlbumServiceImpl
类,此类需要实现以上的IAlbumService
接口:
public class AlbumServiceImpl implements IAlbumService {}
文件结构如下图所示:
然后,需要在接口中设计“添加相册”的抽象方法:
xx xx(xx);
关于抽象方法的名称:可以完全自定义,当前业务是“添加相册”,可以使用addNew
、add
等。
关于抽象方法的参数列表:大多参数是由客户端提交到控制器,再由控制器调用时传递过来的参数,另外,也可能是控制器处理出来的某些数据(例如Session中的当前登录用户信息),本次的参数应该包含:相册名称、相册简介、相册的排序序号,可以将这3个数据封装到自定义的DTO类中,并使用DTO类型作为参数。
关于抽象方法的返回值类型:仅以操作成功为前提来设计返回值类型,如果操作失败,将抛出异常。
在项目的根包下创建pojo.dto.AlbumAddNewDTO
类:
public class AlbumAddNewDTO { private String name; private String description; private Integer sort; }
并在IAlbumService
接口中添加抽象方法:
void addNew(AlbumAddNewDTO albumAddNewDTO);
然后,在AlbumServiceImpl
中实现此抽象方法:
@Slf4j @Service public class AlbumServiceImpl implements IAlbumService { @Autowired private AlbumMapper albumMapper; public AlbumServiceImpl() { log.debug("创建业务对象:AlbumServiceImpl"); } @Override public void addNew(AlbumAddNewDTO albumAddNewDTO) { // 【稍后再实现】应该保证此相册的名称是唯一的 // 创建Album类型的对象 // 调用BeanUtils.copyProperties(源对象, 目标对象)将参数的属性值复制到新创建的Album对象中 // 调用albumMapper的int insert(Album album)方法插入相册数据 } }
初步实现为:
package cn.tedu.csmall.product.service.impl; import cn.tedu.csmall.product.mapper.AlbumMapper; import cn.tedu.csmall.product.pojo.dto.AlbumAddNewDTO; import cn.tedu.csmall.product.pojo.entity.Album; import cn.tedu.csmall.product.service.IAlbumService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Slf4j @Service public class AlbumServiceImpl implements IAlbumService { @Autowired private AlbumMapper albumMapper; public AlbumServiceImpl() { log.debug("创建业务对象:AlbumServiceImpl"); } @Override public void addNew(AlbumAddNewDTO albumAddNewDTO) { // 【稍后再实现】应该保证此相册的名称是唯一的 // 创建Album类型的对象 Album album = new Album(); // 调用BeanUtils.copyProperties(源对象, 目标对象)将参数的属性值复制到新创建的Album对象中 BeanUtils.copyProperties(albumAddNewDTO, album); // 调用albumMapper的int insert(Album album)方法插入相册数据 albumMapper.insert(album); } }
完成后,在src/test/java
下的根包下创建service.AlbumServiceTests
测试类,并在类中编写、执行测试方法:
package cn.tedu.csmall.product.service; import cn.tedu.csmall.product.pojo.dto.AlbumAddNewDTO; import cn.tedu.csmall.product.pojo.entity.Album; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @Slf4j @SpringBootTest public class AlbumServiceTests { @Autowired IAlbumService service; @Test void addNew() { AlbumAddNewDTO album = new AlbumAddNewDTO(); album.setName("测试数据9998"); album.setDescription("测试数据的简介"); album.setSort(99); // 注意:sort值必须是[0, 255]之间的 service.addNew(album); log.debug("添加数据完成!"); } }
在具体实现过程中,还应该保证此次尝试添加的相册的名称是唯一的!
可以通过查询数据库来得知尝试添加的相册的名称是否已经被使用,需要执行的SQL语句可以是:
select id from pms_album where name=?
select count(*) from pms_album where name=?
可以选择使用以上第2种查询来检验相册名称是否已经被使用,则应该在
AlbumMapper.java
接口中添加:
int countByName(String name);
并在AlbumMapper.xml
中配置SQL:
<!-- int countByName(String name); --> <select id="countByName" resultType="int"> SELECT count(*) FROM pms_album WHERE name=#{name} </select>
完成后,应该在AlbumMapperTests.java
中编写并执行测试:
@Test void countByName() { String name = "测试数据"; int count = mapper.countByName(name); log.debug("根据名称【{}】统计完成,结果:{}", name, count); }
接下来,可以在Service的实现过程中进行检查,例如:
String albumName = albumAddNewDTO.getName(); int count = albumMapper.countByName(albumName); if (count > 0) { // 相册名称已经被使用,将不允许添加此相册,应该抛出异常 } else { // 相册名称没有被使用,可以将此相册数据插入到数据库中 }
提示:以上代码中,由于满足if
条件时将抛出异常,所以,可以不必使用else
,并且,在后续的编程中,当需要执行某些判断时,应该优先根据“抛出异常”或“终止当前方法的执行”来设计if
的条件!即:
if (count > 0) {
// 相册名称已经被使用,将不允许添加此相册,应该抛出异常 }
// 相册名称没有被使用,可以将此相册数据插入到数据库中
具体实现为:
@Override public void addNew(AlbumAddNewDTO albumAddNewDTO) { // 应该保证此相册的名称是唯一的 String albumName = albumAddNewDTO.getName(); int count = albumMapper.countByName(albumName); if (count > 0) { throw new RuntimeException(); } // 创建Album类型的对象 Album album = new Album(); // 调用BeanUtils.copyProperties(源对象, 目标对象)将参数的属性值复制到新创建的Album对象中 BeanUtils.copyProperties(albumAddNewDTO, album); // 调用albumMapper的int insert(Album album)方法插入相册数据 albumMapper.insert(album); }
为了避免测试时因为相册名称冲突出现异常而导致测试失败,应该在测试时捕获所抛出的异常,例如:
@Test void addNew() { AlbumAddNewDTO album = new AlbumAddNewDTO(); album.setName("测试数据9998"); album.setDescription("测试数据的简介"); album.setSort(99); // 注意:sort值必须是[0, 255]之间的 try { service.addNew(album); log.debug("添加数据完成!"); } catch (RuntimeException e) { log.debug("添加数据失败!名称已经被占用!"); } }
关于以上实现过程中抛出的异常,使用的是RuntimeException
,是不合适的!因为程序出现RuntimeException
的原因有很多,例如空指针异常、数组下标越界异常、类型转换异常,都属于RuntimeException
,如果“相册名称被占用”时抛出RuntimeException
,则此方法的调用者很难区分出现异常的真正原因!
通常,建议自定义异常,并且,当视为失败时,抛出此自定义异常的对象!
则在根包下创建ex.ServiceException
类,继承自RuntimeException
:
public class ServiceException extends RuntimeException {}
提示:本次自定义的异常应该继承自RuntimeException。
然后,在AlbumServiceImpl
中添加相册时,如果相册名称被使用,则抛出ServiceException
类型的异常:
if (count > 0) { throw new ServiceException(); }
并且,在测试中,捕获的异常也改为ServiceException
:
try { service.addNew(album); log.debug("添加数据完成!"); } catch (ServiceException e) { log.debug("添加数据失败!名称已经被占用!"); }
加载全部内容