Spring Boot 业务逻辑层

关于业务逻辑层(Service层)

业务逻辑层是被Controller直接调用的层(Controller不允许直接调用持久层),通常,在业务逻辑层中编写的代码是为了保证数据的完整性和安全性,使得数据是随着我们设定的规则而产生或发生变化。

通常,在业务逻辑层的代码会由接口和实现类组件,其中,接口被视为是必须的

  • – 推荐使用基于接口的编程方式
  • – 部分框架在处理某些功能时,会使用基于接口的代理模式,例如Spring JDBC框架在处理事务时
  • 在接口中,声明抽象方法时,仅以操作成功为前提来设计返回值类型(不考虑失败),如果业务在执行过程可能出现某些失败(不符合所设定的规则),可以通过抛出异常来表示!

    关于抛出的异常,通常是自定义的异常,并且,自定义异常通常是`RuntimeException`的子类,主要原因:

  • – 不必显式的抛出或捕获,因为业务逻辑层的异常永远是抛出的,而控制器层会调用业务逻辑层,在控制器层的Controller中其实也是永远抛出异常的,这些异常会通过Spring MVC统一处理异常的机制进行处理,关于异常的整个过程都是固定流程,所以,没有必要显式抛出或捕获
  • – 部分框架在处理某些事情时,默认只对`RuntimeException`的子孙类进行识别并处理,例如Spring JDBC框架在处理事务时
  • 所以,在实际编写业务逻辑层之前,应该先规划异常,例如先创建`ServiceException`类:

    public class ServiceException extends RuntimeException { //TODU}

    接下来,再创建具体的对应某种“失败”的异常,例如,在添加管理员时,可能因为“用户名已经存在”而失败,则创建对应的`UsernameDuplicateException`异常:

    public class UsernameDuplicateException extends ServiceException { //TODU}

    另外,当插入数据时,如果返回的受影响行数不是1时,必然是某种错误,则创建对应的插入数据异常:

    public class InsertException extends ServiceException { //TODU}

    关于抽象方法的参数,应该设计为客户端提交的数据类型或对应的封装类型,不可以是数据表对应的实体类型!如果使用封装的类型,这种类型在类名上应该添加某种后缀,例如`DTO`或其它后缀,例如:

    public class AdminAddNewDTO implements Serializable { private String username; private String password; private String nickname; private String avatar; private String phone; private String email; private String description; // Setters & Getters // hashCode(), equals() // toString()}

    然后,在`cn.celinf.boot.demo.service`包下声明接口及抽象方法:

    public interface IAdminService { void addNew(AdminAddNewDTO adminAddNewDTO);}

    并在以上`service`包下创建`impl`子包,再创建`AdminServiceImpl`类:

    import cn.celinf.boot.demo.entity.Admin;import cn.celinf.boot.demo.ex.InsertException;import cn.celinf.boot.demo.ex.UsernameDuplicateException;import cn.celinf.boot.demo.mapper.AdminMapper;import cn.celinf.boot.demo.pojo.dto.AdminAddNewDTO;import cn.celinf.boot.demo.service.IAdminService;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.time.LocalDateTime;@Servicepublic class AdminServiceImpl implements IAdminService { @Autowired private AdminMapper adminMapper; @Override public void addNew(AdminAddNewDTO adminAddNewDTO) { // 通过参数获取用户名 String username = adminAddNewDTO.getUsername(); // 调用adminMapper的Admin getByUsername(String username)方法执行查询 Admin queryResult = adminMapper.getByUsername(username); // 判断查询结果是否不为null if (queryResult != null) { // 是:表示用户名已经被占用,则抛出UsernameDuplicateException throw new UsernameDuplicateException(); } // 通过参数获取原密码 String password = adminAddNewDTO.getPassword(); // 通过加密方式,得到加密后的密码encodedPassword String encodedPassword = password; // 创建当前时间对象now > LocalDateTime.now() LocalDateTime now = LocalDateTime.now(); // 创建Admin对象 Admin admin = new Admin(); // 补全Admin对象的属性值:通过参数获取username,nickname…… admin.setUsername(username); admin.setNickname(adminAddNewDTO.getNickname()); admin.setAvatar(adminAddNewDTO.getAvatar()); admin.setPhone(adminAddNewDTO.getPhone()); admin.setEmail(adminAddNewDTO.getEmail()); admin.setDescription(adminAddNewDTO.getDescription()); // 以上这些从一个对象中把属性赋到另一个对象中,还可以使用: // BeanUtils.copyProperties(adminAddNewDTO, admin); // 补全Admin对象的属性值:password > encodedPassword admin.setPassword(encodedPassword); // 补全Admin对象的属性值:isEnable > 1 admin.setIsEnable(1); // 补全Admin对象的属性值:lastLoginIp > null // 补全Admin对象的属性值:loginCount > 0 admin.setLoginCount(0); // 补全Admin对象的属性值:gmtLastLogin > null // 补全Admin对象的属性值:gmtCreate > now admin.setGmtCreate(now); // 补全Admin对象的属性值:gmtModified > now admin.setGmtModified(now); // 调用adminMapper的insert(Admin admin)方法插入管理员数据,获取返回值 int rows = adminMapper.insert(admin); // 判断以上返回的结果是否不为1,抛出InsertException异常 if (rows != 1) { throw new InsertException(); } }}

    以上代码未实现对密码的加密处理!关于密码加密,相关的代码应该定义在别的某个类中,不应该直接将加密过程编写在以上代码中,因为加密的代码需要在多处应用(添加用户、用户登录、修改密码等),并且,从分工的角度上来看,也不应该是业务逻辑层的任务!所以,在`cn.celinf.boot.demo.util`(包不存在,则创建)下创建`PasswordEncoder`类,用于处理密码加密:

    package cn.celinf.boot.demo.util;@Componentpublic class PasswordEncoder { public String encode(String rawPassword) { return “aaa” + rawPassword + “aaa”; }}

    完成后,需要在`AdminServiceImpl`中自动装配以上`PasswordEncoder`,并在需要加密时调用`PasswordEncoder`对象的`encode()`方法。

    控制器层开发

    Spring MVC是用于处理控制器层开发的,在使用Spring Boot时,在`pom.xml`中添加`spring-boot-starter-web`即可整合Spring MVC框架及相关的常用依赖项(包含`jackson-databind`),可以将已存在的`spring-boot-starter`直接改为`spring-boot-starter-web`,因为在`spring-boot-starter-web`中已经包含了`spring-boot-starter`。

    先在项目的根包下创建`controller`子包,并在此子包下创建`AdminController`,此类应该添加`@RestController`和`@RequestMapping(value = “/admins”, produces = “application/json; charset=utf-8”)`注解,例如:

    @RestController@RequestMapping(values = “/admins”, produces = “application/json; charset=utf-8”)public class AdminController {}

    由于已经决定了服务器端响应时,将响应JSON格式的字符串,为保证能够响应JSON格式的结果,处理请求的方法返回值应该是自定义的数据类型,则从此前学习的`spring-mvc`项目中找到`JsonResult`类及相关类型,复制到当前项目中来。

    @Autowiredprivate IAdminService adminService;// 注意:暂时使用@RequestMapping,不要使用@PostMapping,以便于直接在浏览器中测试// http://localhost:8080/admins/add-new?username=root&password=1234@RequestMapping(“/add-new”)public JsonResult addNew(AdminAddNewDTO adminAddNewDTO) { adminService.addNew(adminAddNewDTO); return JsonResult.ok();}

    完成后,运行启动类,即可启动整个项目,在`spring-boot-starter-web`中,包含了Tomcat的依赖项,在启动时,会自动将当前项目打包并部署到此Tomcat上,所以,执行启动类时,会执行此Tomcat,同时,因为是内置的Tomcat,只为当前项目服务,所以,在将项目部署到Tomcat时,默认已经将Context Path(例如spring_mvc_war_exploded)配置为空字符串,所以,在启动项目后,访问的URL中并没有此前遇到的Context Path值。

    当项目启动成功后,即可在浏览器的地址栏中输入网址进行测试访问!

    【注意】:如果是未添加的管理员账号,可以成功执行结束,如果管理员账号已经存在,由于尚未处理异常,会提示500错误。

    public enum State {OK(200),ERR_USERNAME(201),ERR_PASSWORD(202),ERR_INSERT(500); // 新增的枚举值// 原有其它代码}

    然后,在`cn.celinf.boot.demo.controller`下创建`handler.GlobalExceptionHandler`类,用于统一处理异常,例如:

    package cn.celinf.boot.demo.controller.handler;import cn.celinf.boot.demo.ex.ServiceException;import cn.celinf.boot.demo.ex.UsernameDuplicateException;import cn.celinf.boot.demo.web.JsonResult;import cn.celinf.boot.demo.web.State;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(ServiceException.class) public JsonResult handleServiceException(ServiceException e) { if (e instanceof UsernameDuplicateException) { return JsonResult.fail(State.ERR_USERNAME, “用户名错误!”); } else { return JsonResult.fail(State.ERR_INSERT, “插入数据失败!”); } }}

    完成后,重新启动项目,当添加管理员时的用户名没有被占用时,将正常添加,当用户名已经被占用时,会根据处理异常的结果进行响应!

    由于在统一处理异常的机制下,同一种异常,无论是在哪种业务中出现,处理异常时的描述信息都是完全相同的,也无法精准的表达错误信息,这是不合适的!另外,基于面向对象的“分工”思想,关于错误信息(异常对应的描述信息),应该是由Service来描述,即“谁抛出谁描述”,因为抛出异常的代码片段是最了解、最明确出现异常的原因的!

    为了更好的描述异常的原因,应该在自定义的`ServiceException`和其子孙类异常中添加基于父类的全部构造方法(5个),然后,在`AdminServiceImpl`中,当抛出异常时,可以在异常的构造方法中添加`String`类型的参数,对异常发生的原因进行描述,例如:

    @Overridepublic void addNew(AdminAddNewDTO adminAddNewDTO) { // ===== 原有其它代码 ===== // 判断查询结果是否不为null if (queryResult != null) { // 是:表示用户名已经被占用,则抛出UsernameDuplicateException log.error(“此账号已经被占用,将抛出异常”); throw new UsernameDuplicateException(“添加管理员失败,用户名(” + username + “)已经被占用!”); } // ===== 原有其它代码 ===== // 判断以上返回的结果是否不为1,抛出InsertException异常 if (rows != 1) { throw new InsertException(“添加管理员失败,服务器忙,请稍后再次尝试!”); }}

    最后,在处理异常时,可以调用异常对象的`getMessage()`方法获取抛出时封装的描述信息,例如:

    @ExceptionHandler(ServiceException.class)public JsonResult handleServiceException(ServiceException e) { if (e instanceof UsernameDuplicateException) { return JsonResult.fail(State.ERR_USERNAME, e.getMessage()); } else { return JsonResult.fail(State.ERR_INSERT, e.getMessage()); }}

    完成后,再次重启项目,当用户名已经存在时,可以显示在Service中描述的错误信息!

    //响应的JSON数据例如:{“state”:200,”message”:null,”data”:null}//添加失败时,响应的JSON数据例如:{“state”:201,”message”:”添加管理员失败,用户名(liuguobin)已经被占用!”,”data”:null}

    可以看到,无论是成功还是失败,响应的JSON中都包含了不必要的数据(为`null`的数据),这些数据属性是没有必要响应到客户端的,如果需要去除这些不必要的值,可以在对应的属性上使用注解进行配置,例如:

    //NON_NULL 则响应的JSON中只会包含不为`null`的部分。@Datapublic class JsonResult implements Serializable { // 状态码,例如:200 private Integer state; // 消息,例如:”登录失败,用户名不存在” @JsonInclude(JsonInclude.Include.NON_NULL) private String message; // 数据 @JsonInclude(JsonInclude.Include.NON_NULL) private T data; // ===== 原有其它代码 =====}

    此注解还可以添加在类上,则作用于当前类中所有的属性,例如:

    @Data@JsonInclude(JsonInclude.Include.NON_NULL)public class JsonResult implements Serializable { // ===== 原有其它代码 =====}

    即使添加在类上,也只对当前类的3个属性有效,后续,当响应某些数据时,`data`属性可能是用户、商品、订单等类型,这些类型的数据中为`null`的部分依然会被响应到客户端去,所以,还需要对这些类型也添加相同的注解配置!

    以上做法相对比较繁琐,可以在`application.properties` / `application.yml`中添加全局配置,则作用于当前项目中所有响应时涉及的类,例如在`properties`中配置为:

    //yml文件配置写法spring: jackson: default-property-inclusion: non_null

    注意:当你需要在`yml`中添加以上配置时,前缀属性名可能已经存在,则不允许出现重复的前缀属性名的:

    spring: profiles: active: dev jackson: default-property-inclusion: non_null

    最后,以上配置只是“默认”配置,如果在某些类型中还有不同的配置需求,仍可以在类或属性上通过`@JsonInclude`进行配置。

    15. 解决跨域问题

    在使用前后端分离的开发模式下,前端项目和后端项目可能是2个完全不同的项目,并且,各自己独立开发,独立部署,在这种做法中,如果前端直接向后端发送异步请求,默认情况下,在前端会出现类似以下错误:

    //web端 跨域错误Access to XMLHttpRequest at ‘http://localhost:8080/admins/add-new’ from origin ‘http://localhost:8081’ has been blocked by CORS policy:No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

    以上错误信息的关键字是`CORS`,通常称之为“跨域问题”。

    在基于Spring MVC框架的项目中,当需要解决跨域问题时,需要一个Spring MVC的配置类(实现了`WebMvcConfigurer`接口的类),并重写其中的方法,以允许指定条件的跨域访问,例如:

    //解决跨域的config配置import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.CorsRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class SpringMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(“/**”) .allowedOriginPatterns(“*”) .allowedMethods(“*”) .allowedHeaders(“*”) .allowCredentials(true) .maxAge(3600); }}

    16. 关于客户端提交请求参数的格式

    通常,客户端向服务器端发送请求时,请求参数可以有2种形式,第1种是直接通过`&`拼接各参数与值,例如:

    // FormData//第一种 直接url地址拼接// username=root&password=123456&nickname=jackson&phone=13800138001let data = ‘username=’ + this.ruleForm.username+ ‘&password=’ + this.ruleForm.password+ ‘&nickname=’ + this.ruleForm.nickname+ ‘&phone=’ + this.ruleForm.phone//第2种方式是使用JSON语法来组织各参数与值let data = {‘username’: this.ruleForm.username, // ‘root”password’: this.ruleForm.password, // ‘123456”nickname’: this.ruleForm.nickname, // ‘jackson”phone’: this.ruleForm.phone, // ‘13800138001’};

    具体使用哪种做法,取决于服务器端的设计:

    – 如果服务器端处理请求的方法中,在参数前添加了`@RequestBody`,则允许使用以上第2种做法(JSON数据)提交请求参数,不允许使用以上第1种做法(使用`&`拼接)

    – 如果没有使用`@RequestBody`,则只能使用以上第1种做法

    学习记录,如有侵权请联系删除

    郑重声明:本文内容及图片均整理自互联网,不代表本站立场,版权归原作者所有,如有侵权请联系管理员(admin#wlmqw.com)删除。
    (0)
    用户投稿
    上一篇 2022年6月13日
    下一篇 2022年6月13日

    相关推荐

    联系我们

    联系邮箱:admin#wlmqw.com
    工作时间:周一至周五,10:30-18:30,节假日休息