前后端分离Controller层开发 笔记

前后端分离Controller层开发 笔记

如要开发如下用户接口:

1.登录

POST /user/login

request

Content-Type: application/json

{
	"username":"admin",
	"password":"admin",
}

response

fail

{
    "status": 1,
    "msg": "密码错误"
}

success

{
    "status": 0,
    "data": {
        "id": 12,
        "username": "aaa",
        "email": "[email protected]",
        "phone": null,
        "role": 0,
        "createTime": 1479048325000,
        "updateTime": 1479048325000
    }
}

2.注册

POST /user/register

request

{
	"username":"admin",
	"password":"admin",
	"email":"[email protected]"
}

response

success

{
    "status": 0,
    "msg": "成功"
}

fail

{
    "status": 2,
    "msg": "用户已存在"
}

3.获取登录用户信息

GET /user

request

无参数

response

success

{
    "status": 0,
    "data": {
        "id": 12,
        "username": "aaa",
        "email": "[email protected]",
        "phone": null,
        "role": 0,
        "createTime": 1479048325000,
        "updateTime": 1479048325000
    }
}

fail

{
    "status": 10,
    "msg": "用户未登录,无法获取当前用户信息"
}


4.退出登录

**POST /user/logout

request

response

success

{
    "status": 0,
    "msg": "退出成功"
}

fail

{
    "status": -1,
    "msg": "服务端异常"
}

分析接口:

由于项目是前后端分离的项目,故所有controller返回值都是json格式,可在controller层添加@ResponseBody实现。

由于前段发送http数据采用Content-Type: application/json 格式,故需要 在接受参数时使用@RequestBody 接受参数(如果前端采用:x-www-form-urlencoded ,则controller接口方法需要使用@RequestParam(value= "") 注解 接受参数)

分析所有用户接口,返回值有 三个属性 status,msg, data,为便于处理返回结果值,可新建返回值对象ResponseVo, 其中 data有时需要返回,有时不需要返回,可在ResponseVo对象添加@JsonInclude(value = JsonInclude.Include.NON_NULL)注解使其在序列化时排除属性为null的值。同时考虑到data中的数据为User对象,后期也可能会是其他对象,故ResponseVo对象的data属性需要用泛型

ResponseVo对象设计如下:

package cn.blogsx.mimall.vo;

import cn.blogsx.mimall.enums.ResponseEnum;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import org.springframework.validation.BindingResult;

import java.util.Objects;

/**
 * @author Alex
 * @create 2020-03-26 20:08
 **/
@Data
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class ResponseVo<T> {
    private Integer status;

    private String msg;

    private T data;

    private ResponseVo(Integer status, String msg) {
        this.status = status;
        this.msg = msg;
    }
    private ResponseVo(Integer status, T data) {
        this.status = status;
        this.data=data;
    }

}

根据需要添加构造方法ResponseVo(Integer status, String msg)、ResponseVo(Integer status, T data)

不同的错误需要不同的方法去返回相应,故可添加如下静态方法:

 public static <T> ResponseVo<T> successByMsg(String msg) {
        return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(),msg);
    }
    public static <T> ResponseVo<T> success(T data) {
        return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(),data);
    }
    public static <T> ResponseVo<T> success() {
        return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(),ResponseEnum.SUCCESS.getDesc());
    }


    public static <T> ResponseVo<T> error(ResponseEnum responseEnum) {
        return new ResponseVo<>(responseEnum.getCode(),responseEnum.getDesc());
    }
    public static <T> ResponseVo<T> error(ResponseEnum responseEnum,String msg) {
        return new ResponseVo<>(responseEnum.getCode(),msg);
    }
    public static <T> ResponseVo<T> error(ResponseEnum responseEnum, BindingResult bindingResult) {
        return new ResponseVo<>(responseEnum.getCode(),
                Objects.requireNonNull(bindingResult.getFieldError()).getField()+" "+
                        bindingResult.getFieldError().getDefaultMessage());
    }

分析属性:

status属性的值为 整数,故使用Integer类型,为避免硬编码,status应使用 枚举对值做约束。

msg属性分几种情况:0对应的 成功、2对应的 用户已存在、10对应的 用户未登录、-1对应的服务端错误(泛类型错误)

status和msg存在对应关系,可用枚举类ResponseEnum做对应

package cn.blogsx.mimall.enums;

import lombok.Getter;

@Getter
public enum ResponseEnum {
    ERROR(-1,"服务端错误"),

    SUCCESS(0,"成功"),

    PASSWORD_ERROR(1,"密码错误"),

    USERNNAME_EXIST(2,"用户名已存在"),

    PARAM_ERROR(3,"参数错误"),

    EMAIL_EXIST(4,"邮箱已存在"),

    NEED_LOGIN(10,"用户未登录,请先登录"),

    USERNAME_OR_PASSWORD_ERROR(11,"用户名或密码错误"),
    ;

    Integer code;

    String desc;

    ResponseEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

功能分析:

由于用户登录 只需要传递username和password而不需要传递其他参数,并且需要做参数校验,所以为最好还是专门新建一个类专门用作传参。注册 同理

UserLoginForm类传传参封装使用:

package cn.blogsx.mimall.form;

import lombok.Data;

import javax.validation.constraints.NotBlank;

/**
 * @author Alex
 * @create 2020-03-26 20:50
 **/
@Data
public class UserLoginForm {

    @NotBlank
    private String username;

    @NotBlank
    private String password;
}

UserRegisterForm类传传参封装使用:

package cn.blogsx.mimall.form;

import lombok.Data;

import javax.validation.constraints.NotBlank;

@Data
public class UserRegisterForm {

    @NotBlank
    private String username;

    @NotBlank
    private String password;

    @NotBlank
    private String email;
}

在注册时需要将UserRegisterForm对象的属性拷贝成user对象才能插入数据库中,可使用Spring中提供的BeanUtils.copyProperties(userRegisterForm, user);方法实现。

关于javax.validation.constraints相关注解说明:

@NotBlank //用于String 判断空格
@NotEmpty //用于集合
@NotNull  //判断null

使用如上注解校验参数时只需在Controller层的接口方法上添加@Valid 以及 BindingResult bindingResult 形式参数,并在方法体中使用如下判断并返回前端参数异常信息:

if (bindingResult.hasErrors()) {
    
            return ResponseVo.error(PARAM_ERROR, bindingResult);
  }

以上注解还可以定义参数异常原因:只需在使用注解时添加(message = "用户名不能为空") 即可使用如下方法得到并返回给前端:

log.error("注册提交参数有误,{} {}",
                    Objects.requireNonNull(bindingResult.getFieldError()).getField(),
                    bindingResult.getFieldError().getDefaultMessage());

Objects.requireNonNull(bindingResult.getFieldError()).getField()是得到有误从参数字段,bindingResult.getFieldError().getDefaultMessage()是得到我们在@NotBlank(message = "用户名不能为空")写入的message值,如果不写,则默认为 “不能为空” 四个字。

具体实现接口的Controller方法:

package cn.blogsx.mimall.controller;

import cn.blogsx.mimall.consts.MiMallConst;
import cn.blogsx.mimall.form.UserLoginForm;
import cn.blogsx.mimall.form.UserRegisterForm;
import cn.blogsx.mimall.pojo.User;
import cn.blogsx.mimall.service.IUserService;
import cn.blogsx.mimall.vo.ResponseVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.util.Objects;

import static cn.blogsx.mimall.enums.ResponseEnum.PARAM_ERROR;

/**
 * @author Alex
 * @create 2020-03-26 19:43
 **/
@RestController
@Slf4j
public class UserController {

    @Autowired
    private IUserService userService;

    @PostMapping("/user/register")
    public ResponseVo<User> register(@Valid @RequestBody UserRegisterForm userRegisterForm,
                                     BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            log.error("注册提交参数有误,{} {}",
                    Objects.requireNonNull(bindingResult.getFieldError()).getField(),
                    bindingResult.getFieldError().getDefaultMessage());
            return ResponseVo.error(PARAM_ERROR, bindingResult);
        }
        User user = new User();
        BeanUtils.copyProperties(userRegisterForm, user);//使用Spring中拷贝对象之间的方法
        return userService.register(user);
    }

    @PostMapping("/user/login")
    public ResponseVo<User> login(@Valid @RequestBody UserLoginForm userLoginForm
            , BindingResult bindingResult, HttpSession session) {
        if (bindingResult.hasErrors()) {
            return ResponseVo.error(PARAM_ERROR, bindingResult);
        }
        ResponseVo<User> userResponseVo = userService.login(userLoginForm.getUsername(), userLoginForm.getPassword());

        //设置session
        session.setAttribute(MiMallConst.CURRENT_USER, userResponseVo.getData());
        log.info("login sessionId={}",session.getId());
        return userResponseVo;

    }

    //session保存在内存里改进版:token+redis
    @GetMapping("/user")
    public ResponseVo<User> userInfo(HttpSession httpSession) {
        User user = (User) httpSession.getAttribute(MiMallConst.CURRENT_USER);
        return ResponseVo.success(user);
    }

    @PostMapping("/user/logout")
    public ResponseVo logout(HttpSession httpSession) {
        log.info("/user sessionId={}",httpSession.getId());
        httpSession.removeAttribute(MiMallConst.CURRENT_USER);
        return ResponseVo.success();
    }
}

登录有保存信息到session中,为降低耦合性,也可在专门新建常量MiMallConst类存储session键常量:

package cn.blogsx.mimall.consts;

/**
 * @author Alex
 * @create 2020-03-26 22:33
 **/
public class MiMallConst {
    public static final String CURRENT_USER = "currentUser";
}

session默认有效时间为30分钟。也可在application文件中做如下配置:

server:
  servlet:
    session:
      timeout: 120 #以秒为单位    两分钟

为方便统一管理判断用户是否登录,可使用拦截器做统一拦截:

package cn.blogsx.mimall;

import cn.blogsx.mimall.consts.MiMallConst;
import cn.blogsx.mimall.exception.UserloginException;
import cn.blogsx.mimall.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @Dscription: 登录拦截器
 * @author Alex
 * @create 2020-03-27 10:13
 **/
@Slf4j
public class UserLoginInterceptor implements HandlerInterceptor {
    /**
     * true 表示继续流程,false 表示中断
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("preHandle...");

        User user = (User) request.getSession().getAttribute(MiMallConst.CURRENT_USER);
        if (user == null) {
            log.info("user=null");
            throw new UserloginException();

//            return ResponseVo.error(ResponseEnum.NEED_LOGIN);
//            return false;
        }
        return true;
    }
}

此时拦截器还未生效,需要在配置拦截路径,新建InterceptorConfig配置类:

package cn.blogsx.mimall;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author Alex
 * @create 2020-03-27 10:21
 **/
@Configuration //使用该注解才能在Spring启动后起作用
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserLoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login","/user/register");//登录注册接口无需拦截
    }
}

Service接口及方法:

package cn.blogsx.mimall.service;


import cn.blogsx.mimall.pojo.User;
import cn.blogsx.mimall.vo.ResponseVo;

public interface IUserService {
    /**
     * 注册
     */
    ResponseVo<User> register(User user);

    /**
     * 登录
     */
    ResponseVo<User> login(String username,String password);

}

实现方法:

package cn.blogsx.mimall.service.impl;

import cn.blogsx.mimall.dao.UserMapper;
import cn.blogsx.mimall.enums.RoleEnum;
import cn.blogsx.mimall.pojo.User;
import cn.blogsx.mimall.service.IUserService;
import cn.blogsx.mimall.vo.ResponseVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import java.nio.charset.StandardCharsets;

import static cn.blogsx.mimall.enums.ResponseEnum.*;

/**
 * @author Alex
 * @create 2020-03-26 17:34
 **/
@Service
public class UserServiceImpl implements IUserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public ResponseVo<User> register(User user) {

         //username不能重复
        int countByUsername = userMapper.countByUsername(user.getUsername());
        if(countByUsername>0) {
            return ResponseVo.error(USERNNAME_EXIST);
        }

        //email不能重复
        int countByEmail = userMapper.countByEmail(user.getEmail());
        if(countByEmail>0) {
            return ResponseVo.error(EMAIL_EXIST);
        }
        user.setRole(RoleEnum.CUSTOMER.getCode());

        //MD5摘要算法(Spring自带)
        user.setPassword( DigestUtils.md5DigestAsHex(user.getPassword()
                .getBytes(StandardCharsets.UTF_8)));

        //写入数据库
        int resultCount = userMapper.insertSelective(user);
        if(resultCount==0) {
            return ResponseVo.error(ERROR);
        }

        return ResponseVo.success();

    }

    @Override
    public ResponseVo<User> login(String username, String password) {
        User user = userMapper.selectByUsername(username);//登录查询推荐只使用username查询即可
        if(user == null) {
            //用户不存在,返回 用户名或密码错误
            return ResponseVo.error(USERNAME_OR_PASSWORD_ERROR);
        }
        if(!user.getPassword().equalsIgnoreCase(
                DigestUtils.md5DigestAsHex(
                password.getBytes(StandardCharsets.UTF_8)))) {
            //密码错误,返回 用户名或密码错误
            return ResponseVo.error(USERNAME_OR_PASSWORD_ERROR);

        }
        user.setPassword("");
        return ResponseVo.success(user);
    }
}

使用Srping框架无需再单独写Utils方法做MD5加密,Spring中自带MD5加密方法: DigestUtils.md5DigestAsHex

如果服务端发生其他异常,例如为500的异常也需要返回json格式的提示,此时可以新建RuntimeExcetion异常处理类:

package cn.blogsx.mimall.exception;

import cn.blogsx.mimall.enums.ResponseEnum;
import cn.blogsx.mimall.vo.ResponseVo;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import static cn.blogsx.mimall.enums.ResponseEnum.ERROR;

/**
 * @author Alex
 * @create 2020-03-26 21:43
 **/
@ControllerAdvice
public class RuntimeExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
//    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//可修改http协议状态码
    public ResponseVo handler(RuntimeException e) {
        return ResponseVo.error(ERROR,e.getMessage());
    }

    @ExceptionHandler(UserloginException.class)
    @ResponseBody
    public ResponseVo userLoginHandler() {
        return ResponseVo.error(ResponseEnum.NEED_LOGIN);
    }
}

由于用户账号和密码的错误采用HandlerInterceptor的实现方法拦截,且其preHandle方法返回值只能是true或false以用作判断是否继续流程(true 表示继续流程,false 表示中断)所以为了返回给前端 可新建UserloginException异常做异常处理

public class UserloginException extends RuntimeException {
}

只需继承RuntimeException即可,只要有该类名即可,真正处理异常的地方在 RuntimeExceptionHandler中。

关于测试:一般来说 开发人员单测只需测试service即可,controller层为测试人员测试。

idea中可在serviceimpl 类中 右键空白处-> Go To ->test 即可在mavne test目录下自动生成测试类。

Maven 打包是执行单测:

mvn clean package

Mavnen打包时跳过单测:

mvn clean package -Dmaven.test.skip=true

猜你喜欢

转载自www.cnblogs.com/sxblog/p/12580741.html