谷粒学院在线教育项目学习过程记录

谷粒学院在线教育项目学习过程记录

你说得对,但是谷粒学院是基于SpringCloud微服务➕Vue开发的全栈在线教育项目,你将扮演一位名为“b站用户”的学习者,逐步发掘实战项目的真相…

这篇文章用来记录我在谷粒学院项目学习过程中遇到的一些问题和解决办法。

使用新版Mybatis-plus代码生成器

代码生成器项目的Pom.xml文件,相关依赖及其配置如下。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>moe.tree</groupId>
    <artifactId>CodeGenerator</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
    </parent>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
</project>

生成器代码

import java.util.Collections;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.baomidou.mybatisplus.generator.fill.Column;

import moe.tree.seckill.service.impl.UserServiceImpl;

public class CodeGenerator {
    private static final String DB_URL = "jdbc:mysql://192.168.184.1:3306/guli?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"; // 数据库地址
    private static final String DB_USR = "guli"; // 数据库用户名
    private static final String DB_PASS = "guli"; // 数据库密码

    public static void main(String[] args) {
        FastAutoGenerator.create(DB_URL, DB_USR, DB_PASS).globalConfig(builder -> {
            builder.author("natuki") // 设置作者
                .enableSwagger() // 开启 swagger 模式
                .outputDir("C:/Users/Natuki/eclipse-workspace/GuliGenerator/src/main/java"); // 指定输出目录
        }).packageConfig(builder -> {
            builder.parent("moe.tree") // 设置父包名
                .moduleName("eduservice") // 设置父包模块名
                .pathInfo(Collections.singletonMap(OutputFile.xml,
                            "C:/Users/Natuki/eclipse-workspace/GuliGenerator/src/main/resources")); // 设置mapperXml生成路径
        }).strategyConfig(builder -> {
            builder.addInclude("edu_chapter", "edu_course", "edu_course_description", "edu_video") // 设置需要生成的表名
                .addTablePrefix("edu_") // 设置过滤表前缀

                // entity 策略配置
                .entityBuilder()
                .enableLombok()
                .idType(IdType.ASSIGN_ID)
                .logicDeleteColumnName("is_deleted") //逻辑删除字段名
                .naming(NamingStrategy.underline_to_camel)  //数据库表映射到实体的命名策略:下划线转驼峰命
                .columnNaming(NamingStrategy.underline_to_camel)    //数据库表字段映射到实体的命名策略:下划线转驼峰命
                .addTableFills(
                        new Column("gmt_create", FieldFill.INSERT),
                        new Column("gmt_modified", FieldFill.INSERT_UPDATE)
                )   //添加表字段填充,"create_time"字段自动填充为插入时间,"modify_time"字段自动填充为插入修改时间
                .enableTableFieldAnnotation()       // 开启生成实体时生成字段注解

                // mapper 策略配置
                .mapperBuilder()
                .superClass(BaseMapper.class)   //设置父类
                .formatMapperFileName("%sMapper")   //格式化 mapper 文件名称
                .enableMapperAnnotation()       //开启 @Mapper 注解
                .formatXmlFileName("%sMapper") //格式化 Xml 文件名称 如 UserXml

                // service 策略配置
                .serviceBuilder()
                .formatServiceFileName("%sService") // 如:UserService
                .formatServiceImplFileName("%sServiceImpl") // 如:UserServiceImpl

                // controller 策略配置
                .controllerBuilder()
                .formatFileName("%sController") // 如 UserController
                .enableRestStyle();  //开启生成 @RestController 控制器
        })
        .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
        .execute();
    }
}

使用Sentinel代替Hystrix实现服务熔断

引入Maven依赖

<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-sentinel -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2021.1</version>
</dependency>

在yml配置中启用Sentinel

feign:
  sentinel:
    enabled: true

创建一个用于处理VodClient服务熔断的类,继承自VodClient接口,并实现服务熔断时需要进行的操作

@Component
@Slf4j
public class VodFileDegradeFeignClient implements VodClient{

    @Override
    public R deleteVideo(String videoId) {
        log.error("VodClient:deleteVideo:EduVod服务状态异常");
        return R.error().message("EduVod服务状态异常");
    }

    @Override
    public R deleteBatchVideos(List<String> videoIdList) {
        log.error("VodClient:deleteBatchVideos:EduVod服务状态异常");
        return R.error().message("EduVod服务状态异常");
    }
}

为服务调用接口的@FeignClient注解添加fallback

@FeignClient(name = "service-vod", fallback = VodFileDegradeFeignClient.class)
@Component
public interface VodClient {

    @DeleteMapping("/eduvod/videos/{videoId}")
    public R deleteVideo(@PathVariable("videoId") String videoId);

    @DeleteMapping("/eduvod/videos/")
    public R deleteBatchVideos(@RequestParam("videoIdList") List<String> videoIdList);
}

排除service_cms模块引入service_edu的Controller

在service_cms模块整合热门名师和课程的接口的过程中,由于需要调用service_edu中的Service类并使用其中的实体类,因此pom.xml需引入service_edu依赖,但这同时也带来一个问题:service_edu模块中的方法在service_cms中均可被访问。
file

这显然并不合理,因此需要对service_cms模块启动类中的@ComponentScan注解作出一定的修改,最初我是写成如下的形式(只使用excludeFilters排除service_edu模块包内的所有Controller)但是问题并没有解决。

@SpringBootApplication
@ComponentScan(basePackages = {"moe.tree"}, excludeFilters = {@ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "moe.tree.eduservice.controller.*"}) //Here
@EnableDiscoveryClient
@MapperScan("moe.tree.educms.mapper")
public class EduCmsApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduCmsApplication.class, args);
    }
}

在尝试了FilterType.ASPECTJFilterType.REGEX两种过滤方式都没有解决后,经过多方搜索,在寻找ComponentScan中excludeFilters不生效的讨论中找到了如下一段话:

Each component scan does filtering individually. While you exclude Starter.class from SimpleTestConfig, SimpleTestConfig initializes Application, which does it’s own @ComponentScan without excluding Starter. The clean way of using ComponentScan is for each ComponentScan to scan separate packages, that way each filter work fine. When 2 separate ComponentScans scan the same package (as in your tests), this does not work.
每个组件扫描都会单独进行过滤 当您从SimpleTestConfig中排除Starter.class时,SimpleTestConfig会初始化Application,它会自行执行@ComponentScan而不会排除Starter。 使用ComponentScan的简洁方法是每个ComponentScan扫描单独的包,这样每个过滤器都可以正常工作。 当2个单独的ComponentScans扫描同一个包时(如在测试中),这将不起作用。

即在我的项目引入的service_edu模块中,仍有其他类进行了ComponentScan导致excludeFilters不生效,解决方法是还需排除其他进行ComponentScan的类。最后将问题定位在service_edu模块的启动类中,因此将excludeFilters修改如下,问题完美解决。

@SpringBootApplication
@ComponentScan(basePackages = {"moe.tree"}, excludeFilters = {
    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {EduApplication.class}),
    @ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "moe.tree.eduservice.controller.*")
}) //Here
@EnableDiscoveryClient
@MapperScan("moe.tree.educms.mapper")
public class EduCmsApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduCmsApplication.class, args);
    }
}

file

解决JSON数据存入Redis缓存时,无法序列化LocalDateTime的问题

由于Mybatis-plus提供的新版代码生成器,对于datetime类型的实体类属性默认使用的是LocalDateTime而不是Date,因此在对实体类对象序列化成JSON对象时,将会出现如下的问题:

Java 8 date/time type ‘java.time.LocalDateTime’ not supported by default: add Module "com.fasterxml.jackson.datatype\:jackson-datatype-jsr310"

对此,解决方法是首先引入com.fasterxml.jackson.datatype\:jackson-datatype-jsr310这个依赖。

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310 -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.13.4</version>
</dependency>

接下来的解决方式有两种:
第一种:为所有使用LocalDateTime的属性添加序列化和反序列化注解

@ApiModelProperty("更新时间")
@TableField(value = "gmt_modified", fill = FieldFill.INSERT_UPDATE)
@JsonSerialize(using = LocalDateTimeSerializer.class) // 序列化
@JsonDeserialize(using = LocalDateTimeDeserializer.class) // 反序列化
private LocalDateTime gmtModified;

第二种:在使用ObjectMapper时,注册JavaTimeModule模块
因此我在RedisConfig中做出了如下的修改,在redisTemplate和cacheManager方法中分别在创建ObjectMapper后都注册了JavaTimeModule模块。

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.registerModule(new JavaTimeModule()); //Here
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.registerModule(new JavaTimeModule()); //Here
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题) ,过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

接入支付宝沙箱代替微信支付

首先在Maven引入支付宝提供的SDK,更多版本可以在 Maven项目依赖 找到。

<dependency>
  <groupId>com.alipay.sdk</groupId>
  <artifactId>alipay-sdk-java</artifactId>
  <version>4.34.22.ALL</version>
</dependency>

之后在 支付宝开放平台-沙箱应用 中查看提供的应用APP ID信息,并设置接口加签方式,这里以公钥模式为例,获取支付宝公钥私钥
file

支付和查询订单前,需要先创建AlipayClient

private AlipayClient getAlipayClientWithAlipayPublicKey() {
    //支付宝网关地址,沙箱网关:https://openapi.alipaydev.com/gateway.do
    String gatewayUrl = AlipayConstantPropertiesUtil.GATEWAY_URL;
    //APP ID
    String appId = AlipayConstantPropertiesUtil.APP_ID;
    //应用私钥
    String privateKey = AlipayConstantPropertiesUtil.PRIVATE_KEY;
    //格式
    String format = "json";
    //编码
    String charset = AlipayConstantPropertiesUtil.CHARSET;
    //支付宝公钥
    String publicKey = AlipayConstantPropertiesUtil.ALIPAY_PUBLIC_KEY;
    //签名方式,默认为:RSA2
    String signType = AlipayConstantPropertiesUtil.SIGN_TYPE;

    AlipayClient alipayClient =  new DefaultAlipayClient(gatewayUrl , appId, privateKey, format, charset, publicKey, signType);
    return alipayClient;
}

支付订单

为支付接口创建一个Controller方法,其中result返回的是支付宝SDK生成的跳转到支付页面的html代码。

@GetMapping("/{orderNo}/alipay")
@ResponseBody
    public String payOrderWithAlipay(@PathVariable String orderNo, HttpServletResponse response) {
    String result = orderService.payOrderWithAlipay(orderNo);
    response.setContentType("text/html");
    return result;
}

创建AliBean实体类,用于封装发起支付的请求数据。

@Data
public class AliBean {
    /**
     * 商户订单号,必填
     */
    private String out_trade_no;
    /**
     * 订单名称,必填
     */
    private String subject;
    /**
     * 付款金额,必填
     * 根据支付宝接口协议,必须使用下划线
     */
    private String total_amount;
    /**
     * 商品描述,可空
     */
    private String body;
    /**
     * 超时时间参数
     */
    private String timeout_express = "1h";
    /**
     * 产品编号,PC网页支付强制为"FAST_INSTANT_TRADE_PAY"
     */
    private String product_code = "FAST_INSTANT_TRADE_PAY";
}

编写OrderService中的payOrderWithAlipay方法,主要是通过AlipayTradePagePayRequest发起请求,并提交bizContent。其中bizContentAliBean对象转换的json字符串。成功后,通过alipayClient.pageExecute(alipayRequest).getBody()获取跳转至支付页面的html代码。

@Override
public String payOrderWithAlipay(String orderNo) {
    Order order = getOrderByOrderNo(orderNo);
    if(order == null) {
        throw new GuliException(20001, "订单不存在");
    }

    AliBean aliBean = new AliBean();
    aliBean.setOut_trade_no(order.getOrderNo());
    aliBean.setSubject(order.getCourseTitle());
    aliBean.setTotal_amount(order.getTotalFee().toString());
    aliBean.setBody(order.toString());

    AlipayClient alipayClient = getAlipayClientWithAppPublicKey();

    String returnUrl = AlipayConstantPropertiesUtil.RETURN_URL;
    String notifyUrl = AlipayConstantPropertiesUtil.NOTIFY_URL;

    AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
    // 页面跳转同步通知页面路径
    alipayRequest.setReturnUrl(returnUrl);
    // 服务器异步通知页面路径
    alipayRequest.setNotifyUrl(notifyUrl);
    // 封装参数
    ObjectMapper mapper = new ObjectMapper();
    String bizContent = null;
    try {
        bizContent = mapper.writeValueAsString(aliBean);
    } catch (JsonProcessingException e) {
        e.printStackTrace();
        throw new GuliException(20001, "生成支付宝订单失败");
    }
    alipayRequest.setBizContent(bizContent);
    // 3、请求支付宝进行付款,并获取支付结果
    String result = null;
    try {
        result = alipayClient.pageExecute(alipayRequest).getBody();
    } catch (AlipayApiException e) {
        e.printStackTrace();
        throw new GuliException(20001, "生成支付宝订单失败");
    }
    // 返回付款信息
    return result;
}

查询订单

实现方式主要参考开放文档中的 alipay.trade.query(统一收单交易查询) 部分。

获取AlipayClient对象后,发起AlipayTradeQueryRequest请求,并传入设置商家订单号out_trade_nobizContent,请求成功后则可通过AlipayTradeQueryResponse响应对象中的get方法获取支付宝的订单信息。

AlipayClient alipayClient = getAlipayClientWithAlipayPublicKey();
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
Map<String, Object> map = new HashedMap<>();
map.put("out_trade_no", orderNo);
ObjectMapper objectMapper = new ObjectMapper();
String bizContent = null;
try {
    bizContent = objectMapper.writeValueAsString(map);
} catch (JsonProcessingException e) {
    e.printStackTrace();
    throw new GuliException(20001, "查询订单状态失败");
}
request.setBizContent(bizContent);
try {
    AlipayTradeQueryResponse response = alipayClient.execute(request);
    //do something
} catch (AlipayApiException e) {
    e.printStackTrace();
}

结合Cron实现定时任务

首先,在启动类添加@EnableScheduling注解

@SpringBootApplication
@EnableScheduling //Here
public class StatisticsApplication {
    public static void main(String[] args) {
        SpringApplication.run(StatisticsApplication.class, args);
    }
}

然后为定时执行的方法添加@Scheduled注解,在注解的cron属性中填入cron表达式即可。

@Component
public class ScheduleTask {

    @Autowired
    private StatisticsService statisticsService;

    @Scheduled(cron = "0 0 1 * * ?") //Here
    public void generateDailyStatisticsData() {
        String yesterday = LocalDate.now().plusDays(-1).toString();
        statisticsService.generateDailyStatisticsData(yesterday);
    }
}

在线生成cron表达式的小工具:https://cron.qqe2.com/

Gateway网关全局跨域配置

需要注意的一点是,子模块内的@CrossOrigin或全局跨域配置文件必须全部删除,并统一配置在网关模块中,否则将出现问题(具体表现为get请求成功,post等其他浏览器会提示跨域错误)。

网关模块的统一跨域配置代码,也与之前我的文章 【整理】SpringBoot跨域配置填坑 内提及的统一跨域配置有所区别,如果直接使用先前文章提及的方法将会出现错误(java.lang.NoClassDefFoundError: javax/servlet/Servlet)。

Gateway网关模块内的全局跨域配置代码如下。

package moe.tree.geteway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
public class GlobalCorsConfiguration {
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}

Gateway网关全局异常处理

由于我在学习过程中,部分依赖使用了更高级的版本(SpringBoot 2.5.14),因此在直接使用视频提供的网关全局异常处理会存在ResourceProperties类已过时的问题,在2.7以上的版本中会取消ResourceProperties类,因此需要作出一些修改。

对于ErrorHandlerConfiguration类,WebProperties将代替ResourceProperties,因此在注解和构造函数中均需要做出修改,修改后的代码如下。

package moe.tree.geteway.config;

import moe.tree.geteway.handler.JsonExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import java.util.Collections;
import java.util.List;

@Configuration
@EnableConfigurationProperties({ServerProperties.class, WebProperties.class})
public class ErrorHandlerConfiguration {
    private ServerProperties serverProperties;
    private ApplicationContext applicationContext;
    private WebProperties webProperties;
    private List<ViewResolver> viewResolvers;
    private ServerCodecConfigurer serverCodecConfigurer;

    public ErrorHandlerConfiguration(ServerProperties serverProperties, WebProperties webProperties, ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) {
        this.serverProperties = serverProperties;
        this.applicationContext = applicationContext;
        this.webProperties = webProperties;
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
        JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes, this.webProperties, this.serverProperties.getError(), this.applicationContext);
        exceptionHandler.setViewResolvers(this.viewResolvers);
        exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
}

而在JsonExceptionHandler类中,构造方法内调用父类的构造方法直接传入WebProperties是不正确的,还需要通过webProperties.getResources()进行参数类型转换。除此之外,protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace)的重写也在更高版本的SpringBoot中被标记为过时,新版本中方法参数发生了变化。具体的实现代码如下。

package moe.tree.geteway.handler;

import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.web.reactive.function.server.*;

import java.util.HashMap;
import java.util.Map;

public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {

    public JsonExceptionHandler(ErrorAttributes errorAttributes, WebProperties webProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) {
        super(errorAttributes, webProperties.getResources(), errorProperties, applicationContext);
    }

    @Override
    protected Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
        Map<String, Object> map = new HashMap<>();
        map.put("success", false);
        map.put("code", 40404);
        map.put("message", "404 Not Found");
        map.put("data", null);
        return map;
    }

    /**
     * 指定响应处理方法为JSON处理的方法
     *
     * @param errorAttributes
     */
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    /**
     * 根据code获取对应的HttpStatus
     *
     * @param errorAttributes
     */
    @Override
    protected int getHttpStatus(Map<String, Object> errorAttributes) {
        return 200;
    }
}

参考文章:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注