Spring + AI助力开发工作提效

对于Java开发者来说,Spring框架再熟悉不过了,用一句自黑的话说:我们都是面向Spring编程。Spring强大的整合能力、灵活的配置和良好的扩展性为我们开发项目带来了极大便利,我们使用Spring也经历了繁琐的xml配置时代到SpringBoot自动装配时代。

在AI的井喷式应用中,编程助手成为了一个备受关注的领域。AI编程助手的发展经历了从早期的代码补全和智能提示,到后来的基于机器学习的代码生成,再到现在的基于大语言模型的智能编程助手的过程。

我们进行企业级开发时常用的IDE是JetBrains的IDEA,因此在AI编程时代我们可以借助IDEA AI编程插件来提升我们的编码效率。市面流行的编程插件有GitHub Copilot、CodeGeeX、通义灵码等,下面以使用IDEA开发SpringBoot项目为背景,借助通义灵码IDEA插件生成样例代码,再结合图文说说企业级开发中Spring相关的高频技术类需求的实现,也即我们常说的“造轮子”。

1. AI编码插件使用

市面各大主流AI编码插件支持的功能都大同小异,一般支持代码解释、代码优化、代码补全、单元测试生成及智能问答这几大功能。我们可以使用智能问答帮助我们实现需求,实现步骤分为提示词输入、AI出码及人工监督3步,即输入一句话描述你的需求,发送后AI自动输出代码,然后我们人工对输出的结果进行把关,如果不符合我们的需求再进行新一轮的交互直到符合我们的预期。

通义灵码功能界面:

交互流程:

2. Spring+AI实现高频技术类需求

项目日常迭代中除业务需求外,可能还会涉及技术类需求,这里按展示层、业务层、持久层分类来列举跟Spring相关的高频技术类需求的实现,代码样例使用AI编码插件辅助生成。

2.1. 展示层

2.1.1. 后端日期格式转换

【需求背景】:项目中要求日期类型的字段页面统一格式展示,比如展示成yyyy-MM-dd HH:mm:ss格式。

【设计思路】:首先我们要对SpringMVC相关的知识有一定的储备,比如SpringMVC中一般是通过WebMvcConfigurer来实现跨域、转换、拦截器、静态资源处理等。然后我们就可以向AI提出需求,让其输出具体实现细节。

【提示词】:SpringMVC中,将后端返回Date类型的数据转为yyyy-MM-dd HH:mm:ss前端展示,要求全局统一处理。

【AI输出代码】

package com.example.demo.interfaces.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.text.SimpleDateFormat;
import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public MappingJackson2HttpMessageConverter customJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        jsonConverter.setObjectMapper(objectMapper);
        return jsonConverter;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(customJackson2HttpMessageConverter());
    }
}

【自测运行效果】

2.1.2. 使用Swagger生成API接口文档

【需求背景】:要求配置Swagger生成API接口文档,便于前后端基于接口文档对接、协作。

【设计思路】:向AI描述清楚项目背景,需要通过什么方式生成API接口文档

【提示词】:当前为SpringBoot项目,使用Swagger生成API接口文档。

【AI输出代码】

2.1.2.1. pom.xml文件中添加Swagger依赖
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>
2.1.2.2. 创建配置类启用Swagger并配置其基本信息
package com.example.demo.interfaces.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.demo.interfaces.controller")) // 替换为你的Controller包路径
                .paths(PathSelectors.any())
                .build()
                .apiInfo(apiInfo());
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("API文档")
                .description("API文档描述")
                .version("1.0.0")
                .build();
    }
}
2.1.2.3. Controller和Model中使用Swagger注解
package com.example.demo.interfaces.controller;

import com.example.demo.domain.model.vo.req.UserReq;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
@Api(tags = "示例API")
public class ExampleController {

    @GetMapping("/hello")
    @ApiOperation(value = "获取问候信息", notes = "返回一个简单的问候信息")
    public String sayHello(UserReq user) {
        return "Hello, World!";
    }
}
package com.example.demo.domain.model.vo.req;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@ApiModel(description = "用户信息")
public class UserReq {

    @ApiModelProperty(value = "用户ID", example = "1")
    private Long id;

    @ApiModelProperty(value = "用户名", example = "JohnDoe")
    private String username;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

【自测运行效果】:

启动SpringBoot项目访问http://localhost:应用端口/swagger-ui.html即可看到Swagger UI界面

2.2. 业务层

2.2.1. 方法出入参打印

【需求背景】:当前项目有来自客户端的http请求、与外围系统间的rest请求以及内部微服务间的feign请求,这3类请求本质都是http请求,要求拦截这3类请求,将方法出入参打印封装在公共逻辑中,便于排查问题。

【设计思路】:通过拦截器实现这3类请求的拦截,再将这个功能封装成springboot starter组件,便于在多个项目或模块中复用。像这类相对复杂的需求描述,一般需要跟AI进行多次交互并对其输出答案进行验证优化后才能达到预期。

【提示词】:创建一个标准的springboot starter组件的maven项目,满足以下要求:

1、jdk使用1.8版本,springboot版本使用2.5.4

2、组件支持拦截controller请求、RestTemplate请求和feign请求并打印方法请求参数和请求结果返回对象的日志,参数对象使用json格式打印

3、添加3个开关参数控制是否开启对应的拦截器

4、日志打印使用logback

5、给出业务工程依赖该组件的样例说明

【经多次交互后AI输出的代码】

2.2.1.1. 创建Maven项目,pom.xml如下
<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>starter.demo</groupId>
    <artifactId>spring-boot-starter-log</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.5.4</spring-boot.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>
        <!-- Spring Boot Starter Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>
        <!-- Spring Boot Starter Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>${spring-boot.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- Feign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>3.0.4</version>
        </dependency>
        <!-- Fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
2.2.1.2. 创建拦截Controller请求的Interceptor和Advice
package starter.demo.interceptor;

import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.HandlerInterceptor;

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

/**
 * 拦截Controller GET请求
 */
public class ControllerRequestParamInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(ControllerRequestParamInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (HttpMethod.GET.matches(request.getMethod())) {
            logger.info("Controller请求: uri={}, 入参={}", request.getRequestURI(), JSON.toJSONString(request.getParameterMap()));
        }
        return true;
    }

}
package starter.demo.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;

/**
 * 拦截Controller 请求体格式的请求
 */
@ControllerAdvice
@ConditionalOnProperty(name = "logging.interceptor.controller.enabled", havingValue = "true")
public class ControllerRequestBodyAdvice extends RequestBodyAdviceAdapter {
    private static final Logger logger = LoggerFactory.getLogger(ControllerRespAdvice.class);


    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        InputStream body = inputMessage.getBody();
        byte[] bodyBytes = StreamUtils.copyToByteArray(body);
        String requestBody = new String(bodyBytes);
        String requestUri = "";

        // 获取请求的URL
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            requestUri = request.getRequestURI();
        }
        logger.info("Controller请求: uri={}, 入参={}", requestUri, requestBody);

        return new HttpInputMessage() {
            @Override
            public InputStream getBody() {
                return new ByteArrayInputStream(bodyBytes);
            }

            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }
        };
    }
}
2.2.1.3. 创建拦截Controller响应的Advice
package starter.demo.interceptor;

import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * 拦截Controller方法返参
 */
@ControllerAdvice
@ConditionalOnProperty(name = "logging.interceptor.controller.enabled", havingValue = "true")
public class ControllerRespAdvice implements ResponseBodyAdvice<Object> {
    private static final Logger logger = LoggerFactory.getLogger(ControllerRespAdvice.class);


    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        logger.info("Controller响应: 返参={}", JSON.toJSONString(body));
        return body;
    }
}
2.2.1.4. 创建拦截Feign请求的Interceptor
package starter.demo.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FeignInterceptor implements RequestInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(FeignInterceptor.class);

    @Override
    public void apply(RequestTemplate template) {
        logger.info("Feign请求: uri={}, 入参={}", template.url(), new String(template.body()));
    }
}
2.2.1.5. 创建拦截Rest请求的Interceptor
package starter.demo.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(RestTemplateInterceptor.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        logger.info("RestTemplate请求: uri={}, 入参={}", request.getURI().getPath(), new String(body));
        ClientHttpResponse response = execution.execute(request, body);
        logger.info("RestTemplate响应: 返参={}", new BufferedReader(new InputStreamReader(response.getBody())).readLine());
        return response;
    }
}
2.2.1.6. 创建Spring Boot Starter自动配置类注册Interceptor
package starter.demo.config;

import org.springframework.context.annotation.ComponentScan;
import starter.demo.interceptor.ControllerRequestParamInterceptor;
import starter.demo.interceptor.FeignInterceptor;
import starter.demo.interceptor.RestTemplateInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import feign.RequestInterceptor;
import org.springframework.web.client.RestTemplate;

@Configuration
@ComponentScan(basePackages = {"starter.demo.interceptor"})
public class LoggingAutoConfiguration {


    @Bean
    @ConditionalOnProperty(name = "logging.interceptor.controller.enabled", havingValue = "true")
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(new ControllerRequestParamInterceptor());
            }
        };
    }

    @Bean
    @ConditionalOnProperty(name = "logging.interceptor.resttemplate.enabled", havingValue = "true")
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getInterceptors().add(new RestTemplateInterceptor());
        return restTemplate;
    }

    @Bean
    @ConditionalOnProperty(name = "logging.interceptor.feign.enabled", havingValue = "true")
    public RequestInterceptor feignRequestInterceptor() {
        return new FeignInterceptor();
    }


}
2.2.1.7. 创建spring.factories文件

在src/main/resources/META-INF目录下创建spring.factories文件,注册自动配置类。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=starter.demo.config.LoggingAutoConfiguration
2.2.1.8. 日志配置

在src/main/resources目录下创建logback-spring.xml文件,配置日志输出。

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>
2.2.1.9. 业务工程依赖该组件的样例说明

在业务工程的pom.xml中添加依赖:

<!--  日志拦截组件  -->
        <dependency>
            <groupId>starter.demo</groupId>
            <artifactId>spring-boot-starter-log</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

在application.yml中配置开关参数:

logging:
  interceptor:
    controller:
      enabled: true
    resttemplate:
      enabled: true
    feign:
      enabled: true

【自测运行效果】

2.2.2. 敏感信息脱敏展示

【需求背景】:为符合安全合规要求,项目中用户手机号、邮箱、银行卡号等敏感字段前端页面展示时需要脱敏。

【设计思路】:利用AOP拦截方法,对加了脱敏注解的字段进行脱敏

【提示词】:当前为springboot项目,通过AOP+注解实现查询方法返回的实体如果包含手机号等敏感字段则进行脱敏后返回。

【AI输出代码】

2.2.2.1. 定义脱敏类型枚举及脱敏注解
package com.example.demo.domain.annotation;

public enum SensitiveType {
    MOBILE_PHONE
}
package com.example.demo.domain.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
    SensitiveType value();
}
2.2.2.2. 创建脱敏工具类

用于对不同类型的敏感信息进行脱敏处理。

package com.example.demo.domain.util;

public class SensitiveUtil {

    public static String desensitizeMobilePhone(String mobilePhone) {
        if (mobilePhone == null || mobilePhone.length() != 11) {
            return mobilePhone;
        }
        return mobilePhone.substring(0, 3) + "****" + mobilePhone.substring(7);
    }
}
2.2.2.3. 编写AOP切面

创建一个AOP切面,用于拦截带有脱敏注解的方法,并对返回结果进行脱敏处理。

package com.example.demo.interfaces.aspect;

   import com.example.demo.domain.annotation.Sensitive;
   import com.example.demo.domain.annotation.SensitiveType;
   import com.example.demo.domain.util.SensitiveUtil;
   import org.aspectj.lang.ProceedingJoinPoint;
   import org.aspectj.lang.annotation.Around;
   import org.aspectj.lang.annotation.Aspect;
   import org.springframework.stereotype.Component;

   import java.lang.reflect.Field;

   @Aspect
   @Component
   public class SensitiveAspect {

       @Around("execution(* com.example.demo.interfaces.controller..*.*(..))")
       public Object desensitize(ProceedingJoinPoint joinPoint) throws Throwable {
           Object result = joinPoint.proceed();
           if (result != null) {
               desensitizeObject(result);
           }
           return result;
       }

       private void desensitizeObject(Object obj) throws IllegalAccessException {
           Field[] fields = obj.getClass().getDeclaredFields();
           for (Field field : fields) {
               if (field.isAnnotationPresent(Sensitive.class)) {
                   field.setAccessible(true);
                   Sensitive sensitive = field.getAnnotation(Sensitive.class);
                   if (SensitiveType.MOBILE_PHONE.equals(sensitive.value())) {
                       String mobilePhone = (String) field.get(obj);
                       if (mobilePhone != null) {
                           field.set(obj, SensitiveUtil.desensitizeMobilePhone(mobilePhone));
                       }
                   }
               }
           }
       }
   }
   
2.2.2.4. 在实体类中使用注解

在需要脱敏的字段上添加自定义注解。

package com.example.demo.domain.model.vo;

import com.example.demo.domain.annotation.Sensitive;
import com.example.demo.domain.annotation.SensitiveType;

import java.util.Date;

public class UserVo {
    private String name;
    private String password;
    private Date createTime;
    @Sensitive(SensitiveType.MOBILE_PHONE)
    private String mobilePhone;

    public String getMobilePhone() {
        return mobilePhone;
    }

    public void setMobilePhone(String mobilePhone) {
        this.mobilePhone = mobilePhone;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

【自测运行效果】

2.2.3. 全局异常处理

【需求背景】:项目中一般需要对抛出的业务异常进行统一处理,将其包装成类似ResultVO的响应实体,然后前端根据ResultVO的code、message和data进行页面展示。

【设计思路】:全局异常处理这种网络上资料丰富的问题,AI一般回答得不错,直接输入提示词一次交互就能获取预期的答案。

【提示词】:当前为SpringBoot项目,写一个全局异常处理的代码,如果抛出BizException业务自定义异常则将其转为ResultVO。

【AI输出代码】

2.2.3.1. 定义BizException业务异常类
package com.example.demo.domain.exception;

   public class BizException extends RuntimeException {
       private String errorCode;
       private String errorMessage;

       public BizException(String errorCode, String errorMessage) {
           super(errorMessage);
           this.errorCode = errorCode;
           this.errorMessage = errorMessage;
       }

       // Getters and setters
       public String getErrorCode() {
           return errorCode;
       }

       public void setErrorCode(String errorCode) {
           this.errorCode = errorCode;
       }

       public String getErrorMessage() {
           return errorMessage;
       }

       public void setErrorMessage(String errorMessage) {
           this.errorMessage = errorMessage;
       }
   }
   
2.2.3.2. 定义ResultVO响应类
package com.example.demo.domain.model.vo;

public class ResultVO<T> {
    private int code;
    private String message;
    private T data;

    public ResultVO(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    // Getters and setters
    public int getCode() {
        return code;
    }

    public void setCode(int 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;
    }
}
   
2.2.3.3. 创建全局异常处理器
package com.example.demo.interfaces.advice;

   import com.example.demo.domain.exception.BizException;
   import com.example.demo.domain.model.vo.ResultVO;
   import org.springframework.http.HttpStatus;
   import org.springframework.http.ResponseEntity;
   import org.springframework.web.bind.annotation.ControllerAdvice;
   import org.springframework.web.bind.annotation.ExceptionHandler;

   @ControllerAdvice
   public class GlobalExceptionHandler {

       @ExceptionHandler(BizException.class)
       public ResponseEntity<ResultVO<Void>> handleBizException(BizException ex) {
           ResultVO<Void> resultVO = new ResultVO<>(-1, ex.getErrorMessage(), null);
           return new ResponseEntity<>(resultVO, HttpStatus.BAD_REQUEST);
       }

       // 可以添加其他异常处理器,如处理通用异常
       @ExceptionHandler(Exception.class)
       public ResponseEntity<ResultVO<Void>> handleException(Exception ex) {
           ResultVO<Void> resultVO = new ResultVO<>(-2, "系统错误", null);
           return new ResponseEntity<>(resultVO, HttpStatus.INTERNAL_SERVER_ERROR);
       }
   }
   

【自测运行效果】

2.3. 持久层

2.3.1. 多数据源配置

【需求背景】:项目中通常会按业务模块进行垂直分库,比如订单库、主数据库、财务库等。某些场景下需要跨库访问,比如定时任务进行数据备份,将业务库的数据备份到备份库,此时会涉及多数据源的切换,这就要求项目中支持多数据源。

【设计思路】:向AI描述清楚项目背景,提出需求点,越细越好。

【提示词】:在SpringBoot项目中实现多数据源配置,要实现以下几点:
1、先输出yml配置样例
2、支持1个主数据源多个从数据源,每个数据源都有key,能根据yml配置灵活添加n个从数据源,代码不要写死数据源个数
3、包含数据源定义、数据源注册、数据源切换
4、数据源的切换支持AOP+注解及Utils类通过数据源名称切换两种方式
5、jdbcTemplate配置上数据源
6、给出注解方式和API方式根据key切换数据源样例,使用jdbcTemplate跨库访问

【经多次交互后AI输出的代码】

2.3.1.1. yml配置样例
spring:
  datasource:
    primary:
      url: jdbc:mysql://localhost:3306/primary_db?useSSL=false&serverTimezone=UTC
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    secondary:
      ds1:
        url: jdbc:mysql://localhost:3306/secondary_db1?useSSL=false&serverTimezone=UTC
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
      ds2:
        url: jdbc:mysql://localhost:3306/secondary_db2?useSSL=false&serverTimezone=UTC
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
2.3.1.2. 数据源定义

创建一个DataSourceContextHolder类来管理当前线程的数据源键:

package com.example.demo.infrastructure.dyndatasource.definition;

public class DataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }

    public static String getDataSourceKey() {
        return contextHolder.get();
    }

    public static void clearDataSourceKey() {
        contextHolder.remove();
    }
}

创建动态数据源类:

package com.example.demo.infrastructure.dyndatasource.definition;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceKey();
    }
}

2.3.1.3. 数据源注册

 创建数据源配置类:

package com.example.demo.infrastructure.dyndatasource.registry;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {

    private DataSourceProperty primary;
    private Map<String, DataSourceProperty> secondary;

    public DataSourceProperty getPrimary() {
        return primary;
    }

    public void setPrimary(DataSourceProperty primary) {
        this.primary = primary;
    }

    public Map<String, DataSourceProperty> getSecondary() {
        return secondary;
    }

    public void setSecondary(Map<String, DataSourceProperty> secondary) {
        this.secondary = secondary;
    }

    public static class DataSourceProperty {
        private String url;
        private String username;
        private String password;
        private String driverClassName;

        public String getUrl() {
            return url;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public String getPassword() {
            return password;
        }

        public void setPassword(String password) {
            this.password = password;
        }

        public String getDriverClassName() {
            return driverClassName;
        }

        public void setDriverClassName(String driverClassName) {
            this.driverClassName = driverClassName;
        }
    }

}

数据源注册:

package com.example.demo.infrastructure.dyndatasource.registry;

import com.example.demo.infrastructure.dyndatasource.definition.DynamicDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceRegistryConfig {

    @Resource
    private DataSourceProperties dataSourceProperties;

    @Bean
    @Primary
    public DataSource primaryDataSource() {
        return buildDataSource(dataSourceProperties.getPrimary());
    }

    @Bean
    public Map<Object, Object> secondaryDataSources() {
        Map<Object, Object> dsMap = new HashMap<>();
        for (Map.Entry<String, DataSourceProperties.DataSourceProperty> entry : dataSourceProperties.getSecondary().entrySet()) {
            dsMap.put(entry.getKey(), buildDataSource(entry.getValue()));
        }
        return dsMap;
    }

    private DataSource buildDataSource(DataSourceProperties.DataSourceProperty property) {
        DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
        dataSourceBuilder.url(property.getUrl());
        dataSourceBuilder.username(property.getUsername());
        dataSourceBuilder.password(property.getPassword());
        dataSourceBuilder.driverClassName(property.getDriverClassName());
        return dataSourceBuilder.build();
    }

    @Bean
    public DataSource dynamicDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource,
                                        @Qualifier("secondaryDataSources") Map<Object, Object> secondaryDataSources) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(primaryDataSource);
        dynamicDataSource.setTargetDataSources(secondaryDataSources);
        return dynamicDataSource;
    }
}

2.3.1.4. 数据源切换

AOP方式切换数据源:创建数据源切换注解类 + AOP切面

package com.example.demo.infrastructure.dyndatasource.select;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
    String value();
}

package com.example.demo.infrastructure.dyndatasource.select;

import com.example.demo.infrastructure.dyndatasource.definition.DataSourceContextHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Aspect
@Component
public class DynamicDataSourceAspect {

    @Before("@annotation(targetDataSource)")
    public void changeDataSource(JoinPoint point, TargetDataSource targetDataSource) {
        String dsId = targetDataSource.value();
        if (!StringUtils.isEmpty(dsId)) {
            DataSourceContextHolder.setDataSourceKey(dsId);
        }
    }

    @After("@annotation(targetDataSource)")
    public void clearDataSource(JoinPoint point, TargetDataSource targetDataSource) {
        DataSourceContextHolder.clearDataSourceKey();
    }
}

API方式切换数据源:创建切换数据源Util

package com.example.demo.infrastructure.dyndatasource.select;

import com.example.demo.infrastructure.dyndatasource.definition.DataSourceContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.function.Supplier;

public class DynamicDataSourceUtils {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceUtils.class);

    public static void useDataSource(String dataSourceKey, Supplier<Boolean> bizLogic) {
        try {
            DataSourceContextHolder.setDataSourceKey(dataSourceKey);
            bizLogic.get();
        } catch (Exception e) {
            logger.error("切换数据源异常", e);
        } finally {
            DataSourceContextHolder.clearDataSourceKey();
        }
    }
}

2.3.1.5. JdbcTemplate配置
package com.example.demo.infrastructure.dyndatasource.jdbc;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class JdbcTemplateConfig {

    @Bean
    public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource routingDataSource) {
        return new JdbcTemplate(routingDataSource);
    }
}

【自测运行效果】

3. 小结

在AI时代,我觉得作为开发者首先应该拥抱变化,积极去了解新事物,思考它带来了什么,而我们又能够做些什么。其次应该转变角色,把自己当作设计师,把握整体方案,让AI辅助实现细节和帮助我们完成一些重复性的工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值