基础

关于版本选择

https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E

Spring Cloud Version Spring Cloud Alibaba Version Spring Boot Version
Spring Cloud 2020.0.1 2021.1 2.4.2
Spring Cloud Hoxton.SR9 2.2.6.RELEASE 2.3.2.RELEASE
Spring Cloud Greenwich.SR6 2.1.4.RELEASE 2.1.13.RELEASE
Spring Cloud Hoxton.SR3 2.2.1.RELEASE 2.2.5.RELEASE
Spring Cloud Hoxton.RELEASE 2.2.0.RELEASE 2.2.X.RELEASE
Spring Cloud Greenwich 2.1.2.RELEASE 2.1.X.RELEASE

Spring Data Release Train Spring Data Elasticsearch Elasticsearch Spring Framework Spring Boot
2021.0 (Pascal) 4.2.1 7.12.1 5.3.7 2.5.x
2020.0 (Ockham) 4.1.x 7.9.3 5.3.2 2.4.x
Neumann 4.0.x 7.6.2 5.2.12 2.3.x
Moore 3.2.x 6.8.12 5.2.12 2.2.x
Lovelace[1] 3.1.x[1] 6.2.2 5.1.19 2.1.x
Kay[1] 3.0.x[1] 5.5.0 5.0.13 2.0.x
Ingalls[1] 2.1.x[1] 2.4.0 4.3.25 1.5.x

Springboot-Starter

  1. https://start.spring.io/#!type=maven-project&language=java&platformVersion=2.3.2.RELEASE&packaging=jar&jvmVersion=1.8&groupId=com.intbee&artifactId=sys-base-api&name=sys-base-api&description=Demo%20project%20for%20Spring%20Boot&packageName=com.intbee.sys-base-api&dependencies=devtools,lombok,web,validation

Spring Cloud常见问题

Fegin/Ribbon的超时与重试配置

# readTimeout和connectTimeout必须同时配置方可生效
feign.client.config.default.readTimeout=3000
feign.client.config.default.connectTimeout=3000
feign.client.config.clientsdk.readTimeout=2000
feign.client.config.clientsdk.connectTimeout=2000
# 参数首字母要大写,和 Feign 的配置不同
# 同时配置 Feign 和 Ribbon 的超时,以 Feign 为准
ribbon.ReadTimeout=4000
ribbon.ConnectTimeout=4000
# GET 请求默认超时会重试一次
ribbon.MaxAutoRetriesNextServer=1

okhttp连接池

OkHttpClient.Builder builder = new OkHttpClient.Builder();
Dispatcher dispatcher = new Dispatcher();
//全部host并发数,默认64
dispatcher.setMaxRequests(dispatcherMaxRequests);
//单个host并发数,默认5
dispatcher.setMaxRequestsPerHost(dispatcherMaxRequestsPerHost);
builder.dispatcher(dispatcher);
//每个地址最大并发数,默认5,保留时间默认5分钟
ConnectionPool connectionPool = new ConnectionPool(connectionPoolMaxIdleCount, connectionPoolMaxIdleMinutes,
TimeUnit.MINUTES);
builder.connectionPool(connectionPool);
//链接超时,默认10秒
builder.connectTimeout(connTimeout, TimeUnit.SECONDS);
//读超时,默认10秒
builder.readTimeout(readTimeout, TimeUnit.SECONDS);
//写超时默认10秒
builder.writeTimeout(writeTimeout, TimeUnit.SECONDS);

常见问题

对象复制

org.springframework.cglib.beans.BeanCopier:基于cglib
org.springframework.beans.BeanUtils:基于反射机制

AopContext.currentProxy()无法获取问题

@EnableAspectJAutoProxy(exposeProxy = true,proxyTargetClass = true) # 开启Cglib代理

@Async无效,不起作用

1.没有在@SpringBootApplication启动类当中添加注解@EnableAsync注解。
2.异步方法使用注解@Async的返回值只能为void或者Future。
3.没有走Spring的代理类。因为@Transactional和@Async注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。
解决办法:
1.注解的方法必须是public方法。
2.方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的。
3.如果需要从类的内部调用,需要先获取其代理类-手动获取spring bean再调用

dynamic-datasource无事务

注意:开启Spring事务无法切换数据源
dynamic-datasource-spring-boot-starter不支持原生Spring事务,不过在第三方seata的支持下可用
https://github.com/baomidou/dynamic-datasource-spring-boot-starter/wiki/Integration-With-Seata

@Transactional不起作用

1、同一类中,一个方法调用另外一个有事务的方法,事务不起作用
2、同一类中,一个有事务的方法调用另外一个方法,另外的方法内的事务不起作用
3、只有运行时异常才能触发事务
4、多个数据库源的情况下事务不起作用
解决版本:
1、两个同时需要事务的方法不要写在同一个类中
2、把事务的注释写在类上
3、注入本身调用,注意添加@Lazy

spring-data-mongo使用问题

保存出现重复保存问题

如果Document中包含version字段时,在进行更新操作version必须保持一致,否则会当做一条新数据进行插入

MongoTemplate查询字段名需和数据库一致

使用Query、Criteria、Update时必须和数据库字段名称保持一致,如ID使用 _id

hibernate.validator统一参数校验问题

  1. 对于Get参数必须在类上添加@Validated注解
  2. 对于Post参数在参数或方法添加@Valid注解
  3. 校验类提示放在resouces目录下ValidationMessages_zh_CN.properties,占位符使用{0…}
  4. 提示类消息放在resouces/i18n目录下messages_zh_CN.properties,占位符使用{0…}
  5. 参数message使用{xxx.xxx}

    RestTemplate使用Okhttp3

    @Slf4j
    @Configuration
    @ConfigurationProperties(prefix = "spring.httpclient.okhttp")
    public class RestTemplateConfig {
     private int    maxIdleConnections    = 50;
     private int    keepAliveDuration    = 600;
     private int    connectTimeout        = 30;
     private int    readTimeout            = 40;
     private int    writeTimeout        = 600;
    
     @Bean
     public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
         return new RestTemplate(factory);
     }
    
     @Bean
     public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
         log.info(
                 "spring.httpclient.okhttp Configuration [maxIdleConnections={},keepAliveDuration={},connectTimeout={},readTimeout={},writeTimeout={}]",
                 maxIdleConnections, keepAliveDuration, connectTimeout, readTimeout, writeTimeout);
    
         OkHttpClient okHttpClient = new OkHttpClient.Builder()
                 .connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.SECONDS))
                 .connectTimeout(connectTimeout, TimeUnit.SECONDS).readTimeout(readTimeout, TimeUnit.SECONDS)
                 .writeTimeout(writeTimeout, TimeUnit.SECONDS).build();
    
         return new OkHttp3ClientHttpRequestFactory(okHttpClient);
     }
    }
    

    统一响应格式

    ```java @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface WapperResult {

}

@Data @Accessors(chain = true) public class Result { private int code = 0; private String msg = “SUCCESS”; private String errcode; private String errmsg; private Object data; private long timestmp = System.currentTimeMillis() / 1000;

public static Result ok(Object data) {
    return new Result().setData(data);
}

}

@RestControllerAdvice public class WapperResultHandlerAdvice implements ResponseBodyAdvice {

@Override
public boolean supports(MethodParameter returnType, Class converterType) {
    return null != returnType.getMethodAnnotation(WapperResult.class);
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
        Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    // 判断响应的Content-Type为JSON格式的body
    if (MediaType.APPLICATION_JSON.equals(selectedContentType)
            || MediaType.APPLICATION_JSON_UTF8.equals(selectedContentType)) {
        if (body instanceof Result) { // 如果响应返回的对象为统一响应体,则直接返回body
            return body;
        }
        // 只有正常返回的结果才会进入这个判断流程,所以返回正常成功的状态码
        return Result.ok(body);
    }
    // 非JSON格式body直接返回即可
    return body;
}

}

<a name="EBj1b"></a>
### 统一脱敏处理
```java

import java.util.function.Function;

/**
 * 脱敏类型
 */
public enum SensitiveType {
    /**
     * Username sensitive strategy.
     */
    USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
    /**
     * Id card sensitive type.
     */
    ID_CARD(s -> s.replaceAll("(\\d{4})\\d{2,10}(\\w{4})", "$1****$2")),
    /**
     * Phone sensitive type.
     */
    PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),

    /**
     * Address sensitive type.
     */
    ADDRESS(s -> s.replaceAll("(\\S{3})\\S{3}(\\S*)\\S{2}", "$1****$2****"));

    private final Function<String, String> function;

    SensitiveType(Function<String, String> function) {
        this.function = function;
    }

    /**
     * @param text
     *            待脱敏字符串
     * @return 脱敏处理结果
     */
    public String apply(String text) {
        return function.apply(text);
    }
}
//-----------
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

/**
 * JSON 脱敏注解
 */
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerialize.class)
public @interface Sensitive {
    SensitiveType value();
}
//-----------
import java.io.IOException;
import java.util.Objects;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;

/**
 * 
 * jackson 字段脱敏处理
 *
 */
public class SensitiveSerialize extends JsonSerializer<String> implements ContextualSerializer {
    private SensitiveType type;

    public SensitiveSerialize(SensitiveType type) {
        this.type = type;
    }

    public SensitiveSerialize() {
    }

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
            throws IOException, JsonProcessingException {
        gen.writeString(type.apply(value));
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
            throws JsonMappingException {
        if (property == null) {
            // 为空直接跳过
            return prov.findNullValueSerializer(property);
        }
        if (Objects.equals(property.getType().getRawClass(), String.class)) {
            Sensitive sensitive = property.getAnnotation(Sensitive.class);
            if (sensitive == null) {
                sensitive = property.getContextAnnotation(Sensitive.class);
            }
            if (sensitive != null) {
                return new SensitiveSerialize(sensitive.value());
            }
        }
        return prov.findValueSerializer(property.getType(), property);
    }
}
//使用示例
public class DemoVO {
    @Sensitive(SensitiveType.USERNAME)
    private String    name;
    @Sensitive(SensitiveType.PHONE)
    private String    mobile;
    @Sensitive(SensitiveType.ID_CARD)
    private String    idcard;
    @Sensitive(SensitiveType.ADDRESS)
    private String    address;
}

统一异常处理

/**
* 必须设置:spring.mvc.throw-exception-if-no-handler-found=true
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResultVO<String> handlerNoFoundException(NoHandlerFoundException e) {
    log.error("NoFoundException: {}", e.getMessage());
    return ResultVO.error(404, "路径不存在,请检查路径是否正确");
}

springboot 实现拦截

Filter

@Bean
    public FilterRegistrationBean customerFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();

        // 设置过滤器
        registration.setFilter(new CustomerFilter());

        // 拦截路由规则
        registration.addUrlPatterns("/intercept/*");

        // 设置初始化参数
        registration.addInitParameter("name", "customFilter");

        registration.setName("CustomerFilter");
        registration.setOrder(1);
        return registration;
    }

    public class CustomerFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(CustomerFilter.class);
    private String name;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        name = filterConfig.getInitParameter("name");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        logger.info("Filter {} handle before", name);
        chain.doFilter(request, response);
        logger.info("Filter {} handle after", name);
    }
}

@WebFilter

需要配合@ServletComponentScan才能生效

@Component
@ServletComponentScan
@WebFilter(urlPatterns = "/intercept/*", filterName = "annotateFilter")
public class AnnotateFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(AnnotateFilter.class);
    private final String name = "annotateFilter";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        logger.info("Filter {} handle before", name);
        chain.doFilter(request, response);
        logger.info("Filter {} handle after", name);
    }
}

HanlderInterceptor

public class CustomHandlerInterceptor implements HandlerInterceptor {

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

    /*
     * Controller方法调用前,返回true表示继续处理
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        HandlerMethod method = (HandlerMethod) handler;
        logger.info("CustomerHandlerInterceptor preHandle, {}", method.getMethod().getName());

        return true;
    }

    /*
     * Controller方法调用后,视图渲染前
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {

        HandlerMethod method = (HandlerMethod) handler;
        logger.info("CustomerHandlerInterceptor postHandle, {}", method.getMethod().getName());

        response.getOutputStream().write("append content".getBytes());
    }

    /*
     * 整个请求处理完,视图已渲染。如果存在异常则Exception不为空
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {

        HandlerMethod method = (HandlerMethod) handler;
        logger.info("CustomerHandlerInterceptor afterCompletion, {}", method.getMethod().getName());
    }

}

@Configuration
public class InterceptConfig extends WebMvcConfigurerAdapter {

    // 注册拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(new CustomHandlerInterceptor()).addPathPatterns("/intercept/**");
        super.addInterceptors(registry);
    }

@ExceptionHandler

用途是捕获方法执行时抛出的异常,通常可用于捕获全局异常,并输出自定义的结果。
需要与 @ControllerAdvice配合使用

@ControllerAdvice(assignableTypes = InterceptController.class)
public class CustomInterceptAdvice {

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

    /**
     * 拦截异常
     */
    @ExceptionHandler(value = { Exception.class })
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public String handle(Exception e, HandlerMethod m) {

        logger.info("CustomInterceptAdvice handle exception {}, method: {}", e.getMessage(), m.getMethod().getName());

        return e.getMessage();
    }
}

RequestBodyAdvice/ResponseBodyAdvice

RequestBodyAdvice 则可用于在请求内容对象转换的前后时刻进行拦截处理
ResponseBodyAdvice 的用途在于对返回内容做拦截处理

@Aspect

@Aspect
@Component
public class InterceptControllerAspect {

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

    @Pointcut("target(org.zales.dmo.boot.controllers.InterceptController)")
    public void interceptController() {

    }

    @Around("interceptController()")
    public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {

        logger.info("aspect before.");

        try {
            return joinPoint.proceed();
        } finally {
            logger.info("aspect after.");
        }
    }
}

@Pointcut 用于定义切面点,而使用target关键字可以定位到具体的类。
@Around 定义了一个切面处理方法,通过注入ProceedingJoinPoint对象达到控制的目的

注解 说明
@Before 方法执行之前
@After 方法执行之后
@Around 方法执行前后
@AfterThrowing 抛出异常后
@AfterReturing 正常返回后

多线程并发测试-junit5

  1. 添加配置文件:junit-platform.properties

    #是否允许并行执行true/false
    junit.jupiter.execution.parallel.enabled=true
    #是否支持方法级别多线程 same_thread/concurrent
    junit.jupiter.execution.parallel.mode.default = concurrent
    #是否支持类级别多线程 same_thread/concurrent
    junit.jupiter.execution.parallel.mode.classes.default = concurrent
    #the maximum pool size can bu configured using a ParalleExecutionConfigurationStrategy
    junit.jupiter.execution.parallel.config.strategy=fixed
    junit.jupiter.execution.parallel.config.fixed.parallelism = 200
    
  2. 使用@RepeatedTest

    @DisplayName("并发测试")
    @RepeatedTest(10000) // 10为当前用例执行的次数
    void batchTest() {
     System.out.println("Thread-" + Thread.currentThread().getId());
    }