springboot中配置FastJson作为消息转换器,返回json格式的自定义结构体给前端,另外使用自定义注解判断权限

背景:springboot返回json格式的自定义结构体给前端

前提:

springboot中接口返回自定义结构体时,遇到报错HttpMediaTypeNotAcceptableException:Could not find acceptable representation报错,网上找了很多,都没有靠谱的解决方法,大部分都是说定义的实体类没有加getter/setter方法,还有人说修改属性private为public的,都试了下没法实现。
自己最终发现在springboot中配置FastJson作为消息转换器,能解决这个问题,接口能返回json格式自定义结构体给前端了

1、pom.xml文件引入fastjson依赖

<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>

2、 配置FastJson作为消息转换器

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;
    }

}


3、 创建一个拦截器

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();
        }
    }

}


4、创建一个自定义角色校验注解

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 {};
}


5、自定义返回结构体Result

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);
    }

}

6、自定义异常枚举类

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;
    }
}

7、封装API的错误码

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();
}

8、定义一个实体类

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;
}

9、定义一个controller,用来测试

我这边例子里是结合了自定义注解@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));
    }

}



10、postman中调用controller接口进行测试,请求接口可以看到返回的结果是json格式化形式。而且对null值都做了自动转换!!!

springboot中配置FastJson作为消息转换器,返回json格式的自定义结构体给前端,另外使用自定义注解判断权限_第1张图片

这边也可以顺便测试下自定义角色注解是否有效,当用户不是admin或者rjp时,被拦截器拦截,返回没有相关权限。

springboot中配置FastJson作为消息转换器,返回json格式的自定义结构体给前端,另外使用自定义注解判断权限_第2张图片

你可能感兴趣的:(spring,boot,json,前端)