JAVA笔记-SpringBoot的使用

一、常用注解

    1:@Resource和@AutoWried
       @Resource和@Autowired注解都是用来实现依赖注入的。只是@AutoWried按by type自动注入,而@Resource默认按byName自动注入
        @Resource有两个重要属性,分别是name和type
        spring将name属性解析为bean的名字,而type属性则被解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,如果使用type属性则使用byType的自动注入策略。如果都没有指定,则通过反射机制使用byName自动注入策略

    2:@JsonFormat和@DateTimeFormat
        @JsonFormat用于出参时格式换@DateTimeFormat用于入参时格式化
        @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")

    3:@PostConstruct和@PreDestroy
        javaEE5引入用于Servlet生命周期的注解,实现Bean初始化之前和销毁之前的自定义操作,也可以做初始化init()的一些操作。
        使用此注解会影响服务启动时间

// 用法一:动态变量获取并赋值给静态变量,减少了@Value的多次注入
@Component
public class SystemConstant {
    public static String env;

    @Value("${spring.profiles.active}")
    public String environment;

    @PostConstruct
    public void initialize() {
        System.out.println("初始化环境...");
        env= this.environment;
    }
}
// 用法二:将注入的Bean赋值给静态变量,减少依赖注入
@Component
public class RedisUtil {
    private static RedisTemplate<Object, Object> redisTemplates;

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;
    @PostConstruct
    public void initialize() {
        redisTemplates = this.redisTemplate;
    }
}

    4:@EnableAspectJAutoProxy(proxyTargetClass = false, exposeProxy = true)
        解决同类方法调用时异步@Async和事务@Transactional不生效问题,没有用到代理类导致
        参数proxyTargetClass控制aop的具体实现方式cglib或java的Proxy,默认为false(java的Proxy)
        参数exposeProxy控制代理的暴露方式,解决内部调用不能使用代理的场景,默认为false
        使用AopContext.currentProxy()获取一个代理类,然后用代理类再去调用就好了

// serviceImpl文件
@Async
@TargetDataSource("db1")
@Override
public void do(String value) {
    // 动态切换数据源要注意一个问题,就是在开启事物之前要先切换成需要的数据源,不要在开启事物之后在切换数据源不然会切换失败,因为一个事物的开启是建立在与一个数据源建立连接的基础上开启的所以如果先开启事物然后再切换数据源会报错,切换会失败
    ((Service) AopContext.currentProxy()).dosomething(value);
}

@Transactional
@Override
public void dosomething(String value) {
    //dosomething()
}

// 注入自己的方式无需开启注解
@Autowired
Service service;
@Async
@TargetDataSource("db1")
@Override
public void do(String value) {
    service.dosomething(value);
}

@Transactional
@Override
public void dosomething(String value) {
    //dosomething()
}

// 获取当前Bean的方式无需开启注解(使用hutool SpringUtil 或自己封装一个SpringUtil)
@Async
@TargetDataSource("db1")
@Override
public void do(String value) {
    currentBean().dosomething(value);
}

@Transactional
@Override
public void dosomething(String value) {
    //dosomething()
}
private Service currentBean() {
    return SpringUtil.getBean(Service.class);
}

// 反射调用也可用不用开启注解
@Autowired
Service service;
@Async
@TargetDataSource("db1")
@Override
public void do(String value) {
    currentClazz().getMethod("dosomething").invoke(service);
    currentClazz().getMethod("dosomething").invoke(currentBean());
    currentClazz().getMethod("dosomething").invoke(currentProxy());
}

@Transactional
@Override
public void dosomething(String value) {
    //dosomething()
}
private Class<Service> currentClazz() {
    return Service.class;
}

二、启动应用

    1:maven方式
        mvn spring-boot:run -Dspring-boot.run.profiles=xxx
    2:java -jar方式
        java -server -Xms128m -Xmx256m -Dserver.port=8080 -jar xxx.jar --spring.profiles.active=dev
    3:docker run方式(已经打好镜像)
        环境变量:docker run -d -p 8080:8080 --name xx -e "SPRING_PROFILES_ACTIVE=dev" images:tag
        启动参数(使用ENTRYPOINT []/CMD []时有效):docker run -d -p 8080:8080 --name xx images:tag --spring.profiles.active=dev
        参数加载优先级:配置文件<环境变量<JVM系统变量(-D)<命令行参数(--)
    4:docker-compose方式

#V2.0
environment:
  TZ:Asia/Shanghai
  JAVA_OPTS: -Dserver.port=8180
  JAVA_OPT_EXT: -server -Xms64m -Xmx64m -Xmn64m
#V3.0
environment:
  - TZ=Asia/Shanghai
  - JAVA_OPTS=-server -Dserver.port=8180 -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m -Xms128m -Xmx256m -Xmn128m -Xss256k -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC
  # rocketMq配置- JAVA_OPT_EXT= -server -Xms128m -Xmx128m -Xmn128m

三、Hibernate Validator的使用

    1:常用注解
        @NotNull                不能为null
        @AssertTrue         必须为true
        @AssertFalse        必须为false
        @Min                     必须为数字,其值大于或等于指定的最小值
        @Max                    必须为数字,其值小于或等于指定的最大值
        @DecimalMin        必须为数字,其值大于或等于指定的最小值
        @DecimalMax       必须为数字,其值小于或等于指定的最大值
        @Size                   集合的长度
        @Digits                 必须为数字,其值必须再可接受的范围内
        @Past                   必须是过去的日期
        @Future                必须是将来的日期
        @Pattern               必须符合正则表达式
        @Email                  必须是邮箱格式
        @Length                长度范围
        @NotEmpty            不能为null,长度大于0
        @Range                元素的大小范围
        @NotBlank            不能为null,trim()后字符串长度大于0(限字符串)
    2:注解校验

# 使用
@RestController
@Validated
public class ValidateController {
    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public String paramCheck(@Length(min = 10) @RequestParam String name) {}

    @RequestMapping(value = "/person", method = RequestMethod.POST)
    public void add(@RequestBody @Valid Person person) {}
}
# 配置快速校验返回
@Bean
public Validator validator(){
    ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
            .configure()
            // true  快速失败返回模式    false 普通模式.failFast(true)
            .addProperty( "hibernate.validator.fail_fast", "true" )
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

    3:手动校验
        一般我们在实体上加注解就可以实现入参的校验,但是如果在service层来进行校验的话也可以使用如下方式

// 手动校验入参
Set<ConstraintViolation<@Valid BudgetGetParamDTO>> validateSet = Validation.buildDefaultValidatorFactory()
                .getValidator()
                .validate(paramDTO, Default.class);
if (!CollectionUtils.isEmpty(validateSet)) {
// String messages = validateSet.stream().map(ConstraintViolation::getMessage).reduce((m1, m2) -> m1 + ";" + m2).orElse("参数输入有误!");
// Iterator<ConstraintViolation<BudgetGetParamDTO>> it = validateSet.iterator();
// List<String> errors = new ArrayList<>();
// while (it.hasNext()) {
//   ConstraintViolation<BudgetGetParamDTO> cv = it.next();
//   errors.add(cv.getPropertyPath() + ": " + cv.getMessage());
// }
String messages = validateSet.iterator().next().getPropertyPath() + ": " + validateSet.iterator().next().getMessage();
    return ResultUtil.error(0, messages);
}

    4:自定义校验器

# 使用@Pattern拓展一个校验器
@ConstraintComposition(CompositionType.OR)
//@Pattern(regexp = "1[3|4|5|7|8][0-9]\\d{8}")
@Pattern(regexp = "1\\d{10}")
@Null
@Length(min = 0, max = 0)
@Documented
@Constraint(validatedBy = {})
@Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@ReportAsSingleViolation
public @interface PhoneValid {
    String message() default "手机号校验错误";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
# 自定义一个校验器
public enum CaseMode {
    UPPER,
    LOWER;
}
@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    CaseMode value();
}
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
    private CaseMode caseMode;
    public void initialize(CheckCase checkCase) {
        this.caseMode = checkCase.value();}
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (s == null) {return true;}
        if (caseMode == CaseMode.UPPER) {
            return s.equals(s.toUpperCase());
        } else {
            return s.equals(s.toLowerCase());}
    }
}

    5:分组校验

@Data
public class DemoDto {
    public interface Default {}
    public interface Update {}
    @NotEmpty(message = "名称不能为空")
    private String name;
    @Length(min = 5, max = 25, message = "key的长度为5-25" ,groups = Default.class )
    private String key;
    @Pattern(regexp = "[012]", message = "无效的状态标志",groups = {Default.class,Update.class} )
    private String state;
}

@RequestMapping("test2")
public String test2(@Validated(value = DemoDto.Default.class) @RequestBody DemoDto dto){}
@RequestMapping("test4")
public String test4(@Validated(value = {DemoDto.Default.class,DemoDto.Update.class}) @RequestBody DemoDto dto){}

四、xml和实体转换(也可用使用xstream)

@Data
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "QueryResult")
public class RetDTO {
    @ApiModelProperty(value = "返回信息", notes = "MSG", example = "MSG")
    private String MSG;
    @ApiModelProperty(value = "是否成功 Y成功 N失败", notes = "Y", example = "Y")
    private String BSUCCESS;
    @ApiModelProperty(value = "结果", notes = "[]", example = "[]")
    private InfosDTO INFOS;
}

@Data
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "INFO")
public class InfosDTO {
    @ApiModelProperty(value = "结果集", notes = "[]", example = "[]")
    private List<ItemRetDTO> INFO;
}

@Data
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "INFO")
public class ItemRetDTO implements Serializable {
    @ApiModelProperty(value = "号", notes = "code", example = "code")
    @XmlElement(name = "CODE")
    private String code;
}

// xml转实体
String objStr = re.getBody().substring(re.getBody().indexOf("<QueryResult>"), re.getBody().indexOf("</QueryResult>")) + "</QueryResult>";
JAXBContext context = JAXBContext.newInstance(RetDTO.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
RetDTO obj = (RetDTO) unmarshaller.unmarshal(new StringReader(objStr));

// 实体转xml
String result = null;
JAXBContext context = JAXBContext.newInstance(obj.getClass());
Marshaller marshaller = context.createMarshaller();
// 指定是否使用换行和缩排对已编组 XML 数据进行格式化的属性名称。
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setProperty(Marshaller.JAXB_ENCODING, encoding);
StringWriter writer = new StringWriter();
marshaller.marshal(obj, writer);
result = writer.toString();
return result;

五、HttpServletRequest

    可以把servletRequest转换 HttpServletRequest httpRequest = (HttpServletRequest) servletRequest这样比ServletRequest多了一些针对于Http协议的方法。 例如:getHeader(), getMethod() , getSession()

六、从IOC中获取Bean

package com.bootbase.common.core.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

/**
 * @Description: 常用工具类
 */
@Slf4j
@Service
@Lazy(false)
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
    private static ApplicationContext applicationContext = null;
    /**
     * 取得存储在静态变量中的ApplicationContext.
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
    /**
     * 实现ApplicationContextAware接口, 注入Context到静态变量中.
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextHolder.applicationContext = applicationContext;
    }
    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) {
        return (T) applicationContext.getBean(name);
    }
    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    public static <T> T getBean(Class<T> requiredType) {
        return applicationContext.getBean(requiredType);
    }
    /**
     * 清除SpringContextHolder中的ApplicationContext为Null.
     */
    public static void clearHolder() {
        if (log.isDebugEnabled()) {
            log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
        }
        applicationContext = null;
    }
    /**
     * 发布事件
     *
     * @param event
     */
    public static void publishEvent(ApplicationEvent event) {
        if (applicationContext == null) {
            return;
        }
        applicationContext.publishEvent(event);
    }
    /**
     * 实现DisposableBean接口, 在Context关闭时清理静态变量.
     */
    @Override
    public void destroy() throws Exception {
        SpringContextHolder.clearHolder();
    }
}
// 使用 (XxxService) SpringContextHolder.getBean("xxxServiceImpl")

七、@Qualifier("XXX")注解

    当一个接口有2个不同实现时,使用@Autowired注解时会报会报错,此时可以结合@Qualifier("XXX")注解来解决这个问题。@Autowired和@Qualifier结合使用时,自动注入的策略会从byType转变成byName,还有就是使用@Primary可以理解为默认优先选择,同时不可以同时设置多个。

八、过滤器(Filter)和拦截器(Interceptor)

    过滤器赖于servlet容器,实例只能在容器初始化时调用一次,是用来做一些过滤操作,获取我们想要获取的数据,比如:在Javaweb中,对传入的request、response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者Controller进行业务逻辑操作。通常用的场景是:在过滤器中修改字符编码(CharacterEncodingFilter)、在过滤器中修改HttpServletRequest的一些参数(XSSFilter(自定义过滤器)),如:过滤低俗文字、危险字符等。
    拦截器依赖于web框架,基于Java的反射机制,属于面向切面编程(AOP)的一种运用,就是在service或者一个方法前,调用一个方法,或者在方法后,调用一个方法,比如动态代理就是拦截器的简单实现,在调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在调用方法后打印出字符串,甚至在抛出异常的时候做业务逻辑的操作。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。拦截器可以对静态资源的请求进行拦截处理。
    两者的本质区别:拦截器(Interceptor)是基于Java的反射机制,而过滤器(Filter)是基于函数回调。从灵活性上说拦截器功能更强大些,Filter能做的事情,都能做,而且可以在请求前,请求后执行,比较灵活。Filter主要是针对URL地址做一个编码的事情、过滤掉没用的参数、安全校验(比较泛的,比如登录不登录之类),太细的话,还是建议用interceptor。不过还是根据不同情况选择合适的

九、springcache + redis的使用

// 添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
// 配置
@EnableCaching // 注解会自动化配置合适的缓存管理器(CacheManager)
@Configuration
@AllArgsConstructor
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisTemplateConfig {
    private final RedisConnectionFactory factory;
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }
    @Bean
    public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
        return redisTemplate.opsForValue();
    }
    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }
    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }
    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }
    // @Bean // @EnableCaching也会自动注入
    // public CacheManager redisCacheManager() {
    //    RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
    //    cacheManager.setDefaultExpiration(300);
    //    cacheManager.setLoadRemoteCachesOnStartup(true); // 启动时加载远程缓存
    //    cacheManager.setUsePrefix(true); //是否使用前缀生成器
    //    // 这里可进行一些配置 包括默认过期时间 每个cacheName的过期时间等 前缀生成等等
    //    return cacheManager;
    //}
}
// 使用
@Cacheable(value = "models", key = "#testModel.name", condition = "#testModel.address !=  '' ")
    value (也可使用 cacheNames): 可看做命名空间,表示存到哪个缓存里了。
    key: 表示命名空间下缓存唯一key,使用Spring Expression Language(简称SpEL)生成。
    condition: 表示在哪种情况下才缓存结果(对应的还有unless,哪种情况不缓存),同样使用SpEL
    sync:是否同步
@CacheEvict(value = "models", allEntries = true)@CacheEvict(value = "models", key = "#name")
    value:指定命名空间
    allEntries:清空命名空间下缓存
    key:删除指定key缓存
    beforeInvocation:bool 在调用前清除缓存
@CacheEvict和@Scheduled(fixedDelay = 10000)同时用
    每10s删除一次
@CachePut(value = "models", key = "#name")刷新缓存

十、springboot i18n国际化

    1:返回信息国际化

# Resource 文件夹下添加多语言文件message.propertiies(默认) message_en_US.propertiies message_zh_CN.propertiies
 
# 在application.yml中添加配置多语言文件地址
spring:
  messages:
    encoding: UTF-8
    basename: i18n/messages

# 创建读取配置内容的工具类
@Component
public class LocaleMessageSourceService {
    @Resource
    private MessageSource messageSource;
    public String getMessage(String code) {
        return this.getMessage(code, new Object[]{});}
    public String getMessage(String code, String defaultMessage) {
        return this.getMessage(code, null, defaultMessage);}
    public String getMessage(String code, String defaultMessage, Locale locale) {
        return this.getMessage(code, null, defaultMessage, locale);}
    public String getMessage(String code, Locale locale) {
        return this.getMessage(code, null, "", locale);}
    public String getMessage(String code, Object[] args) {
        return this.getMessage(code, args, "");}
    public String getMessage(String code, Object[] args, Locale locale) {
        return this.getMessage(code, args, "", locale);}
    public String getMessage(String code, Object[] args, String defaultMessage) {
        Locale locale = LocaleContextHolder.getLocale();
        return this.getMessage(code, args, defaultMessage, locale);}
    public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
        return messageSource.getMessage(code, args, defaultMessage, locale);}

}

# 使用
@Autowired
privateLocaleMessageSourceService localeMessageSourceService;
localeMessageSourceService.getMessage("key"); // 只能读取i18n/messages**文件中的key

# 请求
发请求时在请求头加上Accept-Language zh-CN/zh-TW/zh-HK/en-US 和配置文件对应这里就不能为zh或en

    2:Vakidator校验国际化

# 在Resources目录下创建ValidationMessages.properties(默认) ValidationMessages_zh_CN.properties ValidationMessages_en_US.properties

# 默认情况下@Validated的message必须放在Resources下的ValidationMessages名称不可修改,如果想自定义可用通过配置修改
@Resource
private MessageSource messageSource;
@NotNull
@Bean
public Validator getValidator() {
    LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    // 这里使用系统i18n也就是 spring.messages.basename
    validator.setValidationMessageSource(this.messageSource);
    return validator;
}
或(如果配置了校验的快速失败返回应和快速失败返回配置在一起)
@Bean
public Validator validator(){
    ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
            .configure()
            .messageInterpolator(new LocaleContextMessageInterpolator(new ResourceBundleMessageInterpolator(new MessageSourceResourceBundleLocator(this.messageSource))))
            // true  快速失败返回模式    false 普通模式.failFast(true)
            .addProperty( "hibernate.validator.fail_fast", "true" )
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

# 使用
@NotBlank(message = "{key}")
private String name;

# 请求
发请求时在请求头加上Accept-Language zh-CN/zh-TW/zh-HK/en-US 和配置文件对应这里就不能为zh或en

    3:统一异常处理

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public R exception(Exception e) {
        log.error("全局异常信息 ex={}", e.getMessage(), e);
        return R.failed(e.getMessage());}
    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.OK)
    public R exception(BusinessException e) {
        log.error("业务处理异常 ex={}", e.getMessage(), e);
        return R.restResult(null, e.getCode(), e.getMessage());}
    @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public R bodyValidExceptionHandler(MethodArgumentNotValidException exception) {
        List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
        log.warn(fieldErrors.get(0).getDefaultMessage());
        return R.failed(fieldErrors.get(0).getDefaultMessage());}
    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 参数校验错误
    public R paramValidExceptionHandler(ConstraintViolationException exception) {
        Set<ConstraintViolation<?>> fieldErrors = exception.getConstraintViolations();
        List<ConstraintViolation<?>> errors = new ArrayList<>(fieldErrors);
        log.warn(errors.get(0).getMessage());
        return R.failed(errors.get(0).getMessage());}
    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 请求方式错误 json少body
    public R normalException(HttpMessageNotReadableException e) {
        String msg = e.getMessage();
        log.warn("校验异常 ex={}", msg);
        return R.failed(msg);}
}

十一、springboot 自定义错误页面

@ApiIgnore
@Controller
public class ErrorController extends BasicErrorController {
    @Autowired
    public ErrorController(ErrorAttributes errorAttributes,
                           ServerProperties serverProperties,
                           List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, serverProperties.getError(), errorViewResolvers);
    }

//    @Override
//    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
//        HttpStatus status = getStatus(request);
//        // 获取 Spring Boot 默认提供的错误信息,然后添加一个自定义的错误信息
//        Map<String, Object> model = getErrorAttributes(request,
//                isIncludeStackTrace(request, MediaType.TEXT_HTML));
//        model.put("msg", "出错啦++++++");
//        // resource/templates 目录下创建一个 errorPage.html 文件作为视图页面${code}
//        ModelAndView modelAndView = new ModelAndView("errorPage", model, status);
//        return modelAndView;
//    }

    @Override // ajax请求时不再返回错误页面,页面请求还是返回错误页
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> errorAttributes = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        // 获取错误信息 message/error
        String message = errorAttributes.get("message").toString();
        String path = errorAttributes.get("path").toString();
        Map<String, Object> retmap = new HashMap<>();
        BeanUtil.copyProperties(R.failed(path, message), retmap);
        return new ResponseEntity<>(retmap, status);
    }
}

十二、测试类

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = XxxApplication.class)
    public class XxxApplicationTests {
    @Test
    public void contextLoads() {}
    }

十三、统一配置接收参数空转null

@ControllerAdvice
public class GlobalControllerAdiviceConfig {
    // GET参数:WebDataBinder是用来绑定请求参数到指定的属性编辑器,可以继承WebBindingInitializer来实现一个全部controller共享的dataBiner Java代码
    @InitBinder
    public void dataBind(WebDataBinder binder) {
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
    }
    // 处理Post报文体 没有配置ObjectMapper的Bean时使用此方式
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder
                .deserializerByType(String.class, new StdScalarDeserializer<String>(String.class) {
                    @Override
                    public String deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
                        // 重点在这儿:如果为空白则返回null
                        String value = jsonParser.getValueAsString();
                        if (value == null || "".equals(value.trim())) {
                            return null;
                        }
                        return value.trim();
                    }
                });
    }
    // 存在ObjectMapper的Bean时直接在Bean中配置
    // 此方式或@Bean @Primary会覆盖默认配置
    @Bean
    public ObjectMapper getObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")));
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        // javaTimeModule只能手动注册,参考https://github.com/FasterXML/jackson-modules-java8
        objectMapper.registerModule(javaTimeModule);
        // 忽略json字符串中不识别的属性
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 忽略无法转换的对象
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 统一配置接收参数空转null
        SimpleModule strModule = new SimpleModule();
        strModule.addDeserializer(String.class, new StdScalarDeserializer<String>(String.class) {
            @Override
            public String deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
                // 重点在这儿:如果为空白则返回null
                String value = jsonParser.getValueAsString();
                if (value == null || "".equals(value.trim())) {
                    return null;
                }
                return value.trim();
            }
        });
        objectMapper.registerModule(strModule);
       // 对于String类型,使用objectMapper.registerModule(module)的方式;对于POJOs类型,使用objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true)的方式; 
       // objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
        return objectMapper;
    }
}
// 不要覆盖默认配置
// 我们通过实现Jackson2ObjectMapperBuilderCustomizer接口并注册到容器,进行个性化定制,Spring Boot不会覆盖默认ObjectMapper的配置,而是进行了合并增强,具体还会根据Jackson2ObjectMapperBuilderCustomizer实现类的Order优先级进行排序,因此上面的JacksonConfig配置类还实现了Ordered接口。
// 默认的Jackson2ObjectMapperBuilderCustomizerConfiguration优先级是0,因此如果我们想要覆盖配置,设置优先级大于0即可
// 在SpringBoot2环境下,不要将自定义的ObjectMapper对象注入容器,这样会将原有的ObjectMapper配置覆盖!
@Component
public class JacksonConfig implements Jackson2ObjectMapperBuilderCustomizer, Ordered {
    @Override
    public void customize(Jackson2ObjectMapperBuilder builder) {
        // 设置java.util.Date时间类的序列化以及反序列化的格式
        builder.simpleDateFormat(dateTimeFormat);
        // JSR 310日期时间处理
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat);
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter));
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat);
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormatter));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(dateFormatter));
        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(timeFormat);
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(timeFormatter));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(timeFormatter));
        builder.modules(javaTimeModule);
        // 全局转化Long类型为String,解决序列化后传入前端Long类型精度丢失问题
        builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
        builder.serializerByType(Long.class,ToStringSerializer.instance);
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

十四、spring.factories

    如果你有探索过这些Starter的原理,那你一定知道Spring Boot并没有消灭这些原本你要配置的Bean,而是将这些Bean做成了一些默认的配置类,同时利用/META-INF/spring.factories这个文件来指定要加载的默认配置。这样当Spring Boot应用启动的时候,就会根据引入的各种Starter中的/META-INF/spring.factories文件所指定的配置类去加载Bean
    对于自定义的jar包如果想让引入的工程加载jar中的Bean我们就需要仿照Starter的方式把相关的Bean放入此文件中
    Spring Boot2.7发布后不再推荐使用这种方式,3.0后将不支持这种写法
    老写法:org.springframework.boot.autoconfigure.EnableAutoConfiguration=\   com.spring4all.swagger.SwaggerAutoConfiguration
    新写法:创建文件/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports内容为com.spring4all.swagger.SwaggerAutoConfiguration

十五、RedisTemplate的使用

# 引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
# 配置
spring:
  redis:
    password: redis
    host: redis
    database: 1
# 序列化
@Configuration
public class MyRedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //参照StringRedisTemplate内部实现指定序列化器
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // new JdkSerializationRedisSerializer()或new GenericJackson2JsonRedisSerializer()
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        return redisTemplate;
    }
}
# 使用
# opsForValue()-操作字符串;opsForHash()-操作hash;opsForList()-操作list;opsForSet()-操作set;opsForZSet()-操作有序zset
redisTemplate.hasKey(key) // 判断key是否存在
redisTemplate.expire(key, time, TimeUnit.MINUTES) // 指定key的失效时间DAYS HOURS
redisTemplate.getExpire(key) // 根据key获取过期时间
redisTemplate.delete(key) // 根据key删除reids中缓存数据
redisTemplate.opsForValue().set("key1", "value1", 1, TimeUnit.MINUTES)
redisTemplate.opsForValue().set("key2", "value2")
redisTemplate.opsForValue().get("key1").toString()
redisTemplate.opsForList().leftPush("listkey1", list1)
(List<String>) redisTemplate.opsForList().leftPop("listkey1")
redisTemplate.opsForList().rightPush("listkey2", list2)
(List<String>) redisTemplate.opsForList().rightPop("listkey2")
redisTemplate.opsForHash().putAll("map1", map)
redisTemplate.opsForHash().entries("map1")
redisTemplate.opsForHash().values("map1")
redisTemplate.opsForHash().keys("map1")
(String) redisTemplate.opsForHash().get("map1", "key1")
redisTemplate.opsForSet().add("key1", "value1")
Set<String> resultSet = redisTemplate.opsForSet().members("key1")
ZSetOperations.TypedTuple<Object> objectTypedTuple1 = new DefaultTypedTuple<>("zset-5", 9.6)
Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>()
tuples.add(objectTypedTuple1)

十六、Websocket 配置

# 引入依赖
<!-- WebSocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

# 配置文件
@Configuration
@Slf4j
public class WebSocketConfiguration implements ServletContextInitializer {
    /**
     * 这个bean的注册,用于扫描带有@ServerEndpoint的注解成为websocket,如果你使用外置的tomcat就不需要该配置文件
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
    @Override
    public void onStartup(ServletContext servletContext) {
        log.info("WebSocket:::startup");
    }
}

# 操作接口类
public interface SocketSeverService {
    /**
     * 建立WebSocket连接
     * @param session session
     * @param userId  用户ID
     */
    void onOpen(Session session, String userId);
    /**
     * 发生错误
     * @param throwable e
     */
    void onError(Throwable throwable);
    /**
     * 连接关闭
     */
    public void onClose();
    /**
     * 接收客户端消息
     * @param message 接收的消息
     */
    void onMessage(String message);
    /**
     * 推送消息到指定用户
     * @param userId  用户ID
     * @param message 发送的消息
     */
    void sendMessageByUser(Integer userId, String message);
    /**
     * 群发消息
     * @param message 发送的消息
     */
    void sendAllMessage(String message);
}

# 操作实现
@ServerEndpoint("/websocket/{userId}")
@Component
@Slf4j
public class WebSocketSeverService implements SocketSeverService {
    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    // session集合,存放对应的session
    private static final ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>();
    // concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
    private static final CopyOnWriteArraySet<WebSocketSeverService> webSocketSet = new CopyOnWriteArraySet<>();
    @OnOpen
    @Override
    public void onOpen(Session session, @PathParam(value = "userId") String userId) {
        log.info("WebSocket:::建立连接中,连接用户ID:{}", userId);
        try {
            Session historySession = sessionPool.get(userId);
            // historySession不为空,说明已经有人登陆账号,应该删除登陆的WebSocket对象
            if (historySession != null) {
                webSocketSet.remove(historySession);
                historySession.close();
            }
        } catch (IOException e) {
            log.error("WebSocket:::重复登录异常,错误信息:" + e.getMessage(), e);
        }
        // 建立连接
        this.session = session;
        webSocketSet.add(this);
        sessionPool.put(userId, session);
        log.info("WebSocket:::建立连接完成,当前在线人数为:{}", webSocketSet.size());
    }
    @OnError
    @Override
    public void onError(Throwable throwable) {
        log.info("WebSocket:::error:{}", throwable.getMessage());
    }
    @OnClose
    @Override
    public void onClose() {
        webSocketSet.remove(this);
        log.info("WebSocket:::连接断开,当前在线人数为:{}", webSocketSet.size());
    }
    @OnMessage
    @Override
    public void onMessage(String message) {
        log.info("WebSocket:::收到客户端发来的消息:{}", message);
    }
    @Override
    public void sendMessageByUser(Integer userId, String message) {
        log.info("WebSocket:::发送消息,用户ID:" + userId + ",推送内容:" + message);
        Session session = sessionPool.get(userId);
        try {
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("WebSocket:::推送消息到指定用户发生错误:" + e.getMessage(), e);
        }
    }
    @Override
    public void sendAllMessage(String message) {
        log.info("WebSocket:::发送消息:{}", message);
        for (WebSocketSeverService webSocket : webSocketSet) {
            try {
                webSocket.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("WebSocket:::群发消息发生错误:" + e.getMessage(), e);
            }
        }
    }
}

# 使用
@Autowired
private SocketSeverService webSocketSeverService;
webSocketSeverService.sendAllMessage(JSONUtil.toJsonStr(msg));

# 前台连接-微信小程序
  data: {wx: null,pingInterval: null, lockReconnect: false, timer: null, limit: 0}
  reconnect(){
    if (this.lockReconnect) return;
    this.lockReconnect = true;
    clearTimeout(this.timer)
    if (this.data.limit<12){
      this.timer = setTimeout(() => {
        this.linkSocket();
        this.lockReconnect = false;
      }, 5000);
      this.setData({
        limit: this.data.limit+1
      })
    }
  },
  onShow: function() {
      let ws = wx.connectSocket({
        url: 'wss://api.shoutouf.com/testapi/websocket/'+this.data.apiData.query.userId,
        header:{
          'content-type': 'application/json',
          'Authorization': 'Bearer ' + wx.getStorageSync('access_token')
        },
        success: (e) => {console.log('ws连接成功', e)},
        fail: (e) => {console.log('ws连接失败', e)}
      })
      ws.onOpen((res) => {
        console.log(res)
        clearInterval(this.data.pingInterval)
        let pingInterval = setInterval(()=> {
            let msg={msg:'ping',toUser:'root'}
            wx.sendSocketMessage({data:JSON.stringify(msg)})
        }, 10000)
        this.setData({pingInterval: pingInterval})
      })
      ws.onMessage((res) => {console.log(222);console.log(res)})
      ws.onClose((res) => {console.log(333);this.reconnect()})
      ws.onError((res) => {console.log(22);heartCheck.reset();this.reconnect()})
      this.setData({ws: ws})
    }
  }
  onUnload: function () {
    this.data.ws.close()
    clearInterval(this.data.pingInterval)
  }
# 前台连接-vue
  npm i sockjs-client stompjs
  import SockJS from 'sockjs-client';
  import Stomp from 'stompjs';
  connection() {
        let headers = {Authorization: 'Bearer ' + "4cf7d2df-f4a2-4295-b267-03dca1910459"};
        // 建立连接对象,这里配置了代理
        this.socket = new SockJS('/other/ws', null, {timeout: 10000});
        // 连接服务端提供的通信接口,连接以后才可以订阅广播消息和个人消息
        // 获取STOMP子协议的客户端对象
        this.stompClient = Stomp.over(this.socket);
        // 向服务器发起websocket连接
        this.stompClient.connect(headers, () => {
            this.stompClient.subscribe('/app/topic/pc', function(greeting) {
              console.log(greeting, 668);
            });
          }, err => {console.log("订阅失败")});
  }

十七、Redisson 配置

# 引入排除高版本的springdata引入和自己springboot相对于的版本否则报错
# 引入redisson后本身包含redis-starter因此可以不用再引入了
<dependency>
    <!--redisson, 包含redis-->
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>、、
    <version>3.23.4</version>
    <exclusions>
        <exclusion>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-data-31</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-data-24</artifactId>
    <version>3.23.4</version>
</dependency>
# 配置-方式有yaml和文件两种方式
# yaml中redisson其实是字符串所以不支持ENC但是支持表达式,这里的password和host其实是为了加密可以不要直接放在redisson中
# codec是redis的序列化方式因为之前redis时配置的是默认jdk所以redisson也使用jdk,这里也可以使用json不过对于java来说使用redisson默认的Kryo5Codec和Jdk方式比json方式性能要高很多
spring:
  redis:
    password: ENC(83PKsY3qVKiIX9AehVyniQ==)
    host: localhost
    redisson:
      # 或 file: classpath:xxx.yml
      config: |
        singleServerConfig:
          address: redis://${spring.redis.host}:6379
          password: ${spring.redis.password}
        codec:
          class: "org.redisson.codec.SerializationCodec"
        transportMode: "NIO"
        lockWatchdogTimeout: 10000 # 看门狗默认30s这里配置10s
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
//        redisTemplate.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        return redisTemplate;
    }
    # 当使用CacheManager操作缓存时一定要配置序列化方式一致不然oauth2会抛出[no body]错误
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        //  解决Redisson引入后查询缓存转换异常的问题
    //        ObjectMapper om = new ObjectMapper();
    //        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    //        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 
    // ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        RedisCacheConfiguration config = 
RedisCacheConfiguration.defaultCacheConfig().serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));   //设置序列化器
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(config)
                .build();
    }
# 使用
@Autowired
private RedissonClient redissonClient;
RLock lock = redissonClient.getLock("key");
try {
    lock.lock();
} finally {
    if (lock.isLocked()) {
        lock.unlock();
    } else {
        // 执行超时看门狗释放锁抛异常回滚事务
        throw new BusinessException("操作失败请稍后重试");
    }
}

十八、Logback 配置

# 遇到一个问题,使用Logback时在logback.xml中使用${spring.application.name}获取配置文件属性时无法获取到,查阅资料后找到解决方案。
加载顺序logback.xml--->application.properties--->logback-spring.xml,所以需要把logback.xml修改为logback-spring.xml
在logback-spring.xml中用<springProperty scop="context" name="spring.application.name" source="spring.application.name" defaultValue=""/>这个来获取spring配置文件中的属性

# 日志从高到地低有 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL
日志输出规则  根据当前ROOT 级别,日志输出时,级别高于root默认的级别时会输出
属性描述
scan:设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false
<configuration scan="true" scanPeriod="60 seconds" debug="false"></configuration>

十九、若依框架相关

    1:配置开放接口
        在SecurityConfig.java中的.antMatchers("/mobile/login/**").permitAll()中追加路由地址,注意.anonymous()是允许匿名访问没有token时不能访问.permitAll()不管有没有都能访问;
        开放接口判断是否登录Authentication authentication = SecurityUtils.getAuthentication();
if (authentication != null && authentication.isAuthenticated() && !(authentication instanceof AnonymousAuthenticationToken)) {// 已登录}

二十、若依druid配置多数据源

# POM
<!-- 引入sqlserver数据库链接包 jtds支持TLS12去连 sqljdbc4支持10去连 sqlserver不开SSL时用sqljdbc4就可以,开SSL时低版本的sqlserver协议无法支持TLS12时需要改用jtds不然会报不支持TLS12错误 sqljdbc4是官方的而jtds不是。也有SSL下使用sqljdbc4通过修改jre环境配置来解决TLS不支持问题的不过个人感觉还是不要修改运行环境来处理。-->
<!--        <dependency>-->
<!--            <groupId>com.microsoft.sqlserver</groupId>-->
<!--            <artifactId>sqljdbc4</artifactId>-->
<!--            <version>4.0</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>net.sourceforge.jtds</groupId>
            <artifactId>jtds</artifactId>
            <version>1.3.1</version>
        </dependency>

# 数据源配置 如果使用不同数据库需要修改SELECT 1 FROM DUAL为SELECT 1因为DUAL是MYSQL特有的
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # 主库数据源
      master:
        url: jdbc:mysql://ip:3306/db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: db
        password: db
      # 从库数据源
      slave:
        # 从数据源开关/默认关闭
        enabled: true
        # sqljdbc4为com.microsoft.sqlserver.jdbc.SQLServerDriver
        driver-class-name: net.sourceforge.jtds.jdbc.Driver
        # sqljdbc4为jdbc:sqlserver://ip:1433;database=DB;SelectMethod=cursor;
        url: jdbc:jtds:sqlserver://ip:1433;DatabaseName=DB;
        username: db
        password: db
      # 初始连接数
      initialSize: 5
      # 最小连接池数量
      minIdle: 10
      # 最大连接池数量
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效
      validationQuery: SELECT 1
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # 设置白名单,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # 控制台管理用户名和密码
        login-username: admin
        login-password: admin
      filter:
        stat:
          enabled: true
          # 慢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true

# 若依通过DruidConfig来配置加载数据源和druid相关设置
# 在Service方法或Mapper方法通过@DataSource(DataSourceType.SLAVE)注解来切换

二十一、mybatis配置打印SQL

    mybatis-plus/mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl或
    logging:level:com.*: debug

二十二、定时任务的实现

    定时任务的使用场景很多比如订单超时关闭任务定时执行等,实现方式有很多常见的有:Java自带的DelayQueue、Redis过期监听、Redisson的DelayedQueue、死信队列等。
    Redis不会保证在设定的过期时间立即删除并发送过期通知,实际上,过期通知晚于设定的过期时间数分钟的情况也比较常见,所以Redis过期监听方式最好不要使用。
    Redisson DelayQueue 是一种基于 Redis Zset 结构的延时队列实现,它保证在Redis不崩溃的情况下不会丢失消息可以同时使用扫描数据库的方法作为补偿机制,避免中间件故障造成任务丢失,在轻量级架构中可以考虑使用。
    推荐使用RocketMQ、Pulsar等拥有定时投递功能的消息队列或者xxl-job、openjob这样的任务调度框架,其次可以考虑RabbitMQ死信队列和Redisson DelayQueue这样的延时队列,最好不要使用Redis过期监听
    下面看一下各种方式的实现

# Java DelayQueue 方式 start
public class MyDelay<T> implements Delayed {
    // 延迟时间,(时间单位会在计算剩余时间的方法getDelay里面,由你自己指定,一般来说都会使用毫秒,更精确一点。)
    long delayTime;
    // 过期时间,(时间单位会在计算剩余时间的方法getDelay里面,由你自己指定,一般来说都会使用毫秒,更精确一点。)
    long expire;
    // 你自己放进队列里的数据
    T data;

    public MyDelay(long delayTime, TimeUnit delayTimeUnit, T t) {
        // 将用户传进来的时间转换为毫秒
        this.delayTime = TimeUnit.MILLISECONDS.convert(delayTime, delayTimeUnit);
        // 过期时间 = 当前时间 + 延迟时间(时间单位会在计算剩余时间的方法getDelay里面,由你自己指定,一般来说都会使用毫秒,更精确一点。)
        // 当然你也可以使用别的时间,随意的
        this.expire = System.currentTimeMillis() + this.delayTime;
        data = t;
    }

    /**
     * 剩余时间 = 过期时间 - 当前时间
     */
    @Override
    public long getDelay(TimeUnit unit) {
        // 注意convert这个方法,第一个参数是一个long类型的数值,第二个参数的意思是告诉convert第一个long类型的值的单位是毫秒
        return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    /**
     * 优先级:俩个任务比较,时间短的优先执行
     */
    @Override
    public int compareTo(Delayed o) {
        long f = this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
        return (int) f;
    }

    @Override
    public String toString() {
        // 这个toString()方法不是必须的,你可以不重写。写不写都无所谓
        return "delayTime=" + delayTime + ",expire=" + expire + ",data=" + data;
    }
}
@Component
public class InfiniteLoopTask {
    static DelayQueue<Delayed> queue = new DelayQueue(); // 全局变量
    @Scheduled(fixedRate = 1000) // 每秒执行一次
    public void executeInfiniteLoop() {
        queue.add(new MyDelay(10, TimeUnit.SECONDS, "第一次添加任务"));
        queue.add(new MyDelay(1, TimeUnit.SECONDS, "第二次添加任务"));
        queue.add(new MyDelay(5, TimeUnit.SECONDS, "第三次添加任务"));
        queue.add(new MyDelay(10000, TimeUnit.MILLISECONDS, "第四次添加任务,只有到了指定的延迟时间才能调用queue.take()方法,把这个任务取出来"));

        while(!queue.isEmpty()){
            // queue.take()从延迟队列中取出任务,如果任务指定的延迟时间还没有到,这里是取不出来的,线程将一直阻塞
            // 线程状态将处于java.lang.Thread.State: TIMED_WAITING (parking),会释放CPU,底层调用的是 UNSAFE.park方法。
            Delayed delayed = queue.take();
            System.out.println("任务:" + delayed);
        }
    }
}
# Java DelayQueue 方式 end

# Redisson 方式 start
# Redisson 的配置参考上面
# 添加到队列
@Autowired
private RedissonClient redissonClient;
    // RBlockingDeque的实现类为:new RedissonBlockingDeque
    RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque("testDely");
    // RDelayedQueue的实现类为:new RedissonDelayedQueue
    RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);

    System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "添加任务到延时队列里面");
    delayedQueue.offer("添加一个任务", 3, TimeUnit.SECONDS);
    delayedQueue.offer("添加二个任务", 6, TimeUnit.SECONDS);
    delayedQueue.offer("添加三个任务", 9, TimeUnit.SECONDS);
# 消费
@Scheduled(fixedRate = 1000) // 每秒执行一次 或 使用while true
public void executeInfiniteLoop() {
    RBlockingDeque<String> blockingDeque = redissonClient.getBlockingDeque("testDely");
    // 注意虽然delayedQueue在这个方法里面没有用到,但是这行代码也是必不可少的。
    RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);

    while (blockingDeque.isExists()) {
        String a = blockingDeque.take();
        System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "延时队列收到:" + a);
    }
}
# 封装
@Component
@Slf4j
public class RedissonDelayQueue {
    @Autowired
    private RedissonClient redissonClient;
    private RDelayedQueue<String> delayQueue;
    private RBlockingQueue<String> blockingQueue;
    @PostConstruct
    public void init() {
        initDelayQueue();
        startDelayQueueConsumer();
    }
    private void initDelayQueue() {
        blockingQueue = redissonClient.getBlockingQueue("testDely");
        delayQueue = redissonClient.getDelayedQueue(blockingQueue);
    }
    @Scheduled(fixedRate = 1000) // 每秒执行一次
    private void executeInfiniteLoop() {
        while (blockingDeque.isExists()) {
            String a = blockingDeque.take();
            log.info("接收到延迟任务:{}", task);
        }
    }
    private void startDelayQueueConsumer() {
        new Thread(() -> {
            while (true) {
                try {
                    String task = blockingQueue.take();
                    log.info("接收到延迟任务:{}", task);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "testDely-Consumer").start();
    }
    public void offerTask(String task, long seconds) {
        log.info("添加延迟任务:{} 延迟时间:{}s", task, seconds);
        delayQueue.offer(task, seconds, TimeUnit.SECONDS);
    }
}
# 发送
@Autowired
private RedissonDelayQueue redissonDelayQueue;
redissonDelayQueue.offerTask(task, 5)
# Redisson 方式 end

# Redis 过期监听方式 start
@Configuration
public class RedisConfiguration {
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setConnectionFactory(connectionFactory);
        return redisMessageListenerContainer;
    }
    @Bean
    public KeyExpirationEventMessageListener redisKeyExpirationListener(RedisMessageListenerContainer redisMessageListenerContainer) {
        return new KeyExpirationEventMessageListener(redisMessageListenerContainer);
    }
}
# 监听有时不会马上收到,key的过期事件发布时机并不是当这个key的过期时间到了之后就发布,而是这个key在Redis中被清理之后,也就是真正被删除之后才会发布。(惰性清除-当这个key过期之后,访问时,这个Key才会被清除;定时清除-后台会定期(没有固定时间)检查一部分key,如果有key过期了,就会被清除
@Component
public class MyRedisKeyExpiredEventListener implements ApplicationListener<RedisKeyExpiredEvent> {
    @Override
    public void onApplicationEvent(RedisKeyExpiredEvent event) {
        byte[] body = event.getSource();
        System.out.println("获取到延迟消息:" + new String(body));
    }
}

# Redis 过期监听方式 end

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值