Springboot项目搭建问题记录

记录毕业设计项目搭建遇见的问题与坑,欢迎大佬指正

一、项目后端技术

此次毕业设计使用前后端分离,我主要负责的是后端,以及数据库的部分。
介绍项目后端使用的技术:

  • Springboot
  • Mybatis
  • MySQL
  • Swagger

二、项目搭建

在使用idea创建好Springboot项目之后,要做很多的配置,我们这是一个前后端分离的项目,首当其冲的是跨域配置。

2.1 跨域配置

不知道跨域的可以参考这篇文章:什么是跨域?跨域解决方法

@Configuration
public class CorsConfig {private CorsConfiguration buildConfig() {CorsConfiguration corsConfiguration = new CorsConfiguration();//允许任何域名corsConfiguration.addAllowedOrigin("*");//允许任何头corsConfiguration.addAllowedHeader("*");//允许任何方法corsConfiguration.addAllowedMethod("*");return corsConfiguration;}@Beanpublic CorsFilter corsFilter() {UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();//注册source.registerCorsConfiguration("/**", buildConfig());return new CorsFilter(source);}
}

2.2 响应格式

2.2.1 之前的写法

在此次项目之前,我都是采用如下格式的代码,每次需要返回时就将响应状态码、响应信息、响应数据封装到Result对象里面进行返回。

public class Result<T> {private Integer code;private String message;private T data;public Result(Integer code, String message, T data) {this.code = code;this.message = message;this.data = data;}public Result(Integer code, String message) {this.code = code;this.message = message;}
}// 省略get和set方法
2.2.2 现在的写法

而这次我决定不在使用使用这样的返回方式。我参考这篇转载的文章《看看人家那后端API接口写得,那叫一个巴适~,再看看我的,像坨屎!》对响应格式进行了更改,具体思路请参考这篇文章,我这里只放上代码。

1、首先创建enum对响应的内容进行了枚举:

public enum ResultCode {/*成功状态码*/SUCCESS(200, "成功"),/*参数错误状态码*/PARAM_IS_INVALID(1001, "参数无效"),PARAM_IS_BLANK(1002, "参数为空"),PARAM_TYPE_BIND_ERROR(1003, "参数类型错误"),PARAM_NOT_COMPLETE(1004, "参数缺失"),/*用户错误*/USER_NOT_LOGGED_IN(2001, "用户未登录,访问路径需验证,请登录"),USER_NOT_EXIST(2002, "账户不存在"),USER_LOGIN_PASSWORD_ERROR(2003, "密码不正确"),USER_ACCOUNT_FORBIDDEN(2004, "用户被禁用"),/*系统错误*/SYSTEM_ERROR(404, "系统错误"),/*token校验错误*/JWT_ERRCODE_EXPIRE(405, "token校验错误"),JWT_ERRCODE_FAIL(405, "token签名错误"),/*** 无权限*/USER_NOT_POWER(411, "权限不足"),/*** 用户错误*/USER_ADD_ERROR(412, "添加失败"),USER_UPDATE_ERROR(413, "更新失败"),user_delete_user(414,"删除用户失败"),/*** json格式错误*/JSON_PARSE_ERROR(415, "json格式错误");/*** 状态码*/private final Integer code;/*** 提示信息*/private final String message;private ResultCode(Integer code, String message) {this.code = code;this.message = message;}public Integer getCode() {return code;}public String getMessage() {return message;}
}

2、设置相应格式:

public class Result<T> implements Serializable {private Integer code;private String message;/*** 返回的数据*/private T data;public Result() {}// 返回成功public static Result success(){Result result = new Result();result.setCode(ResultCode.SUCCESS.getCode());result.setMessage(ResultCode.SUCCESS.getMessage());return result;}// 返回成功public static Result success(Object data){Result result = new Result();result.setCode(ResultCode.SUCCESS.getCode());result.setMessage(ResultCode.SUCCESS.getMessage());result.setData(data);return result;}// 返回失败public static Result failure(ResultCode resultCode){Result result = new Result();result.setCode(resultCode.getCode());result.setMessage(resultCode.getMessage());return result;}// 返回失败public static Result failure(ResultCode resultCode, Object data){Result result = new Result();result.setCode(resultCode.getCode());result.setMessage(resultCode.getMessage());result.setData(data);return result;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public T getData() {return data;}public void setData(T data) {this.data = data;}
}

3、 添加注解类

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface ResponseResult {
}

4、 创建拦截器

@Component
public class ResponseResultInterceptor implements HandlerInterceptor {// 标记名称public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 请求方法if (handler instanceof HandlerMethod){final HandlerMethod handlerMethod = (HandlerMethod) handler;final Class<?> clazz = handlerMethod.getBeanType();final Method method = handlerMethod.getMethod();// 判断是否在类的对象上加了注解if (clazz.isAnnotationPresent(ResponseResult.class)){// 设置此请求返回体,需要包装,往下传递,在ResponseBodyAdvice接口进行判断request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));}else if (method.isAnnotationPresent(ResponseResult.class)){// 设置此请求返回体,需要包装,往下传递,在ResponseBodyAdvice接口进行判断request.setAttribute(RESPONSE_RESULT_ANN, method.getAnnotation(ResponseResult.class));}}return true;}
}

5、添加拦截器配置

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {@Beanpublic WebMvcConfigurer webMvcConfigurer(){return new WebMvcConfigurer() {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new ResponseResultInterceptor()).addPathPatterns("/**");}};}
}

6、 重写响应体

@Slf4j
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice {// 标记名称public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";// 无论如何都要重写返回体@Overridepublic boolean supports(MethodParameter methodParameter, Class aClass) {ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = sra.getRequest();ResponseResult result = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);return result == null ? false : true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass,ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {return Result.success(body);}
}

如登录请求:

    @PostMapping(value = "/login")@ResponseResultpublic String login(@RequestBody LoginVO loginVO) throws BaseException {User user = new User();BeanUtils.copyProperties(loginVO, user);String token = loginService.login(user);return token;}

这样就配置好了,这样我们的controller就不用每次都写上Result了。但是这样写仍然存在问题:

  1. 这样Swagger接口文档显示的响应格式就不正确了,显示的响应格式就是data的内容.这个问题我也没有解决办法,大佬有办法欢迎指正
  2. 当我们返回的数据未String时,就会出现java.lang.ClassCastException,这个问题的解决办法,我参考的这篇文章:《SpringBoot 使用 beforeBodyWrite 实现统一的接口返回类型》去解决的。

解决办法重写configureMessageConverters

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {@Beanpublic WebMvcConfigurer webMvcConfigurer(){return new WebMvcConfigurer() {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new Jwtinterceptor()).addPathPatterns("/**").excludePathPatterns("/user/login","/doc.html","/webjars/**");registry.addInterceptor(new ResponseResultInterceptor()).addPathPatterns("/**");}};}// 答主给了两种方式,经过我测试只使用第二种是可以的,只是用第一种还是会报类型转换异常@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {// 第一种方式是将 json 处理的转换器放到第一位,使得先让 json 转换器处理返回值,这样 String转换器就处理不了了。converters.add(new MappingJackson2HttpMessageConverter());// 第二种就是把String类型的转换器去掉,不使用String类型的转换器converters.removeIf(httpMessageConverter -> httpMessageConverter.getClass() == StringHttpMessageConverter.class);}
}

那么为啥我把两种方式都写上?

我有这样的需求:后端为null返回给前端转换为空串,空数组转换为[],空map转换为{};前端返回给我的空串我需要转换成null
这时有查的有两种方式:使用Jackson或者Fastjson。经过我测试,不知道为啥我是用Fastjson无法出现效果(开始有效果,不知道我改动了啥就没效果了,一直没找到原因),于是我就使用了Jackson处理.

MyMvcConfig添加配置:

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {@Beanpublic WebMvcConfigurer webMvcConfigurer(){return new WebMvcConfigurer() {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new Jwtinterceptor()).addPathPatterns("/**").excludePathPatterns("/user/login","/doc.html","/webjars/**");registry.addInterceptor(new ResponseResultInterceptor()).addPathPatterns("/**");}};}/*** 使用阿里 fastjson 作为 JSON MessageConverter* @param converters*/@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {// 第一种方式是将 json 处理的转换器放到第一位,使得先让 json 转换器处理返回值,这样 String转换器就处理不了了。converters.add(new MappingJackson2HttpMessageConverter());// 第二种就是把String类型的转换器去掉,不使用String类型的转换器converters.removeIf(httpMessageConverter -> httpMessageConverter.getClass() == StringHttpMessageConverter.class);}// 处理null与空串、[]、{}之间的相互转换@Beanpublic MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper){objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {@Overridepublic void serialize(Object param, JsonGenerator jsonGenerator,SerializerProvider paramSerializerProvider) throws IOException {String fieldName = jsonGenerator.getOutputContext().getCurrentName();try {// 反射获取字段类型Field field = jsonGenerator.getCurrentValue().getClass().getDeclaredField(fieldName);Class<?> type = field.getType();if (Objects.equals(type, String.class)){// 处理字符串的null值jsonGenerator.writeString("");return;}else if (Objects.equals(type, List.class)){// 处理数组集合的null值jsonGenerator.writeStartArray();jsonGenerator.writeEndArray();return;}else if (Objects.equals(type, Boolean.class)){// 处理数值型的null值jsonGenerator.writeNumber(0);return;}else if (Objects.equals(type, Map.class)){// 处理对象型null值jsonGenerator.writeStartObject();jsonGenerator.writeEndObject();return;}} catch (NoSuchFieldException e) {e.printStackTrace();}//默认返回“”jsonGenerator.writeString("");}});// 反序列化jsonSimpleModule module = new SimpleModule();module.addDeserializer(String.class, new StdDeserializer<String>(String.class) {@Overridepublic String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {String result = StringDeserializer.instance.deserialize(p, ctxt);if (StringUtils.isEmpty(result)) {return null;}return result;}});objectMapper.registerModule(module);objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper);return mappingJackson2HttpMessageConverter;}
}

2.3 异常处理

上面的响应体并没有将异常进行处理,接下来我们需要考虑对异常的处理,自定义异常:

public class BaseException extends Exception{private ResultCode resultCode;private Object data;public BaseException(ResultCode resultCode, Object data){super(resultCode.getMessage());this.resultCode = resultCode;this.data = data;}public BaseException(ResultCode resultCode){super(resultCode.getMessage());this.resultCode = resultCode;}// 省略get与set方法
}

全局异常处理:

@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(value = Exception.class)@ResponseBodypublic BaseException jsonErrorHandler(HttpServletRequest request, Exception e) throws Exception {e.printStackTrace();if (e instanceof BaseException) {return (BaseException) e;} else if (e instanceof HttpMessageNotReadableException) {return new BaseException(ResultCode.JSON_PARSE_ERROR, e.getMessage());} else {return new BaseException(ResultCode.SYSTEM_ERROR, e.getMessage());}}
}

这样配置之后,加上重写了响应体,如果在crotrollerservive中出现异常每次都会返回Result.success(body);,body是一个异常,很明显这样的格式不是我们想要的。修改ResponseResultHandlerbeforeBodyWrite方法,对异常重写返回格式。

    @Overridepublic Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass,ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {// 对异常进行处理if (body instanceof BaseException){BaseException baseException = (BaseException) body;ResultCode resultCode = baseException.getResultCode();return Result.failure(resultCode, baseException.getData());}return Result.success(body);}

这样看似没有问题了,但是仍然有问题。接下来我考虑使用JWT进行验证用户。

2.4 JWT验证

可供参考的文章:《使用JWT实现单点登录(完全跨域方案)》

public interface SystemConstant {/*** token密钥*/String JWT_SECERT = "xxxxxxxxxxxxxx";/*** token 有效时间* 600000L = 30分钟*/long JWT_TIME = 3600000L;/*** token电话*/String TOKEN_TELEPHONE = "token_telephone";/*** token用户类型*/String TOKEN_USER_TYPE = "token_user_type";
}

新建JWT工具类

public class JwtUtils {/*** 签发JWT** @param id* @param subject   可以是JSON数据 尽可能少* @return String**/public static String createJWT(String id, String subject, UserToken userToken) {long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);String jwtSecert = SystemConstant.JWT_SECERT;JwtBuilder builder = Jwts.builder().setId(id).setSubject(subject) // 主题.setIssuer("user") // 签发者.setIssuedAt(now) // 签发时间// 签名算法以及密匙.signWith(SignatureAlgorithm.HS256, jwtSecert) // 签名算法以及密匙.claim(SystemConstant.TOKEN_TELEPHONE, userToken.getTelephone())// 加密电话号码.claim(SystemConstant.TOKEN_USER_TYPE, userToken.getUserType());// 加密用户类型long jwtTime = SystemConstant.JWT_TIME;if (jwtTime >= 0) {long expMillis = nowMillis + jwtTime;Date expDate = new Date(expMillis);builder.setExpiration(expDate); // 过期时间}return builder.compact();}/*** 验证JWT** @param jwtStr* @return*/public static Result validateJWT(String jwtStr) throws BaseException {Result result = new Result();Claims claims = null;try {claims = parseJWT(jwtStr);ResultCode success = ResultCode.SUCCESS;result.setCode(success.getCode());result.setMessage(success.getMessage());result.setData(claims);} catch (ExpiredJwtException e) {ResultCode jwtErrcodeExpire = ResultCode.JWT_ERRCODE_EXPIRE;throw new BaseException(jwtErrcodeExpire);} catch (SignatureException e) {ResultCode jwtErrcodeFail = ResultCode.JWT_ERRCODE_FAIL;throw new BaseException(jwtErrcodeFail);} catch (Exception e) {ResultCode jwtErrcodeExpire = ResultCode.JWT_ERRCODE_EXPIRE;throw new BaseException(jwtErrcodeExpire);}return result;}/**** 解析JWT字符串** @param jwt* @return* @throws Exception*/public static Claims parseJWT(String jwt) throws Exception {return Jwts.parser().setSigningKey(SystemConstant.JWT_SECERT).parseClaimsJws(jwt).getBody();}
}

配置JWT拦截器

@Component
public class Jwtinterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//只要解析正常就放行。具体能不能操作还是在具体的操作中去判断。//拦截器只是负责把头请求头中包含token的令牌进行一个解析验证。String token = request.getHeader("Authorization");if (token != null && !"".equals(token)) {//如果有包含有Authorization头信息,就对其进行解析//对令牌进行验证try {Claims claims = JwtUtils.parseJWT(token);String telephone = (String) claims.get(SystemConstant.TOKEN_TELEPHONE);Integer userType = (Integer) claims.get(SystemConstant.TOKEN_USER_TYPE);request.setAttribute(SystemConstant.TOKEN_TELEPHONE, telephone);request.setAttribute(SystemConstant.TOKEN_USER_TYPE, userType);} catch (Exception e) {// 令牌不正确throw new BaseException(ResultCode.JWT_ERRCODE_EXPIRE,e.getMessage());}}return true;}
}

JWT拦截器进行配置

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {@Beanpublic WebMvcConfigurer webMvcConfigurer(){return new WebMvcConfigurer() {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new Jwtinterceptor()).addPathPatterns("/**").excludePathPatterns("/user/login","/doc.html","/webjars/**");registry.addInterceptor(new ResponseResultInterceptor()).addPathPatterns("/**");}};}
}

这样配置后,就可以通过HttpServletRequest的对象获取解密后的值,如:

public class TokenInfo {/*** @auther: zqq* @date: 21/1/10 20:58* @Description: 获取登录者的用户类型*/public static Integer getUserType(HttpServletRequest request){return (Integer) request.getAttribute(SystemConstant.TOKEN_USER_TYPE);}/*** @auther: zqq* @date: 21/1/10 20:59* @Description: 获取登陆者的电话号码*/public static String getTelephone(HttpServletRequest request){return (String) request.getAttribute(SystemConstant.TOKEN_TELEPHONE);}
}

这样就完成了JWT用户验证。
使用举例:

    @GetMapping("/userInfo")public LoginUserInfo userInfo(HttpServletRequest request){// 通过request获取解密后的电话号码(即登录账户)String telephone = getTelephone(request);User user = loginService.queryUserInfoByTelephone(telephone);LoginUserInfo loginUserInfo = new LoginUserInfo();BeanUtils.copyProperties(user, loginUserInfo);return loginUserInfo;}

到此JWT就配置好了。还记得在2.3最后说的仍然存在问题吗。没错,问题就在jwt里面。我们在进行解析的时候,使用了拦截器Jwtinterceptor此时请求还没有到达controller层,这时如果JWT解析验证的出现异常,自定义的@ResponseResult注解没有生效(ResponseResultInterceptor拦截器检测不到这个注解),这样进入重写方法体的时候ResponseResultHandlersupports方法就会返回false。就不会进入beforeBodyWrite方法。那么返回的格式就不再是满足我们要求的格式。

解决方法,
第一步:给所有的异常返回值添加Attribute标记:

@ControllerAdvice
public class GlobalExceptionHandler {public static final String EXCEPTION = "EXCEPTION";@ExceptionHandler(value = Exception.class)@ResponseBodypublic BaseException jsonErrorHandler(HttpServletRequest request, Exception e) throws Exception {request.setAttribute(EXCEPTION, BaseException.class);e.printStackTrace();if (e instanceof BaseException) {return (BaseException) e;} else if (e instanceof HttpMessageNotReadableException) {return new BaseException(ResultCode.JSON_PARSE_ERROR, e.getMessage());} else {return new BaseException(ResultCode.SYSTEM_ERROR, e.getMessage());}}
}

第二步:修改ResponseResultHandlersupports方法:

    @Overridepublic boolean supports(MethodParameter methodParameter, Class aClass) {ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = sra.getRequest();// 所有异常都需要进行重写方法体Object exception = request.getAttribute(EXCEPTION);if (null != exception){return true;}ResponseResult result = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);return result == null ? false : true;}

这里你可能要问,那不然让这个方法任何情况下都返回true,全部都要重写方法体?
这个方法就是我要说的下面的一个坑,因为是前后端分离项目,所以我使用了Swagger作为接口文档,如果全部进行重写,那么swagger的所有资源都访问不到,页面就会报错:请确保swagger资源正确。

2.5 Swagger配置

pom.xml


<dependency><groupId>io.springfoxgroupId><artifactId>springfox-swagger2artifactId><version>2.9.2version>
dependency>
<dependency><groupId>com.github.xiaoymingroupId><artifactId>swagger-bootstrap-uiartifactId><version>1.9.6version>
dependency>

Swagger配置:

@Configuration
@EnableSwagger2
@EnableSwaggerBootstrapUI
public class SwaggerConfiguration {@Beanpublic Docket createRestApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.cduestc.kcwxy.controller"))//controller包的位置.paths(PathSelectors.any()).build();}private ApiInfo apiInfo() {return new ApiInfoBuilder().title("xxx系统接口文档").description("科城微校园是一款针对于成都学院的学生校园生活设计的小程序").termsOfServiceUrl("http://www.youcongtech.com").contact("zqqpluto@qq.com").version("1.0").build();}
}

注意:

  1. 访问接口文档的方法:http://IP地址:端口/项目名/doc.html没有项目名:http://IP地址:端口/doc.html
  2. 千万不要让ResponseResultHandlersupports任何情况下都返回true,不然就会swagger的接口文档的所有资源都访问不到,页面就会报错:请确保swagger资源正确。

2.7 分页插件存在问题

使用的分页插件:



<dependency><groupId>com.github.pagehelpergroupId><artifactId>pagehelper-spring-boot-starterartifactId><version>1.2.12version>
dependency>

我是用时存在list找不到数据的问题,我参考我以前的博客《PageInfo对处理过的list进行分页》重新写的一个工具类:

public class PageInfoUtils {public static <T> PageInfo<T> list2PageInfo(List<T> arrayList, Integer pageNum, Integer pageSize) {//实现list分页PageHelper.startPage(pageNum, pageSize);int pageStart = pageNum == 1 ? 0 : (pageNum - 1) * pageSize;int pageEnd = arrayList.size() < pageSize * pageNum ? arrayList.size() : pageSize * pageNum;List<T> pageResult = new LinkedList<T>();if (arrayList.size() > pageStart) {pageResult = arrayList.subList(pageStart, pageEnd);}PageInfo<T> pageInfo = new PageInfo<T>(pageResult);//获取PageInfo其他参数pageInfo.setTotal(arrayList.size());int endRow = pageInfo.getEndRow() == 0 ? 0 : (pageNum - 1) * pageSize + pageInfo.getEndRow() + 1;pageInfo.setEndRow(endRow);boolean hasNextPage = arrayList.size() <= pageSize * pageNum ? false : true;pageInfo.setHasNextPage(hasNextPage);boolean hasPreviousPage = pageNum == 1 ? false : true;pageInfo.setHasPreviousPage(hasPreviousPage);pageInfo.setIsFirstPage(!hasPreviousPage);boolean isLastPage = (arrayList.size() > pageSize * (pageNum - 1) && arrayList.size() <= pageSize * pageNum) ? true : false;pageInfo.setIsLastPage(isLastPage);int pages = arrayList.size() % pageSize == 0 ? arrayList.size() / pageSize : (arrayList.size() / pageSize) + 1;pageInfo.setNavigateLastPage(pages);int[] navigatePageNums = new int[pages];for (int i = 1; i < pages; i++) {navigatePageNums[i - 1] = i;}pageInfo.setNavigatepageNums(navigatePageNums);int nextPage = pageNum < pages ? pageNum + 1 : 0;pageInfo.setNextPage(nextPage);pageInfo.setPageNum(pageNum);pageInfo.setPageSize(pageSize);pageInfo.setPages(pages);pageInfo.setPrePage(pageNum - 1);pageInfo.setSize(pageInfo.getList().size());int starRow = arrayList.size() < pageSize * pageNum ? 1 + pageSize * (pageNum - 1) : 0;pageInfo.setStartRow(starRow);return pageInfo;}
}

使用此框架后会有很对参数被放回,而有的参数是前端用不到的,要使得返回的尽量简洁,只要求总数与数据。

于是重写了返回的分页:

public class PageInfo<T> {/*** 总数*/private Integer total;/*** 数据*/private List data;public PageInfo() {}public Integer getTotal() {return total;}public void setTotal(Integer total) {this.total = total;}public List getData() {return data;}public void setData(List data) {this.data = data;}public static  <T> PageInfo<T> of(List<T> data){PageInfo pageInfo = new PageInfo();pageInfo.setData(data);pageInfo.setTotal(data.size());return pageInfo;}
}

使用方式:

  1. 查询仍然使用PageHelper插件
  2. 对查询的结果使用上述代码进行封装

2.8 权限验证

在做的项目里面,拥有四种角色,不同角色的权限不一样,此次我后台没有将权限写入数据库进行动态权限判断,我们是前后端分离项目(前端vue),我这里采用的是对view进行控制权限的方式进行判断。
首先,我将所有的视图进行枚举:

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum BackgroundViewEnum implements Serializable {/*** 后台权限*/USER_VIEW(1001, "el-icon-user","/userview", "用户管理", null),STUDENT_INFO_VIEW(100101,"", "/sudentInfoView", "学生管理", null),TEACHER_INFO_VIEW(100102, "","/teacherInfoView", "教师管理", null),GOODS_VIEW(1002, "el-icon-shopping-bag-2","/goodsview", "二手管理", null),CAROUSEL_VIEW(1003, "el-icon-money","/carouselview", "轮播管理", null),NOTICE_VIEW(1004, "el-icon-chat-dot-round","/noticeview", "公告管理", null),SCORE_VIEW(1005, "el-icon-notebook-2","/scoreview", "成绩管理", null),BAIYE_VIEW(1006, "el-icon-receiving","/baiyeview", "百叶管理", null),CALL_VIEW(1007, "el-icon-alarm-clock","/callview", "课堂点名", null),POSTBAR_VIEW(1008, "el-icon-chat-line-square","/postbarview", "校园贴吧", null),RECRUIT_VIEW(1009, "el-icon-bank-card","/recruitview", "兼职招聘", null);/*** 视图标识*/private final Integer id;/*** 视图图标*/private final String icon;/*** 视图*/private final String path;/*** 视图名*/private final String title;/*** 子菜单*/private List<BackgroundViewEnum> childrens;BackgroundViewEnum(Integer id, String icon, String path, String title, List<BackgroundViewEnum> childrens) {this.id = id;this.icon = icon;this.path = path;this.title = title;this.childrens = childrens;}public Integer getId() {return id;}public String getIcon() {return icon;}public String getPath() {return path;}public String getTitle() {return title;}public List<BackgroundViewEnum> getChildrens() {return childrens;}public void setChildrens(List<BackgroundViewEnum> childrens) {this.childrens = childrens;}
}

注意:@JsonFormat(shape = JsonFormat.Shape.OBJECT)
使用它是因为后台返回给前端数据的时候,如果是enum类型的数据,自动将该枚举的类型名称响应给了前端,而不是枚举里面的数据,所以要加上该注解

枚举了所有视图之后,动态的根据不同的用户拼装视图:

public class Menu {/*** @auther: zqq* @date: 21/1/7 21:10* @Description: 管理员权限菜单*/public static List<BackgroundViewEnum> adminMenu(){cancelUserChildView();ArrayList<BackgroundViewEnum> adminMenu = new ArrayList<>();ArrayList<BackgroundViewEnum> user_view_childs = new ArrayList<>();user_view_childs.add(BackgroundViewEnum.STUDENT_INFO_VIEW);user_view_childs.add(BackgroundViewEnum.TEACHER_INFO_VIEW);BackgroundViewEnum userView = BackgroundViewEnum.USER_VIEW;userView.setChildrens(user_view_childs);adminMenu.add(userView);adminMenu.add(BackgroundViewEnum.GOODS_VIEW);adminMenu.add(BackgroundViewEnum.CAROUSEL_VIEW);adminMenu.add(BackgroundViewEnum.NOTICE_VIEW);adminMenu.add(BackgroundViewEnum.SCORE_VIEW);adminMenu.add(BackgroundViewEnum.BAIYE_VIEW);adminMenu.add(BackgroundViewEnum.CALL_VIEW);adminMenu.add(BackgroundViewEnum.POSTBAR_VIEW);adminMenu.add(BackgroundViewEnum.RECRUIT_VIEW);return adminMenu;}/*** @auther: zqq* @date: 21/1/7 21:13* @Description: 教师权限*/public static List<BackgroundViewEnum> teacherMenu(){ArrayList<BackgroundViewEnum> teacherMenu = new ArrayList<>();teacherMenu.add(BackgroundViewEnum.SCORE_VIEW);teacherMenu.add(BackgroundViewEnum.CALL_VIEW);return teacherMenu;}/*** @auther: zqq* @date: 21/1/7 21:20* @Description: 百叶管理者的权限*/public static List<BackgroundViewEnum> baiyeMenu(){cancelUserChildView();ArrayList<BackgroundViewEnum> baiyeMenu = new ArrayList<>();baiyeMenu.add(BackgroundViewEnum.USER_VIEW);baiyeMenu.add(BackgroundViewEnum.BAIYE_VIEW);return baiyeMenu;}// 拼装之后再将视图杰出关系public static void cancelUserChildView(){BackgroundViewEnum.USER_VIEW.setChildrens(null);}}

之后每次请求都会在请求代码里面验证是否拥有权限:

public class UserAuthority {public static void haveAuthority(HttpServletRequest request, BackgroundViewEnum backgroundViewEnum) throws BaseException {Integer userType = getUserType(request);List<BackgroundViewEnum> menus = null;switch (userType) {case 2:menus = Menu.teacherMenu();break;case 3:menus = Menu.adminMenu();break;case 4:menus = Menu.baiyeMenu();break;default:break;}// 没有权限就抛出异常if (!menus.contains(backgroundViewEnum)){throw new BaseException(ResultCode.USER_NOT_POWER);}}}

此方法每次都需要将该请求对应的视图名传过来,每次验证都放在一个具体的请求,如:

@PostMapping("/put")
public void putUser(@RequestBody User user, HttpServletRequest request) throws BaseException {// 验证权限haveAuthority(request, USER_VIEW);
//省略
}

每次在请求里面进行一次判断,在实际的逻辑代码里面显得毫无意义,我决定采取使用注解的形式进行判断。
首先新建一个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface PowerView {BackgroundViewEnum view();
}

之后在响应格式的拦截器总添加权限判断的代码

@Component
public class ResponseResultInterceptor implements HandlerInterceptor {// 标记名称public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 请求方法if (handler instanceof HandlerMethod){final HandlerMethod handlerMethod = (HandlerMethod) handler;final Class<?> clazz = handlerMethod.getBeanType();final Method method = handlerMethod.getMethod();// 判断是否在类的对象上加了注解if (clazz.isAnnotationPresent(ResponseResult.class)){// 设置此请求返回体,需要包装,往下传递,在ResponseBodyAdvice接口进行判断request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));}else if (method.isAnnotationPresent(ResponseResult.class)){// 设置此请求返回体,需要包装,往下传递,在ResponseBodyAdvice接口进行判断request.setAttribute(RESPONSE_RESULT_ANN, method.getAnnotation(ResponseResult.class));}// 权限判断// 权限判断if (method.isAnnotationPresent(PowerView.class)){// 获取类上的注解PowerView powerView = clazz.getAnnotation(PowerView.class);BackgroundViewEnum view = powerView.view();// 验证权限haveAuthority(request, view);}else if (clazz.isAnnotationPresent(PowerView.class)){// 获取类上的注解PowerView powerView = clazz.getAnnotation(PowerView.class);BackgroundViewEnum view = powerView.view();// 验证权限haveAuthority(request, view);}}return true;}
}

这样就实现了在拦截器中进行权限判断,权限判断就不用再写在具体的业务逻辑中了。

2.9 get请求参数为空串时转换为null

之前在响应格式中,我有将json的数据格式中的空串转换为null,但是静态测试时大象,他对get请求的参数是不适用的,于是我百度查到了这位大佬的文件《SpringBoot项目,如何优雅的把接口参数中的空白值替换为null值?》,大佬介绍的方式将get请求的空串转换为null的方式如下:

@ControllerAdvice
public class GlobalControllerAdiviceController {//WebDataBinder是用来绑定请求参数到指定的属性编辑器,可以继承WebBindingInitializer//来实现一个全部controller共享的dataBiner@InitBinderpublic void dataBind(WebDataBinder binder) {// 方式1:自定义binder.registerCustomEditor(String.class, new StringEditors());///方式2:只有spring的api,給指定类型注册类型转换器操作binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));}
}class StringEditors extends PropertyEditorSupport {//setAsText完成字符串到具体对象类型的转换,@Overridepublic void setAsText(String text) throws IllegalArgumentException {if (text == null || "".equals(text.trim())) {text = null;}setValue(text);}//getAsText完成具体对象类型到字符串的转换。@Overridepublic String getAsText() {if (getValue() != null) {return getValue().toString();}return null;}
}

经过测试两种方式都可以。

后续有其他问题我会持续更新。。。

2.10 文件上传

有一个需求是创建活动时需要上传图片,便当你们既有数据,也有图片,最开始我都做法是混合上传,接口里面既有POJO也有MultipartFile,但是在测试的时候始终出问题,于是我百度查到表单里面有文件时,先上传文件,返回文件存储路径,然后再提交表单数据,也就是两个接口。

    @ApiOperation(value = "上传活动海报图片", notes = "返回上传的图片地址")@PostMapping("/images")public String putImage(MultipartFile file) throws IOException, BaseException {return baiyeService.putImage(file);}// 活动图片地址,在yml文件中配置映射@Value("${product.activityImagePath}")private String activityImagePath;@Overridepublic String putImage(MultipartFile multipartFile) throws IOException, BaseException {String UUIDPictureName = IdUtil.simpleUUID();String originalFileName = multipartFile.getOriginalFilename();//后缀String ext = originalFileName.substring(originalFileName.lastIndexOf('.'));//验证文件是否是图片Image img = ImageIO.read(multipartFile.getInputStream());if (img == null || img.getWidth(null) <= 0 || img.getHeight(null) <= 0 ) {throw new BaseException(FILE_TYPE_ERROR);}//保存的时候还是要保存到真实路径的,这里的这个路径有file前缀String fileName = UUIDPictureName + ext;String imageRealPath = activityImagePath.substring(activityImagePath.indexOf(':') + 1) + "/" + fileName;File image = new File(imageRealPath);//存入目标路径multipartFile.transferTo(image);String path = "/image/activity";return path + "/" + fileName;}


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部