springboot中接口返回自定义结构体时,遇到报错HttpMediaTypeNotAcceptableException:Could not find acceptable representation报错,网上找了很多,都没有靠谱的解决方法,大部分都是说定义的实体类没有加getter/setter方法,还有人说修改属性private为public的,都试了下没法实现。
自己最终发现在springboot中配置FastJson作为消息转换器,能解决这个问题,接口能返回json格式自定义结构体给前端了
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
package com.example.annotationdemo.demo2_role;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter4;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 配置拦截器
*
* @author 任珏朋
* @version V1.0.0 2023/6/3
* @since V100R001
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private RoleInterceptor roleInterceptor;
/**
* 添加拦截器
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(roleInterceptor)
.addPathPatterns("/**"); // 要拦截的url
}
/**
* 配置FastJson作为消息转换器
*
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter4 converter = new FastJsonHttpMessageConverter4();
// 设置类型
converter.setSupportedMediaTypes(getSupportedMediaTypes());
// 转换配置
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(
// List字段如果为null,输出为[],而非null
SerializerFeature.WriteNullListAsEmpty,
// 输出值为null的字段,默认为false
SerializerFeature.WriteMapNullValue,
// 字符类型字段如果为null,输出为”“,而非null; 由于WriteNullStringAsEmpty在map的value为字符串且为null时,
// 转换失效,值为null的还是转为null。这边简便的方法是在map中添加元素时,就手动判断和处理空数据就好了;
//另外,发现map里添加Boolean布尔类型,fastjson默认转换也会失效(SerializerFeature.WriteNullBooleanAsFalse),
// 处理方法和上面的类似,也是手动判断即可
SerializerFeature.WriteNullStringAsEmpty,
// 数值字段如果为null,输出为0,而非null
SerializerFeature.WriteNullNumberAsZero,
// Boolean字段如果为null,输出为false,而非null
SerializerFeature.WriteNullBooleanAsFalse,
//禁止循环引用
SerializerFeature.DisableCircularReferenceDetect
);
converter.setFastJsonConfig(config);
// 设置默认编码
converter.setDefaultCharset(StandardCharsets.UTF_8);
converters.add(converter);
}
/**
* 获得支持的媒体类型
* @return
*/
private List<MediaType> getSupportedMediaTypes() {
List<MediaType> supportedMediaTypes = new ArrayList<>();
// 如果你是作为 api 服务器,那么全部的 contentType 我建议都设置成 application/json
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
// 下面的这边可加可不加,根据需要来
// supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
// supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
// supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
// supportedMediaTypes.add(MediaType.APPLICATION_PDF);
// supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
// supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
// supportedMediaTypes.add(MediaType.APPLICATION_XML);
// supportedMediaTypes.add(MediaType.IMAGE_GIF);
// supportedMediaTypes.add(MediaType.IMAGE_JPEG);
// supportedMediaTypes.add(MediaType.IMAGE_PNG);
// supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
// supportedMediaTypes.add(MediaType.TEXT_HTML);
// supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
// supportedMediaTypes.add(MediaType.TEXT_PLAIN);
// supportedMediaTypes.add(MediaType.TEXT_XML);
return supportedMediaTypes;
}
}
package com.example.annotationdemo.demo2_role;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
/**
* 创建一个拦截器
* 这个拦截器拦截所有访问路径的url,如果访问方法上带有我们创建的自定义注解RoleAuthorize ,则获取这个注解上限定的访问角色,方法没有注解再获取这个类是否有这个注解,如果这个类也没有注解,则这个类的访问没有角色限制,放行,如果有则校验当前用户的springsecurity是否有这个角色,有则放行,没有则抛出和springsecurity一样的异常AccessDeniedException,全局异常捕获这个异常,返回状态码403(表示没有权限访问)
*
* @author 任珏朋
* @version V1.0.0 2023/6/3
* @since V100R001
*/
@Component
@Slf4j
public class RoleInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
//在方法上寻找注解
RoleAuthorize permission = handlerMethod.getMethodAnnotation(RoleAuthorize.class);
if (permission == null) {
//方法不存在则在类上寻找注解则在类上寻找注解
permission = handlerMethod.getBeanType().getAnnotation(RoleAuthorize.class);
}
//如果没有添加权限注解则直接跳过允许访问
if (permission == null) {
return true;
}
// 拦截器获取请求参数
String queryString = request.getParameter("roleName");
log.info("请求参数:{}", queryString);
//获取注解中的值
String[] validateRoles = permission.value();
//校验是否含有对应的角色
//(如果用到了springsecurity,可以从springsecurity的上下文获取用户角色是否存在当前的角色名称)
// 因为这边没有整合springsecurity,所以直接判断下角色是否有效,假定判断角色是admin才有权限进入
if (Arrays.stream(validateRoles).anyMatch(a -> a.equals(queryString))) {
return true;
}
// 返回格式化json到前端页面
returnJson(response);
return false;
}
/**
* 返回格式化json到前端页面
*
* @param response
* @throws Exception
*/
private void returnJson(HttpServletResponse response) throws Exception {
PrintWriter writer = null;
// 注意点1:这边返回配置为josn格式
response.setContentType("application/json;charset=UTF-8");
try {
writer = response.getWriter();
// 注意点2,这样要用fastjson转换一下后,返回的前端才是格式化后的json格式
writer.print(JSON.toJSONString(Result.failedUnauthorized()));
} catch (IOException e) {
} finally {
if (writer != null)
writer.close();
}
}
}
package com.example.annotationdemo.demo2_role;
import java.lang.annotation.*;
/**
* 创建一个自定义角色校验注解
*
* 我们在使用springsecurity有一个注解@PreAuthorize可以作用在类或方法上,用来校验是否有权限访问,
* 我们可以模仿这个注解,写一个我们自定义注解来实现同样的功能
*
* @author 任珏朋
* @version V1.0.0 2023/6/3
* @since V100R001
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RoleAuthorize {
String[] value() default {};
}
Result是我结合网上例子,自己又优化了其中很多方法。
package com.example.annotationdemo.demo2_role;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 自定义返回结构体
*
* @author rjp
* @version V1.0.0 2023/6/3
* @since V100R001
*/
@Data
@NoArgsConstructor
public class Result<T> {
private String code;
private String message;
private T data;
private boolean success;
/**
* 返回成功的构造器
* @param code
* @param message
* @param data
*/
protected Result(String code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.success = true;
}
protected Result(String code, String message, T data, boolean success) {
this(code, message, data);
this.success = success;
}
/**
* 成功,默认返回Data为null
*
* @param
* @return
*/
public static <T> Result<T> ok() {
return ok((T) null);
}
/**
* 成功,并返回data
*
* @param data 获取的数据
* @return
*/
public static <T> Result<T> ok(T data) {
return new Result<>(ExceptionEnum.SUCCESS.getResultCode(),
ExceptionEnum.SUCCESS.getResultMsg(), data);
}
/**
* 成功,并返回data + message
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> Result<T> ok(T data, String message) {
return new Result<>(ExceptionEnum.SUCCESS.getResultCode(),
message, data);
}
/**
* 成功返,并返回list
*
* @param list 获取的数据
* @return
*/
public static <T> Result<List<T>> ok(List<T> list) {
return new Result<>(ExceptionEnum.SUCCESS.getResultCode(),
ExceptionEnum.SUCCESS.getResultMsg(), list);
}
/**
* 成功返,并返回list + message
*
* @param list 获取的数据
* @return
*/
public static <T> Result<List<T>> ok(List<T> list, String message) {
return new Result<>(ExceptionEnum.SUCCESS.getResultCode(),
ExceptionEnum.SUCCESS.getResultMsg(), list);
}
/**
* 失败返回结果(默认返回Data为null, failedCommon方法被其他具体失败方法调用)
*
* @param error 错误码
*/
public static <T> Result<T> failedCommon(IError error) {
return new Result<>(error.getResultCode(),
error.getResultMsg(), null, false);
}
/**
* 失败返回结果(400:参数检验失败)
*/
public static <T> Result<T> failedValidate() {
return failedCommon(ExceptionEnum.VALIDATE_FAILED);
}
/**
* 失败返回结果(400:参数检验失败,这边自定义message)
*
* @param message 提示信息
*/
public static <T> Result<T> failedValidate(String message) {
return new Result<>(ExceptionEnum.VALIDATE_FAILED.getResultCode(),
message, null, false);
}
/**
* 失败返回结果(404:参数未找到,检验失败)
*/
public static <T> Result<T> failedParamsNotFound() {
return failedCommon(ExceptionEnum.NOT_FOUND);
}
/**
* 失败返回结果(401:暂未登录或token已经过期)
*/
public static <T> Result<T> failedUnauthorized() {
return failedCommon(ExceptionEnum.UNAUTHORIZED);
}
/**
* 未登录返回结果(401:将登录者的信息data一起返回)
*/
public static <T> Result<T> failedUnauthorized(T data) {
return new Result<>(ExceptionEnum.UNAUTHORIZED.getResultCode(),
ExceptionEnum.UNAUTHORIZED.getResultMsg(), data, false);
}
/**
* 失败返回结果(403:没有相关权限)
*/
public static <T> Result<T> failedForbidden() {
return failedCommon(ExceptionEnum.FORBIDDEN);
}
/**
* 失败返回结果(403:没有相关权限,这边将data一起返回)
*/
public static <T> Result<T> failedForbidden(T data) {
return new Result<>(ExceptionEnum.FORBIDDEN.getResultCode(),
ExceptionEnum.FORBIDDEN.getResultMsg(), data, false);
}
/**
* 失败返回结果(408:请求时间超时)
*/
public static <T> Result<T> failedTimeOut() {
return failedCommon(ExceptionEnum.REQUEST_TIME_OUT);
}
/**
* 失败返回结果(500:服务器内部错误)
*/
public static <T> Result<T> failedServerError() {
return failedCommon(ExceptionEnum.INTERNAL_SERVER_ERROR);
}
/**
* 失败返回结果(503:结果服务器正忙,请稍后再试!)
*/
public static <T> Result<T> failedServerBusy() {
return failedCommon(ExceptionEnum.SERVER_BUSY);
}
/**
* 失败返回结果(手动方式,只是自定义message,不返回code)
*
* @param message 错误信息
*/
public static <T> Result<T> failedByCustom(String message) {
return new Result<>(null, message, null, false);
}
/**
* 失败返回结果(手动方式,自定义其他类型错误息errorCode + message)
*
* @param errorCode 错误码
* @param message 错误信息
*/
public static <T> Result<T> failedByCustom(String errorCode, String message) {
return new Result<>(errorCode, message, null, false);
}
/**
* 失败返回结果(使用IError自带的code,但是是自定义message)
*
* @param error 错误码
* @param message 错误信息
*/
public static <T> Result<T> failedByError(IError error, String message) {
return new Result<>(error.getResultCode(), message, null, false);
}
}
package com.example.annotationdemo.demo2_role;
/**
* 自定义异常枚举类
*
* @author rjp
* @version V1.0.0 2023/6/3
* @since V100R001
*/
public enum ExceptionEnum implements IError {
// 数据操作状态码和提示信息定义
SUCCESS("200", "操作成功"),
VALIDATE_FAILED("400", "参数检验失败"),
NOT_FOUND("404", "参数未找到,检验失败"),
UNAUTHORIZED("401", "暂未登录或token已经过期"),
FORBIDDEN("403", "没有相关权限"),
REQUEST_TIME_OUT("408", "请求时间超时"),
INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
SERVER_BUSY("503", "服务器正忙,请稍后再试!");
/**
* 错误码
*/
private String resultCode;
/**
* 错误描述
*/
private String resultMsg;
private ExceptionEnum(String resultCode, String resultMsg) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
}
@Override
public String getResultCode() {
return resultCode;
}
@Override
public String getResultMsg() {
return resultMsg;
}
}
package com.example.annotationdemo.demo2_role;
/**
* 封装API的错误码
*
* @author rjp
* @version V1.0.0 2023/6/3
* @since V100R001
*/
public interface IError {
/**
* 错误码
*/
String getResultCode();
/**
* 错误描述
*/
String getResultMsg();
}
package com.example.annotationdemo.demo2_role;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
/**
* @author 任珏朋
* @version V1.0.0 2023/6/5
* @since V100R001
*/
@Getter
@Setter
@NoArgsConstructor
public class UserA implements Serializable {
private Long id;
private String name;
private Boolean flagSuccess;
}
我这边例子里是结合了自定义注解@RoleAuthorize来校验角色权限的,小伙伴们可以将这个注解去掉。controller里接口其实和我们平时写的差不多,没有什么特别的注解要加。
package com.example.annotationdemo.demo2_role;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 测试使用注解校验角色,返回json格式的结构体
*
* @author 任珏朋
* @version V1.0.0 2023/6/3
* @since V100R001
*/
@RestController
@RequestMapping(value = "/test")
@RoleAuthorize(value = {"admin", "rjp"}) // 把注解加到接口的类或方法上验证
public class ProcessInstanceController {
/**
* 测试使用注解校验角色,返回json格式的结构体
*
* @param roleName 角色名称:如admin
* @return
*/
@GetMapping(value = "/role")
public Result<Object> getGroupList(@RequestParam("roleName") String roleName) {
System.out.println("进入接口啦!");
Map<String, Object> result = new HashMap<>();
result.put("role", "登录角色为" + roleName);
// 加个Boolean试试;另外,发现map里添加Boolean布尔类型,fastjson默认转换也会失效
// (SerializerFeature.WriteNullBooleanAsFalse),处理方法和上面的类似,也是手动判断即可
Boolean b = null;
result.put("boolean", b == null ? false : b);
// 由于WriteNullStringAsEmpty在map的value为字符串且为null时,转换失效,值为null的还是转为null;
// 这边简便的方法是在map中添加元素时,就手动判断和处理空数据就好了
String serialNumber = null;
result.put("serialNumber", StringUtils.isEmpty(serialNumber) ? "" : serialNumber);
UserA user = new UserA();
// Long数值类型为空
user.setId(null);
user.setName(null);
user.setFlagSuccess(null);
// 加个对象试试
result.put("user", user);
// 加个空list试试
result.put("emptyList", Collections.EMPTY_LIST);
// 加个空map试试
result.put("emptyMap", Collections.EMPTY_MAP);
return Result.ok(result);
// 很奇怪啊,注意这样写,反而是返回的json是有横杠且堆在一起的!!! 和PrintWriter类的print方法是相反的!!!
// return Result.ok(JSON.toJSONString(result));
}
}
这边也可以顺便测试下自定义角色注解是否有效,当用户不是admin或者rjp时,被拦截器拦截,返回没有相关权限。