开发日志
酒债寻常行处有,人生七十古来稀。
参数校验——JSR303介绍和使用
关于JSR
JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
关于 JSR-303
JSR-303 是JAVA EE 6 中的一项子规范,叫做 Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
Hibernate 对其实现
Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
简单介绍
补充
Hibernate 中填充一部分
代码实现
依赖
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.16.Final</version>
</dependency>
(1)给要校验的javaBean上标注校验注解
@ToString
@Getter
@Setter
public class UmsAdminParam {
//规定长度
@Length(min = 6, max = 19, message = "用户名长度是6-18位")
@ApiModelProperty(value = "用户名", required = true)
private String username;
@ApiModelProperty(value = "密码", required = true)
private String password;
//不能是空的
@NotEmpty
@ApiModelProperty(value = "用户头像")
private String icon;
@Email(message = "邮箱格式错误")
@ApiModelProperty(value = "邮箱")
private String email;
@NotNull
@ApiModelProperty(value = "用户昵称")
private String nickName;
@ApiModelProperty(value = "备注")
private String note;
}
(2)告诉spring这个数据需要校验@Valid
(3)感知校验成功/失败
public Stringregister(@Valid @RequestBody UmsAdminParam user,BindingResult result) {
//得到所有错误信息计数
int errorCount = result.getErrorCount();
//错误数大于0
if (errorCount>0){
//得到所有错误
List<FieldError> fieldErrors = result.getFieldErrors();
//迭代错误
fieldErrors.forEach((fieldError)->{
//错误信息
String field = fieldError.getField();
log.debug("属性:{},传来的值是:{},出错的提示消息:{}",
field,fieldError.getRejectedValue(),fieldError.getDefaultMessage());
});
return fieldError.getRejectedValue()+"出错:"+fieldError.getDefaultMessage();
}else{
return "登录成功";
}
}
分组校验
适用于有些字段在部分情况下才需要校验
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
@NotNull(groups = {UpdateGroup.class, UpdateStatusGroup.class},message = "更新操作需要指明id")
@Null(groups = AddGroup.class,message = "新增时无需指明id")
private Long brandId;
...
}
其中 AddGroup、UpdateStatusGroup 为标识性接口
public interface AddGroup {
}
public interface UpdateStatusGroup {
}
使用:
@RequestMapping("/update/status")
public R updateStatus(@Validated(value = UpdateStatusGroup.class) @RequestBody BrandEntity brand) {
brandService.updateById(brand);
return R.ok();
}
自定义参数校验注解
-
创建一个校验 IP 地址的注解
/** * <p>自定义参数校验注解</p> * <p>IPv4地址校验</p> * @author pikachu * @since 2023/5/5 20:28 */ @Target({ElementType.FIELD,ElementType.PARAMETER}) @Documented @Inherited @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = IPv4ConstraintValidator.class) public @interface IPv4 { String message() default "{com.pika.validation.annotation.IPv4}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; /** * 额外允许的IP段 */ String[] allows() default {}; }
-
配置默认错误消息(可选)
在 resources 目录下创建ValidationMessages.properties
com.pika.validation.annotation.IPv4=非法的IP地址
-
实现
ConstraintValidator
接口,并重写isValid
方法@Slf4j public class IPv4ConstraintValidator implements ConstraintValidator<IPv4, String> { private Set<String> allows; private final String IPV4_REGEX = "^([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}$"; @Override public void initialize(IPv4 constraintAnnotation) { if (Arrays.stream(constraintAnnotation.allows()).anyMatch(s -> !s.matches(IPV4_REGEX))) { log.warn("允许的ip设置错误"); } allows = new HashSet<>(List.of(constraintAnnotation.allows())); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (!StringUtils.hasText(value)) { return false; } if (allows.size() > 0 && allows.contains(value)) { return true; } else if (value.matches(IPV4_REGEX)) { return true; } return false; } }
-
注意
单独使用 @Valid 时不会生效
@GetMapping("3") public R test3(@Valid @IPv4 @RequestParam String ip) { return R.ok().put("ip", ip); }
Spring并不会进行参数校验
-
测试
一定不要忘记在类上加上
Validated
注解了,这个参数可以告诉 Spring 去校验方法参数。@RestController @RequestMapping("test") @Validated public class TestController { @GetMapping("3") public R test3(@Valid @IPv4 @RequestParam String ip) { return R.ok().put("ip", ip); } }
分层领域模型规约
- DO(Data Object):此对象的属性与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
- DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
- BO(Business Object):业务对象,由 Service 层输出的封装业务逻辑的对象。
- AO(ApplicationObject):应用对象,在Web层与Service层之间抽象的复用对象模型, 极为贴近展示层,复用度不高。
- VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
- Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。
领域模型命名规约:
- 数据对象:xxxDO,xxx即为数据表名
- 数据传输对象:xxxDTO,xxx为业务领域相关的名称。
- 展示对象:xxxVO,xxx一般为网页名称。
- POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。
先看一张各个对象间的关系图,有个印象
然后,再来慢慢解释各个对象的作用。
VO
Value Object
用于表示前端的展示对象;相比与PO(数据库映射对象),VO对象与前端交互的数据可能需要经过过滤、拆分、聚合等操作;比方说部分不需要展示的数据,VO层将其踢出后返回;如果数据来源于多个地方,也将会在VO对象进行聚合再返回等操作;
遵循Java Bean的规范,其拥有getter / setter方法,对于请求的命名时可加上Req后缀,响应的可加上Rep后缀
DTO
Data Transfer Object
数据传输对象;DTO主要协调于各个服务之间,用于做数据的扭转并传输;比如,数据库有20个字段,但实际业务只需要5个,那么就可以借助DTO对PO对象进行传输;避免数据库结构的暴露,并减少不必要的数据交互
遵循Java Bean的规范,其拥有getter / setter方法
BO
Business Object
表示一个业务对象;BO包含了一些业务逻辑,通常用于封装对DAO、RPC等相关的调用,同时还可以进行PO、VO、DTO之间的数据转换;
BO通常都是位于业务层,并提供了基本的业务操作;在设计上属于被服务层业务逻辑调用的对象,一段业务的执行,可能需要多个BO对象的相互配合才能完成
PO
persistant object
表示着Java对象与数据库之间的映射关系;其仅用于表示数据,并没有任何的数据操作;
遵循Java Bean的规范,其拥有getter / setter方法
DAO
Data Access Object
通过Dao配合PO对象进行数据库访问,其中包含了增删改查等一系列的数据库操作,DAO一般在持久层,其完全封装了数据库的行为,并对外提供方法,上层通过他访问数据完全不需要关心数据库任何信息;
POJO
Plain Ordinary Java Object 的缩写
表示一个简单 java 对象;只要遵循Java Bean的规范,并赋予getter / setter方法,就是一个POJO对象;
只是在不用的场景,不同的功能和定义下,POJO会演变为PO、VO、DTO等
java.util.Optional
java.util.Optional是Java 8新增的类,作为一个持有实例的容器类,可以帮我们把判空的代码写得更优雅,并且该类还提供了一些实用的api,官方文档在这里,接下来我们通过实战来学习吧:
三种Optional构造方法
第一种. Optional.of(Object object):入参object不能为空,否则会抛出空指针异常,查看Optional源码发现会调用Objects.requireNonNull方法,里面有判空:
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
第二种. Optional.ofNullable(Object object):入参object可以为空,如果object不为空,就创建一个Optional实例;如果object为空就返回一个static fainal的Option对象,注意这里不会新建Option实例,而是使用一个static final的实例EMPTY,这里比较有意思的是泛型的问题,例如我需要两个Optional对象,类型分别是String和Integer,代码如下:
Optional<String> optionalStr = Optional.ofNullable(null);
Optional<Integer> optionalInt = Optional.ofNullable(null);
类型不同又如何保证返回同一个对象呢?直接看ofNullable的源码,发现会调用empty方法:
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
原来是通过强制转换实现的,再看EMPTY对象:
private static final Optional<?> EMPTY = new Optional<>();
是通过”?”声明的;
第三种. Optional.empty():就是上面分析Optional.ofNullable的时候用到的empty方法,直接返回一个static final的实例EMPTY;
Optional.of()方法的用法有点像断言,对象为空的时候代表着某种业务上不可接受的异常,需要尽早处理,并且业务拒绝执行,这种场景下可以使用Optional.of;
接下来我们开始实战吧;
例子中用到的对象:Student
Student是个普通的bean,有三个字段和对应的get&set方法
@Data
public class Student {
private int id;
private String name;
private int age;
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}
Optional.ofNullable的用法
下面举例说明最常用的Optional.ofNullable,我们打算根据名称从其他系统获取student对象,如果对象为空就返回默认对象,先看不用Optional的时候我们平常是怎么写的,如下代码所示,标准的if&else判断:
private Student queryById(int id){
//TODO 这里模拟从数据库查询
return null;
}
public Student getStudent(int id){
Student student = queryById(id));
//如果为空就返回DEFAULT对象
return student==null ? DEFAULT : student;
}
用Optional之后,如下所示,不需要通过判空来避免空指针异常了:
private Student queryById(int id){
//TODO 这里模拟从数据库查询
return null;
}
public Student getStudent(int id){
Optional<Student> optional = Optional.ofNullable(queryById(id));
//如果为空就返回DEFAULT对象
return optional.orElse(DEFAULT);
}
orElse方法可以指定一个value为空时的返回对象,如果这个对象需要调用方法才能获取(例如我们拿不到DEFAULT对象,要通过getDefault()方法才能拿到),这是就需要orElseGet方法来达到目的,如下:
private Student queryById(int id){
//TODO 这里模拟从数据库查询
return null;
}
private Student getDefault(){
return DEFAULT;
}
public Student getStudent(int id){
Optional<Student> optional = Optional.ofNullable(queryById(id));
//如果为空就返回DEFAULT对象
return optional.orElseGet(() -> getDefault());
}
Optional的map方法
假如我们的需求是student对象非空就返回name的大写,如果student对象为空就返回”invalid”,在没有Optional的时候写法如下,除了检查student变量是否为空,还要检查name是否为空:
private Student queryById(int id){
//TODO 这里模拟从数据库查询
return null;
}
public String getStudentUpperName(int id){
Student student = queryById(id);
if(student!=null && student.getName()!=null){
return student.getName().toUpperCase();
}
return "invalid";
}
用了Optional可以这么写:
private Student queryById(int id){
//TODO 这里模拟从数据库查询
return null;
}
public String getStudentUpperName(int id){
Optional<Student> optional = Optional.ofNullable(queryById(id));
return optional.map(student -> student.getName())
.map(name -> name.toUpperCase())
.orElse("invalid");
}
MVC
controller
- 处理请求,接收和校验数据(jsr303)
- 调用service进行业务处理
- 封装service处理完的数据为所需vo
通用返回结果封装
package com.pika.common.utils;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.TypeReference;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Map;
@SuppressWarnings("unused")
public class R extends JSONObject {
private static final long serialVersionUID = 1L;
public static final String RESULT_DATA_NAME = "data";
public static final String RESULT_MESSAGE_NAME = "msg";
public static final String RESULT_DEFAULT_MESSAGE = "success";
public static final String RESULT_CODE_NAME = "code";
public static final String RESULT_TOKEN_NAME = "token";
public static final int RESULT_SUCCESS_CODE = 200;
public static final int RESULT_ERROR_CODE = 500;
public static final String APPLICATION_JSON_VALUE = "application/json";
public static final String DEFAULT_CHARSET = "UTF-8";
private R() {
put(RESULT_CODE_NAME, RESULT_SUCCESS_CODE);
put(RESULT_MESSAGE_NAME, RESULT_DEFAULT_MESSAGE);
}
public static R error() {
return error(RESULT_ERROR_CODE, "未知异常,请联系管理员");
}
public static R error(String message) {
return error(RESULT_ERROR_CODE, message);
}
public static R error(int code, String message) {
R r = new R();
r.put(RESULT_CODE_NAME, code);
r.put(RESULT_MESSAGE_NAME, message);
return r;
}
public static R ok(String message) {
R r = new R();
r.put(RESULT_MESSAGE_NAME, message);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public String getToken() {
return getString(RESULT_TOKEN_NAME);
}
public R setToken(String token) {
put(RESULT_TOKEN_NAME, token);
return this;
}
public R setData(Object data) {
this.put(RESULT_DATA_NAME, data);
return this;
}
public Object getData() {
return get(RESULT_DATA_NAME);
}
public <T> T getData(TypeReference<T> typeReference) {
return getData(RESULT_DATA_NAME, typeReference);
}
public <T> T getData(String key, TypeReference<T> typeReference) {
return getObject(key, typeReference, JSONReader.Feature.IgnoreNoneSerializable);
}
public <T> T getData(Class<T> clazz) {
return getObject(RESULT_DATA_NAME, clazz, JSONReader.Feature.IgnoreNoneSerializable);
}
public R setMsg(String msg) {
this.put(RESULT_MESSAGE_NAME, msg);
return this;
}
public String getMsg() {
return getString(RESULT_MESSAGE_NAME);
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public Integer getCode() {
return getIntValue(RESULT_CODE_NAME);
}
public R setCode(Integer code) {
super.put(RESULT_CODE_NAME, code);
return this;
}
public static void sendResponse(String msg) {
sendResponse(currResponse(), msg);
}
public static void sendResponse(HttpServletResponse response, String msg) {
sendResponse(response, RESULT_SUCCESS_CODE, msg);
}
public static void sendResponse(HttpServletResponse response, int code, String msg) {
try {
response.setStatus(RESULT_SUCCESS_CODE);
response.setContentType(APPLICATION_JSON_VALUE);
response.setCharacterEncoding(DEFAULT_CHARSET);
response.getWriter().print(JSON.toJSONString(error(code, msg)));
} catch (Exception ignored) {
}
}
public static void sendToken(HttpServletResponse response, String token) {
sendToken(currResponse(), RESULT_TOKEN_NAME, token);
}
public static void sendToken(HttpServletResponse response, String tokenName, String token) {
try {
response.setStatus(RESULT_SUCCESS_CODE);
response.setContentType(APPLICATION_JSON_VALUE);
response.setCharacterEncoding(DEFAULT_CHARSET);
response.getWriter().print(JSON.toJSONString(ok().put(tokenName, token)));
} catch (Exception ignored) {
}
}
public static HttpServletRequest currRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
return attributes.getRequest();
}
public static HttpServletResponse currResponse() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
return attributes.getResponse();
}
}
获取当前request
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
Feign远程调用时对象接收
统一的异常处理
@RestControllerAdvice异常控制器,@ExceptionHandler异常捕获范围
package com.pika.gstore.product.exception;
import com.pika.gstore.common.exception.BaseException;
import com.pika.gstore.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
/**
* Desc:
*
* @author pikachu
* @since 2022/11/27 21:41
*/
@RestControllerAdvice(basePackages = "com.pika.gstore.product.controller")
@Slf4j
public class ExceptionController {
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
public R handle(MethodArgumentNotValidException e) {
HashMap<String, String> map = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(fieldError ->
map.put(fieldError.getField(), fieldError.getDefaultMessage()
));
log.error(e.getMessage());
return R.error(BaseException.INVALID_DATA.getCode(), BaseException.INVALID_DATA.getMsg()).put("error", map);
}
@ExceptionHandler(value = HttpMessageNotReadableException.class)
public R test1(HttpMessageNotReadableException e) {
log.error(e.getMessage());
return R.error(BaseException.CONVERT_ERROR.getCode(), BaseException.CONVERT_ERROR.getMsg());
}
@ExceptionHandler(value = Exception.class)
public R test2(Exception e) {
String name = e.getClass().getName();
log.error(name);
e.printStackTrace();
return R.error(BaseException.UNKOWN_EXCEPTION.getCode(), e.getMessage());
}
}
Json
全局日期格式化
spring:
# jackson时间格式化
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
Json转Bean,使用hutool工具,info为Map类型数据,直接强转为SkuInfoVo会出错
错误转换:
@org.junit.jupiter.api.Test
public void test4() {
String json ="{skuId=1, spuId=1, skuName=Apple iPhone 14 Pro 暗紫色 8+128, skuDesc=null, catalogId=225, brandId=5, skuDefaultImg=https://gstore-piks.oss-cn-hangzhou.aliyuncs.com/2022/12/04/a90228bf-ba23-4dbb-8b20-b750f86d04d5_Snipaste_2022-12-04_19-21-57.png, skuTitle=Apple iPhone 14 Pro 暗紫色 8+128, skuSubtitle=支持移动联通电信5G 双卡双待手机, price=9429.0, saleCount=0}";
SkuInfoVo skuInfoVo = JSONUtil.toBean(json, SkuInfoVo.class);
System.out.println("skuInfoVo = " + skuInfoVo);
}
JSONUtil.parse(info).toBean(SkuInfoVo.class);
密码加密
- 服务器:
可使用hutool或者springframework自带的工具方法,两者兼容,可互相密码匹配
存在漏洞:Vulnerable API usage CVE-2020-5408 Use of Insufficiently Random Values vulnerability pending CVSS allocation
@Test
public void test2(){
//huTool
// $2a$10$HGWZrtojpX4POBWrmQFL9OjJBxSVBDEqsy9Ue4DSc2hYaW8YvhJ1q
// $2a$10$yQtLP1KTKGZ/.PrFyMqMy.jdGbjIYbdGp/4pamejZZNwothg35ir2
// 加密
String encode = BCrypt.hashpw("12345");
//plaintext – 需要验证的明文密码 hashed – 密文
boolean checkpw = BCrypt.checkpw("12345", encode);
System.out.println("encode = " + encode);
System.out.println("checkpw = " + checkpw);
}
@Test
public void test3(){
//org.springframework.security.crypto.bcrypt
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encode = encoder.encode("12345");
boolean matches = encoder.matches("12345", encode);
System.out.println("encode = " + encode);
System.out.println("matches = " + matches);
}
- 浏览器
结果排序——自定义比较器
-
中文字符串按字母序号排序
语言比较器:Collator.getInstance( )
@Test public void test1() throws Exception { String[] text = new String[]{"加油","Huawei","华为","加油","China"}; List<String> list = Arrays.stream(text).sorted(Collator.getInstance(Locale.CHINA)).toList(); System.out.println("list = " + list); }
输出:
list = [China, Huawei, 华为, 加油, 加油]
Service
业务逻辑层,需要考虑事务控制、业务流程处理。
Model
模型视图
View
仅作视图跳转的Controller配置:
@Configuration
public class ViewMappingConfig implements WebMvcConfigurer {
/**
* Configure simple automated controllers pre-configured with the response
* status code and/or a view to render the response body. This is useful in
* cases where there is no need for custom controller logic -- e.g. render a
* home page, perform simple site URL redirects, return a 404 status with
* HTML content, a 204 with no content, and more.
*
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login.html").setViewName("login");
registry.addViewController("/reg.html").setViewName("reg");
WebMvcConfigurer.super.addViewControllers(registry);
}
}
转发视图时为避免静态资源加载问题,需要在url 中添加域名,在RedirectAttributes中添加视图属性变量
@PostMapping("registry")
public String registry(@Valid UserRegistryReqVo reqVo,
BindingResult result,RedirectAttributes redirectAttributes){
if(result.hasErrors()){
Map<String, String> map=result.getFieldErrors().stream()
.filter(i->i.getDefaultMessage()!=null)
.collect(Collectors.toMap(FieldError::getField,DefaultMessageSourceResolvable::getDefaultMessage));
//仅可取出一次的数据
// TODO: 2023/1/10 分布式session
redirectAttributes.addFlashAttribute("errors",map);
return"redirect:http://auth.gulimall.com/reg.html";
}else{
...
}
}
redirectAttributes.addFlashAttribute(),将数据存放在session中,且只能获取一次
redirectAttributes.addAttribute(),将数据拼接在将要跳转的url 后面
DAO
mybatisPlus
LambdaQueryWrapper实现set num = num+1
不存在记录则新增,存在则更新stock=stock+skuNum
WareSkuEntity wareSku = new WareSkuEntity();
wareSku.setWareId(wareId);
wareSku.setSkuId(skuId);
wareSku.setStock(skuNum);
LambdaUpdateWrapper<WareSkuEntity> wrapper = new LambdaUpdateWrapper<>();
wrapper.setSql(skuNum > 0, "stock=stock+" + skuNum)
.eq(WareSkuEntity::getWareId, wareId)
.eq(WareSkuEntity::getWareId, wareId);
saveOrUpdate(wareSku, wrapper);
逻辑删除@TableLogic
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
# logic-delete-field: show_status # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 0 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 1 # 逻辑未删除值(默认为 0)
返回结果处理
resultMap不支持封装内部类
缓存
Redis缓存中间件的使用
基本步骤
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
-
配置yaml
-
使用逻辑
-
往redis存放的数据实时性,一致性要求不高
-
往redis存储的数据类型尽量跨平台兼容,如json
-
业务逻辑需要从数据库获取数据时先从redis中获取,
若获取成功则json转换处理;
获取失败则需要查询数据库,并将获取的数据转换为json存入redis
-
问题
Redisson分布式锁
依赖:
<!-- redisson分布式锁,分布式对象 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.19.0</version>
</dependency>
配置:
@Configuration
@Data
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value(("${spring.redis.port}"))
private String port;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port);
return Redisson.create(config);
}
}
https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8
看门狗机制
大家都知道,如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {
//1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
//2、加锁
myLock.lock(); //阻塞式等待。默认加的锁都是30s
//1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
//2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
// myLock.lock(10,TimeUnit.SECONDS); //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
//问题:在锁时间到了以后,不会自动续期
//1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
//2、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
//只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
// internalLockLeaseTime 【看门狗时间】 / 3, 10s
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception ex) {
ex.printStackTrace();
} finally {
//3、解锁 假设解锁代码没有运行,Redisson会不会出现死锁
System.out.println("释放锁..." + Thread.currentThread().getId());
myLock.unlock();
}
return "hello";
}
读写锁
基于Redis的Redisson分布式可重入读写锁RReadWriteLock
Java对象实现了java.util.concurrent.locks.ReadWriteLock
接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
/**
* 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁
* 写锁没释放读锁必须等待
* 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功
* 写 + 读 :必须等待写锁释放
* 写 + 写 :阻塞方式
* 读 + 写 :有读锁。写也需要等待
* 只要有读或者写的存都必须等待
* @return
*/
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock rLock = readWriteLock.writeLock();
try {
//1、改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("writeValue",s);
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@GetMapping(value = "/read")
@ResponseBody
public String readValue() {
String s = "";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
//加读锁
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
s = ops.get("writeValue");
try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
信号量
RLock
对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException
错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore
对象.
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore
采用了与java.util.concurrent.Semaphore
相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
/**
* 车库停车
* 3车位
* 信号量也可以做分布式限流
*/
@GetMapping(value = "/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.acquire(); //获取一个信号、获取一个值,占一个车位
boolean flag = park.tryAcquire();
if (flag) {
//执行业务
} else {
return "error";
}
return "ok=>" + flag;
}
@GetMapping(value = "/go")
@ResponseBody
public String go() {
RSemaphore park = redisson.getSemaphore("park");
park.release(); //释放一个车位
return "ok";
}
SpringCache
Spring | JSR-107 | Remark |
---|---|---|
@Cacheable |
@CacheResult |
非常相似。@ CacheResult 可以缓存特定的异常,并强制执行方法,而不管缓存的内容如何。 |
@CachePut |
@CachePut |
当 Spring 使用方法调用的结果更新缓存时,JCache 要求将它作为一个参数传递,该参数用@CacheValue 进行注释。由于这种差异,JCache 允许在实际方法调用之前或之后更新缓存。 |
@CacheEvict |
@CacheRemove |
非常相似。当方法调用导致异常时,@CacheRemove 支持条件驱逐。 |
@CacheEvict(allEntries=true) |
@CacheRemoveAll |
See @CacheRemove . |
@CacheConfig |
@CacheDefaults |
允许您以类似的方式配置相同的概念。 |
使用步骤
- 配置依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
spring:
#缓存
cache:
type: redis
redis:
#过期时间 ms
time-to-live: 3600000 #60*60*1000=1h
cache-null-values: true #是否缓存空值
key-prefix: 'cache::product::' #use-key-prefix=true时作为键前缀,此项为配置时使用缓存名作为前缀
use-key-prefix: true #是否使用指定前缀
- 编写配置类(其它类型缓存同理)
@EnableCaching
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
public class MyRedisCacheConfiguration {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//使用json序列化
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
- 添加注解
@Cacheable
@Cacheable(cacheNames = {“categoryFirstLever”}, key = “#root.methodName” )中key未普通字符串时需要添加单引号,
如@Cacheable(cacheNames = {“categoryFirstLever”}, key = “‘getFirstLevel’” )
@Cacheable(cacheNames = {"categoryFirstLever"}, key = "#root.methodName" )
public List<CategoryEntity> getFirstLevel() {
...
}
use-key-prefix: true,设置key-prefix
use-key-prefix=false
use-key-prefix: true,未设置key-prefix
条件缓存
-
condition
用于使方法缓存具有条件的Spring表达式语言(SpEL)表达式。 默认值为“”,表示方法结果始终缓存。 SpEL表达式根据提供以下元数据的专用上下文进行计算: #root.method、#root.target和#root.caches分别用于对方法、目标对象和受影响缓存的引用。 方法名(#root.methodName)和目标类(#root.targetClass)的快捷方式也可用。 方法参数可以通过索引访问。例如,第二个参数可以通过#root.args[1]、#p1或#a1访问。如果该信息可用,也可以按名称访问参数。
-
unless
用于否决方法缓存的Spring表达式语言(SpEL)表达式。 与
condition
不同,此表达式是在调用方法之后计算的,因此可以引用结果。 默认值为“”,这意味着缓存永远不会被否决。 SpEL表达式根据提供以下元数据的专用上下文进行计算: #result获取方法调用结果的引用。对于支持的包装器(如Optional),#result指的是实际对象,方法的返回结果,而不是包装器。 #root.method、#root.target和#root.caches分别用于对方法、目标对象和受影响缓存的引用。 方法名(#root.methodName)和目标类(#root.targetClass)的快捷方式也可用。 方法参数可以通过索引访问。例如,第二个参数可以通过#root.args[1]、#p1或#a1访问。如果该信息可用,也可以按名称访问参数。eg:当方法返回值为空时不进行缓存
@Cacheable(cacheNames = {"check"}, key = "'check:'+#root.methodName+':'+#root.args[0]",unless = "#result==null")
@CacheEvict失效模式
删除缓存
@Override
@CacheEvict(cacheNames = "category", key = "'getFirstLevel'")
public void updateCascade(CategoryEntity category) {
...
}
删除缓存分区的所有数据:
@CacheEvict(cacheNames = "category",allEntries = true)
public void updateCascade(CategoryEntity category) {
// 更新本表数据
updateById(category);
// 更新 CategoryBrandRelation 表数据
LambdaUpdateWrapper<CategoryBrandRelationEntity> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(CategoryBrandRelationEntity::getCatelogName, category.getName())
.eq(CategoryBrandRelationEntity::getCatelogId, category.getCatId());
categoryBrandRelationService.update(wrapper);
// TODO: 2022/11/28 更新其它冗余表数据
}
@Caching
批量操作:批量删除缓存
@Caching(evict = {
@CacheEvict(cacheNames = "category", key = "'getFirstLevel'"),
@CacheEvict(cacheNames = "category", key = "'getCatalogJson'"),
})
public void updateCascade(CategoryEntity category) {
...
}
@CachePut双写模式
将修改后的结果写入缓存
约定
- 同一类型的数据使用相同缓存分区,利于批量删除
Spring-Cache的不足之处:
1)、读模式 缓存穿透:查询一个null数据。解决方案:缓存空数据 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间 2)、写模式:(缓存与数据库一致) 1)、读写加锁。 2)、引入Canal,感知到MySQL的更新去更新Redis 3)、读多写多,直接去数据库查询就行
总结: 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了) 特殊数据:特殊设计
原理: CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
Idea
批量启动服务
限制内存
-Xmx100m
代码调试
条件断点
条件筛选启用断点: 当beanName包含“AServiceImpl”时断点才生效
对象视图
Gateway
跨域
@Configuration
public class MyCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
configSource.registerCorsConfiguration("/**", config);
return new CorsWebFilter(configSource);
}
}
防止硬编码
枚举的使用
package com.pika.gstore.common.constant;
import lombok.Getter;
/**
* Desc:
*
* @author pikachu
* @since 2022/12/5 0:14
*/
public class WareConstant {
@Getter
public enum PurchaseEnum {
/**
* 基本属性
*/
CREATED(0, "新建"),
ASSIGNED(1, "已分配"),
GOT(2, "已领取"),
FINISHED(3, "已完成"),
ERROR(4, "有异常"),
;
private final int code;
private final String desc;
PurchaseEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
}
@Getter
public enum PurchaseDetailEnum {
/**
* 基本属性
*/
CREATED(0, "新建"),
ASSIGNED(1, "已分配"),
BUYING(2, "正在采购"),
FINISHED(3, "已完成"),
ERROR(4, "采购失败"),
;
private final int code;
private final String desc;
PurchaseDetailEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
}
}
MyBatis-Plus表查询
尽可能使用LambdaQueryWrapper
避免手动拼接表名
public void updateBySpuId(String spuId, List<ProductAttrValueEntity> attrValueEntities) {
LambdaQueryWrapper<ProductAttrValueEntity> wrapper = new LambdaQueryWrapper<>();
List<ProductAttrValueEntity> collect = attrValueEntities.stream().peek(item -> {
wrapper.or(w ->
w.eq(ProductAttrValueEntity::getSpuId, spuId)
.eq(ProductAttrValueEntity::getAttrId, item.getAttrId())
);
item.setSpuId(Long.valueOf(spuId));
}).collect(Collectors.toList());
remove(wrapper);
saveBatch(collect);
}
Log日志打印
打印sql语句
设置日志打印级别,具体到程序包:
logging:
level:
com.pika.gstore: debug
打印异常
对 ‘printStackTrace()’ 的调用可能应当替换为更可靠的日志
e.printStackTrace();
log.error(e.getMessage(), e);
linux下监听日志文件
tail -n 50 -f flume.log
回车,留出空白便于观察
Linux nohup 命令
nohup 英文全称 no hang up(不挂起),用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行。
nohup 命令,在默认情况下(非重定向时),会输出一个名叫 nohup.out 的文件到当前目录下,如果当前目录的 nohup.out 文件不可写,输出重定向到 $HOME/nohup.out 文件中。
使用权限
所有使用者
语法格式
nohup Command [ Arg … ] [ & ]
参数说明:
Command:要执行的命令。
Arg:一些参数,可以指定输出文件。
&:让命令在后台执行,终端退出后命令仍旧执行。
实例
以下命令在后台执行 root 目录下的 runoob.sh 脚本:
nohup /root/runoob.sh &
在终端如果看到以下输出说明运行成功:
appending output to nohup.out
这时我们打开 root 目录 可以看到生成了 nohup.out 文件。
如果要停止运行,你需要使用以下命令查找到 nohup 运行脚本到 PID,然后使用 kill 命令来删除:
ps -aux | grep "runoob.sh"
参数说明:
- a : 显示所有程序
- u : 以用户为主的格式来显示
- x : 显示所有程序,不区分终端机
另外也可以使用 **ps -def | grep “runoob.sh**” 命令来查找。 |
找到 PID 后,就可以使用 kill PID 来删除。
kill -9 进程号PID
以下命令在后台执行 root 目录下的 runoob.sh 脚本,并重定向输入到 runoob.log 文件:
nohup /root/runoob.sh > runoob.log 2>&1 &
2>&1 解释:
将标准错误 2 重定向到标准输出 &1 ,标准输出 &1 再被重定向输入到 runoob.log 文件中。
- 0 – stdin (standard input,标准输入)
- 1 – stdout (standard output,标准输出)
- 2 – stderr (standard error,标准错误输出)
反射
Java中TypeReference用法说明
表示泛型类型T。Java还没有提供表示泛型类型的方法,所以这个类提供了。强制客户端创建此类的子类,即使在运行时也可以检索类型信息。
用途
在使用fastJson时,对于泛型的反序列化很多场景下都会使用到TypeReference,例如:
public <T> T getData(TypeReference<T> typeReference) {
Object data = this.get("data");
String jsonStr = JSONUtil.toJsonStr(data);
return JSONUtil.toBean(jsonStr, typeReference, false);
}
public R setData(Object data) {
this.put("data", data);
return this;
}
@Test
public void test4(){
R r = new R();
r.setData(Arrays.asList(1,2,3,4,5));
List<String> data = r.getData(new TypeReference<List<String>>() {
});
System.out.println(data);
}
输出:
[1, 2, 3, 4, 5]
使用TypeReference可以明确的指定反序列化的类型,具体实现逻辑参考TypeReference的构造函数
protected TypeReference(){
Type superClass = getClass().getGenericSuperclass();
Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
Type cachedType = classTypeCache.get(type);
if (cachedType == null) {
classTypeCache.putIfAbsent(type, type);
cachedType = classTypeCache.get(type);
}
this.type = cachedType;
}
解说
其中核心的方法是:getActualTypeArguments,它可以得到父类的泛型类型
ParameterizedType是一个记录类型泛型的接口, 继承自Type,一共三方法:
- Type[] getActualTypeArguments(); //返回泛型类型数组
- Type getRawType(); //返回原始类型Type
- Type getOwnerType(); //返回 Type 对象,表示此类型是其成员之一的类型。
例如 Map<String,String>
对应的 ParameterizedType 三个方法分别取值如下:
- [class java.lang.String, class java.lang.String]
- interface java.util.Map
- null
例证
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
public class TypeReferencBaseLearn {
public static class IntMap extends HashMap<String, Integer> {}
void test1() {
IntMap intMap = new IntMap();
System.out.println("getSuperclass:" + intMap.getClass().getSuperclass());
System.out.println("getGenericSuperclass:" + intMap.getClass().getGenericSuperclass());
Type type = intMap.getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType)type;
for (Type t : p.getActualTypeArguments()) {
System.out.println(t);
}
}
}
/*
getSuperclass:class java.util.HashMap
getGenericSuperclass:java.util.HashMap<java.lang.String, java.lang.Integer>
class java.lang.String
class java.lang.Integer
*/
void test2() {
Map<String, Integer> intMap = new HashMap<>();
System.out.println("\ngetSuperclass:" + intMap.getClass().getSuperclass());
System.out.println("getGenericSuperclass:" + intMap.getClass().getGenericSuperclass());
Type type = intMap.getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType)type;
for (Type t : p.getActualTypeArguments()) {
System.out.println(t);
}
}
}
/*
getSuperclass:class java.util.AbstractMap
getGenericSuperclass:java.util.AbstractMap<K, V>
K
V
*/
void test3() {
Map<String, Integer> intMap = new HashMap<String, Integer>(){};
System.out.println("\ngetSuperclass:" + intMap.getClass().getSuperclass());
System.out.println("getGenericSuperclass:" + intMap.getClass().getGenericSuperclass());
Type type = intMap.getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType)type;
for (Type t : p.getActualTypeArguments()) {
System.out.println(t);
}
}
}
/*
getSuperclass:class java.util.HashMap
getGenericSuperclass:java.util.HashMap<java.lang.String, java.lang.Integer>
class java.lang.String
class java.lang.Integer
*/
public static void main(String[] args) {
TypeReferencBaseLearn obj = new TypeReferencBaseLearn();
obj.test1();
obj.test2();
obj.test3();
}
}
关于public T method(T t)函数的说明
public <T> T method(T t){
// CODE
return t;
}
上面的代码,在public和method之间有两个部分
其中
正确实例:
public <T> int method1(T t){
// CODE
return 1;
}
上面的函数,
正确实例:
public <K,V> Map<K,V> test5(K key,V value){
HashMap<K, V> kvHashMap = new HashMap<>();
kvHashMap.put(key, value);
return kvHashMap;
}
相反的,在使用自定义的范型T之前,如果不首先使用
[错误示例]-(没有声明范型变量类型T)
public T method(T t){
// CODE
return t;
}
[错误示例]-(没有声明范型变量类型T)
public int method(T t){
// CODE
return 1;
}
SMS短信发送
以阿里云为例,官方api为准
导入依赖
<!-- 短信验证码 -->
<!-- 异步-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibabacloud-dysmsapi20170525</artifactId>
<version>2.0.22</version>
</dependency>
<!--同步-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.23</version>
</dependency>
抽取工具类
@Configuration
@ConfigurationProperties(prefix = "alibaba.cloud.sms")
@Data
public class SmsConfig {
private String accessKeyId;
private String accessKeySecret;
/**
* 异步短信发送客户端
*/
@Bean
public AsyncClient asyncClient() {
// Configure Credentials authentication information, including ak, secret, token
StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
.accessKeyId(accessKeyId)
.accessKeySecret(accessKeySecret)
//.securityToken("<your-token>") // use STS token
.build());
// Configure the Client
AsyncClient client = AsyncClient.builder()
.region("cn-hangzhou") // Region ID
//.httpClient(httpClient) // Use the configured HttpClient, otherwise use the default HttpClient (Apache HttpClient)
.credentialsProvider(provider)
//.serviceConfiguration(Configuration.create()) // Service-level configuration
// Client-level configuration rewrite, can set Endpoint, Http request parameters, etc.
.overrideConfiguration(
ClientOverrideConfiguration.create()
.setEndpointOverride("dysmsapi.aliyuncs.com")
//.setConnectTimeout(Duration.ofSeconds(30))
).build();
return client;
}
/**
* 同步短信发送客户端
*/
@Bean
public Client createClient() throws Exception {
Config config = new Config()
// 必填,您的 AccessKey ID
.setAccessKeyId(accessKeyId)
// 必填,您的 AccessKey Secret
.setAccessKeySecret(accessKeySecret);
// 访问的域名
config.endpoint = "dysmsapi.aliyuncs.com";
return new Client(config);
}
}
需要在yaml文件中配置sms发送的授权accessKeyId,accessKeySecret
异步发送,同步发送:
@GetMapping("sendAsync")
public R sendAsync(@RequestParam("phone") String phone, @RequestParam("code") String code) {
if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {
return R.error(BaseException.MISS_ERROR.getCode(),
BaseException.MISS_ERROR.getMsg());
}
try {
// Parameter settings for API request
SendSmsRequest sendSmsRequest = SendSmsRequest.builder()
.signName("阿里云短信测试")
.templateCode("SMS_154950909")
.phoneNumbers(phone)
.templateParam("{\"code\":\"" + code + "\"}")
// Request-level configuration rewrite, can set Http request parameters, etc.
// .requestConfiguration(RequestConfiguration.create().setHttpHeaders(new HttpHeaders()))
.build();
// Asynchronously get the return value of the API request
CompletableFuture<SendSmsResponse> response = asyncClient.sendSms(sendSmsRequest);
// Synchronously get the return value of the API request
SendSmsResponseBody body = response.get().getBody();
boolean resultCode = "OK".equalsIgnoreCase(body.getCode());
return resultCode ? R.ok() : R.ok(body.getMessage()).put("code", BaseException.OTHER_ERROR.getCode());
} catch (ExecutionException | InterruptedException e) {
log.error(e.getMessage());
return R.error();
} finally {
// Finally, close the client
asyncClient.close();
}
}
@GetMapping("send")
public R send(@RequestParam("phone") String phone, @RequestParam("code") String code) {
if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {
return R.error(BaseException.MISS_ERROR.getCode(),
BaseException.MISS_ERROR.getMsg());
}
// 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html
com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest()
.setSignName("阿里云短信测试")
.setTemplateCode("SMS_154950909")
.setPhoneNumbers(phone)
.setTemplateParam("{\"code\":\"" + code + "\"}");
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
try {
// 复制代码运行请自行打印 API 的返回值
com.aliyun.dysmsapi20170525.models.SendSmsResponse response = client.sendSmsWithOptions(sendSmsRequest, runtime);
boolean resultCode = "OK".equalsIgnoreCase(response.body.getCode());
return resultCode ? R.ok() : R.ok(response.body.getMessage()).put("code", BaseException.OTHER_ERROR.getCode());
} catch (Exception e) {
log.error(e.getMessage());
}
return R.error(BaseException.OTHER_ERROR.getCode(), BaseException.OTHER_ERROR.getMsg());
}
SpringSession
使用
依赖:
<!-- spring-session共享 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置:
spring.session.store-type=redis # Session store type.
server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds is used.
spring.session.redis.flush-mode=on_save # Sessions flush mode.
spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.
spring.redis.host=localhost # Redis server host.
spring.redis.password= # Login password of the redis server.
spring.redis.port=6379 # Redis server port.
@EnableRedisHttpSession
public class SpringSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("PIKACHU");
//子域共享session
serializer.setDomainName("gulimall.com");
return serializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return RedisSerializer.json();
}
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory();
}
}
原理
Cookie和Session的区别
一、共同之处: cookie和session都是用来跟踪浏览器用户身份的会话方式。
二、工作原理: 1.Cookie的工作原理 (1)浏览器端第一次发送请求到服务器端 (2)服务器端创建Cookie,该Cookie中包含用户的信息,然后将该Cookie发送到浏览器端 (3)浏览器端再次访问服务器端时会携带服务器端创建的Cookie (4)服务器端通过Cookie中携带的数据区分不同的用户 2.Session的工作原理 (1)浏览器端第一次发送请求到服务器端,服务器端创建一个Session,同时会创建一个特殊的Cookie(name为JSESSIONID的固定值,value为session对象的ID),然后将该Cookie发送至浏览器端 (2)浏览器端发送第N(N>1)次请求到服务器端,浏览器端访问服务器端时就会携带该name为JSESSIONID的Cookie对象 (3)服务器端根据name为JSESSIONID的Cookie的value(sessionId),去查询Session对象,从而区分不同用户。 name为JSESSIONID的Cookie不存在(关闭或更换浏览器),返回1中重新去创建Session与特殊的Cookie name为JSESSIONID的Cookie存在,根据value中的SessionId去寻找session对象 value为SessionId不存在(Session对象默认存活30分钟),返回1中重新去创建Session与特殊的Cookie value为SessionId存在,返回session对象 Session的工作原理图 三、区别:
cookie数据保存在客户端,session数据保存在服务端。
session 简单的说,当你登陆一个网站的时候,如果web服务器端使用的是session,那么所有的数据都保存在服务器上,客户端每次请求服务器的时候会发送当前会话sessionid,服务器根据当前sessionid判断相应的用户数据标志,以确定用户是否登陆或具有某种权限。由于数据是存储在服务器上面,所以你不能伪造。
cookie sessionid是服务器和客户端连接时候随机分配的,如果浏览器使用的是cookie,那么所有数据都保存在浏览器端,比如你登陆以后,服务器设置了cookie用户名,那么当你再次请求服务器的时候,浏览器会将用户名一块发送给服务器,这些变量有一定的特殊标记。服务器会解释为cookie变量,所以只要不关闭浏览器,那么cookie变量一直是有效的,所以能够保证长时间不掉线。
如果你能够截获某个用户的cookie变量,然后伪造一个数据包发送过去,那么服务器还是 认为你是合法的。所以,使用cookie被攻击的可能性比较大。
如果cookie设置了有效值,那么cookie会保存到客户端的硬盘上,下次在访问网站的时候,浏览器先检查有没有cookie,如果有的话,读取cookie,然后发送给服务器。
所以你在机器上面保存了某个论坛cookie,有效期是一年,如果有人入侵你的机器,将你的cookie拷走,放在他机器下面,那么他登陆该网站的时候就是用你的身份登陆的。当然,伪造的时候需要注意,直接copy cookie文件到 cookie目录,浏览器是不认的,他有一个index.dat文件,存储了 cookie文件的建立时间,以及是否有修改,所以你必须先要有该网站的 cookie文件,并且要从保证时间上骗过浏览器
两个都可以用来存私密的东西,session过期与否,取决于服务器的设定。cookie过期与否,可以在cookie生成的时候设置进去。
四、区别对比: (1)cookie数据存放在客户的浏览器上,session数据放在服务器上 (2)cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session (3)session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE (4)单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。 (5)所以:将登陆信息等重要信息存放为SESSION;其他信息如果需要保留,可以放在COOKIE中
OAuth2
OAuth2 认证基本流程
OAuth2 获取 AccessToken 认证步骤
1. 授权码模式
- 应用通过 浏览器 或 Webview 将用户引导到码云三方认证页面上( GET请求 )
https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code
- 用户对应用进行授权
注意: 如果之前已经授权过的需要跳过授权页面,需要在上面第一步的 URL 加上 scope 参数,且 scope 的值需要和用户上次授权的勾选的一致。如用户在上次授权了user_info、projects以及pull_requests。则步骤A 中 GET 请求应为:
https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope=user_info%20projects%20pull_requests
- 码云认证服务器通过回调地址{redirect_uri}将 用户授权码 传递给 应用服务器 或者直接在 Webview 中跳转到携带 用户授权码的回调地址上,Webview 直接获取code即可({redirect_uri}?code=abc&state=xyz)
- 应用服务器 或 Webview 使用 access_token API 向 码云认证服务器发送post请求传入 用户授权码 以及 回调地址( POST请求 )注:请求过程建议将 client_secret 放在 Body 中传值,以保证数据安全。
https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret}
- 码云认证服务器返回 access_token 应用通过 access_token 访问 Open API 使用用户数据。
- 当 access_token 过期后(有效期为一天),你可以通过以下 refresh_token 方式重新获取 access_token( POST请求 )
https://gitee.com/oauth/token?grant_type=refresh_token&refresh_token={refresh_token}
- 注意:如果获取 access_token 返回 403,可能是没有设置User-Agent的原因。 详见:获取Token时服务端响应状态403是什么情况
2. 密码模式
- 用户向客户端提供邮箱地址和密码。客户端将邮箱地址和密码发给码云认证服务器,并向码云认证服务器请求令牌。( POST请求。Content-Type: application/x-www-form-urlencoded )
curl -X POST --data-urlencode "grant_type=password" --data-urlencode "username={email}" --data-urlencode "password={password}" --data-urlencode "client_id={client_id}" --data-urlencode "client_secret={client_secret}" --data-urlencode "scope=projects user_info issues notes" https://gitee.com/oauth/token
scope表示权限范围,有以下选项,请求时使用空格隔开user_info projects pull_requests issues notes keys hook groups gists enterprises
- 码云认证服务器返回 access_token 应用通过 access_token 访问 Open API 使用用户数据。
创建应用流程
- 在 修改资料 -> 第三方应用,创建要接入码云的应用。
- 填写应用相关信息,勾选应用所需要的权限。其中: 回调地址是用户授权后,码云回调到应用,并且回传授权码的地址。
- 创建成功后,会生成 Cliend ID 和 Client Secret。他们将会在上述OAuth2 认证基本流程用到。
单点登录(SSO)
单点登录英文全称Single Sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。
如图所示,图中有4个系统,分别是Application1、Application2、Application3、和SSO。Application1、Application2、Application3没有登录模块,而SSO只有登录模块,没有其他的业务模块,当Application1、Application2、Application3需要登录时,将跳到SSO系统,SSO系统完成登录,其他的应用系统也就随之登录了。这完全符合我们对单点登录(SSO)的定义。
技术实现
在说单点登录(SSO)的技术实现之前,我们先说一说普通的登录认证机制。
如上图所示,我们在浏览器(Browser)中访问一个应用,这个应用需要登录,我们填写完用户名和密码后,完成登录认证。这时,我们在这个用户的session中标记登录状态为yes(已登录),同时在浏览器(Browser)中写入Cookie,这个Cookie是这个用户的唯一标识。下次我们再访问这个应用的时候,请求中会带上这个Cookie,服务端会根据这个Cookie找到对应的session,通过session来判断这个用户是否登录。如果不做特殊配置,这个Cookie的名字叫做jsessionid,值在服务端(server)是唯一的。
同域下的单点登录
一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。比如我们有个域名叫做:a.com,同时有两个业务系统分别为:app1.a.com和app2.a.com。我们要做单点登录(SSO),需要一个登录系统,叫做:sso.a.com。
我们只要在sso.a.com登录,app1.a.com和app2.a.com就也登录了。通过上面的登陆认证机制,我们可以知道,在sso.a.com中登录了,其实是在sso.a.com的服务端的session中记录了登录状态,同时在浏览器端(Browser)的sso.a.com下写入了Cookie。那么我们怎么才能让app1.a.com和app2.a.com登录呢?这里有两个问题:
- Cookie是不能跨域的,我们Cookie的domain属性是sso.a.com,在给app1.a.com和app2.a.com发送请求是带不上的。
- sso、app1和app2是不同的应用,它们的session存在自己的应用内,是不共享的。
那么我们如何解决这两个问题呢?针对第一个问题,sso登录以后,可以将Cookie的域设置为顶域,即.a.com,这样所有子域的系统都可以访问到顶域的Cookie。我们在设置Cookie时,只能设置顶域和自己的域,不能设置其他的域。比如:我们不能在自己的系统中给baidu.com的域设置Cookie。
Cookie的问题解决了,我们再来看看session的问题。我们在sso系统登录了,这时再访问app1,Cookie也带到了app1的服务端(Server),app1的服务端怎么找到这个Cookie对应的Session呢?这里就要把3个系统的Session共享,如图所示。共享Session的解决方案有很多,例如:Spring-Session。这样第2个问题也解决了。
同域下的单点登录就实现了,但这还不是真正的单点登录。
不同域下的单点登录
同域下的单点登录是巧用了Cookie顶域的特性。如果是不同域呢?不同域之间Cookie是不共享的,怎么办?
这里我们就要说一说CAS流程了,这个流程是单点登录的标准流程。
上图是CAS官网上的标准流程,具体流程如下:
- 用户访问app系统,app系统是需要登录的,但用户现在没有登录。
- 跳转到CAS server,即SSO登录系统,以后图中的CAS Server我们统一叫做SSO系统。 SSO系统也没有登录,弹出用户登录页。
- 用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器(Browser)中写入SSO域下的Cookie。
- SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app系统,同时将ST作为参数传递给app系统。
- app系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
- 验证通过后,app系统将登录状态写入session并设置app域下的Cookie。
至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。
- 用户访问app2系统,app2系统没有登录,跳转到SSO。
- 由于SSO已经登录了,不需要重新登录认证。
- SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
- app2拿到ST,后台访问SSO,验证ST是否有效。
- 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
这样,app2系统不需要走登录流程,就已经是登录了。SSO,app和app2在不同的域,它们之间的session不共享也是没问题的。
有的同学问我,SSO系统登录后,跳回原业务系统时,带了个参数ST,业务系统还要拿ST再次访问SSO进行验证,觉得这个步骤有点多余。他想SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?
其实这样问题时很严重的,如果我在SSO没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?这是很可怕的。
总结
单点登录(SSO)的所有流程都介绍完了,原理大家都清楚了。总结一下单点登录要做的事情:
- 单点登录(SSO系统)是保障各业务系统的用户资源的安全 。
- 各个业务系统获得的信息是,这个用户能不能访问我的资源。
- 单点登录,资源都在各个业务系统这边,不在SSO那一方。 用户在给SSO服务器提供了用户名密码后,作为业务系统并不知道这件事。 SSO随便给业务系统一个ST,那么业务系统是不能确定这个ST是用户伪造的,还是真的有效,所以要拿着这个ST去SSO服务器再问一下,这个用户给我的ST是否有效,是有效的我才能让这个用户访问。
解决方案
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。
https://gitee.com/dromara/sa-token
支付
支付宝
sdk
<!-- 支付宝 -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.35.37.ALL</version>
</dependency>
config
@ConfigurationProperties(prefix = "alipay")
@Configuration
@Data
public class AlipayTemplate {
/**
* 在支付宝创建的应用的id
*/
private String appId;
/**
* 商户私钥,您的PKCS8格式RSA2私钥
*/
private String merchantPrivateKey;
/**
* 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
*/
private String alipayPublicKey;
/**
* 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
* 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
*/
private String notifyUrl;
/**
* 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
* 同步通知,支付成功,一般跳转到成功页
*/
private String returnUrl;
/**
* 签名方式
*/
private String signType = "RSA2";
/**
* 字符编码格式
*/
private String charset = "utf-8";
/**
* 支付宝网关; https://openapi.alipaydev.com/gateway.do
*/
private String gatewayUrl;
/**
* 支付超时关单
*/
private String timeoutExpress = "3m";
public String pay(PayVo vo) throws AlipayApiException {
System.out.println("app_id = " + appId);
//1、根据支付宝的配置生成一个支付客户端
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl, appId, merchantPrivateKey, "json", charset, alipayPublicKey, signType);
//2、创建一个支付请求 //设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(returnUrl);
alipayRequest.setNotifyUrl(notifyUrl);
//商户订单号,商户网站订单系统中唯一订单号,必填
String outTradeNo = vo.getOut_trade_no();
//付款金额,必填
String totalAmount = vo.getTotal_amount();
//订单名称,必填
String subject = vo.getSubject();
//商品描述,可空
String body = vo.getBody();
HashMap<String, String> map = new HashMap<>();
map.put("out_trade_no", outTradeNo);
map.put("total_amount", totalAmount);
map.put("subject", subject);
map.put("body", body);
map.put("product_code", "FAST_INSTANT_TRADE_PAY");
map.put("timeout_express", timeoutExpress);
alipayRequest.setBizContent(JSON.toJSONString(map));
String result = alipayClient.pageExecute(alipayRequest).getBody();
//会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
// System.out.println("支付宝的响应:" + result);
return result;
}
}
支付成功异步通知验证签名:
private boolean isSignVerified(HttpServletRequest request) throws AlipayApiException {
//获取支付宝POST过来反馈信息
Map<String, String> params = new HashMap<>();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
//乱码解决,这段代码在出现乱码时使用。如果mysign和sign不相等也可以使用这段代码转化
// valueStr = new String(valueStr.getBytes(StandardCharsets.ISO_8859_1), "gbk");
params.put(name, valueStr);
}
//调用SDK验证签名
return AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipayPublicKey(), alipayTemplate.getCharset(), alipayTemplate.getSignType());
}
@Transactional事务处理
@Transactional 事务注解一般添加再业务逻辑层( Service ),通过 AOP 切面拦截方式进行事务控制,因此调用需要事务支持的方式时应当通过代理对象调用。
Mybatis-Plus 中,Service 默认实现类 com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
中部分方法已添加
@Transactional
注解。
Spring 中开启事务支持
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
}
@Transactional
注解用于事务支持,可标注与类或方法。如:Service、Controller。
注意:
- 不要再切面类的方法中添加改注解,这样做事务并不会生效。
错误的注解使用
@Aspect
@Component
@Slf4j
public class Aspect {
@Pointcut("execution()")
public void pointcut() {
}
@Around("pointcut()")
// @Transactional
// 事务注解加在切面无效
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
}
}
- 不能误以为标注了
@Transactional
注解接口发生异常后会回滚(需要考虑隔离级别、传播行为、作用范围等)。
保存 Task 的 Service
@Service
public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
implements TaskService{
@Override
@Transactional
public boolean saveTask(Task task) {
return save(task);
}
}
Controller 调用后发生异常, saveTask 没有发生异常时事务依然会提交。
@RestController
@RequestMapping("tx")
public class TxTestController {
@Resource
private TaskService taskService;
@GetMapping("save")
public R save(Task task) {
taskService.saveTask(task);
throwException("控制器方法异常。。。");
return R.ok().setData(task);
}
private void throwException(String msg) {
throw new RuntimeException(msg);
}
}
在 @Transactional
事务下读取未提交数据
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
异常处理策略
不回滚事务
try{
}catch(Exception e){
// 不再向上抛出会导致事务回滚的异常
}
回滚事务
try{
}catch(Exception e){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
防止事务失效
调用本类方法时需要通过代理对象调用。
- 使用 AopContext 获取当前代理对象
- 使用 ApplicationContext 容器中进行获取
ApplicationContext 工具类
@Getter
@Component
public class ContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
public static T getBean(Class clazz) {
return context.getBean(clazz);
}
public static Object getBean(String name) {
return context.getBean(name);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
}
</code></pre>
</details>
获取当前对象
@Override
@Transactional
public void methodA() {
// methodA 中调用本类的 methodB ,2 个方法均需要事务支持
ContextUtils.getBean(this.getClass()).methodB();
}
@Override
@Transactional
public void methodB() {
//todo ...
}
# What‘s your problem
## nacos
背景:引入nacos注册,配置中心
加入日志配置出错:
```yaml
spring:
cloud:
nacos:
server-addr: localhost:9948
logging:
level:
com.pika.gstore: debug
```
```java
2022-12-06 00:29:46.503 ERROR 16212 --- [t.remote.worker] c.a.n.c.remote.client.grpc.GrpcClient : Server check fail, please check server localhost ,port 9848 is available , error ={}
java.util.concurrent.TimeoutException: Waited 3000 milliseconds (plus 13 milliseconds, 78400 nanoseconds delay) for com.alibaba.nacos.shaded.io.grpc.stub.ClientCalls$GrpcFuture@1ae23cc0[status=PENDING, info=[GrpcFuture{clientCall={delegate={delegate=ClientCallImpl{method=MethodDescriptor{fullMethodName=Request/request, type=UNARY, idempotent=false, safe=false, sampledToLocalTracing=true,
...
```
解决: 将application.yaml改为bootstrap.yaml,或者将nacos的配置移入bootstrap.yaml
## Java
### Stream
java.lang.IllegalStateException: Duplicate key异常解决
使用场景:
在实际应用开发中,会常把一个List的查询数据集合转为一个Map,那么在这里的 list.[stream](https://so.csdn.net/so/search?q=stream&spm=1001.2101.3001.7020)().collect()其实就是做了这么一件事情,它是java8的stream方式实现的它是以type为key,以entity对象为value构成Map。
```java
//查询
List list = questionCategoryService.selectList(entityWrapper);
Map<String, String> categoryMap = list.stream().collect(
Collectors.toMap(
QuestionCategoryEntity::getCategoryCode,
QuestionCategoryEntity::getCategoryName
)
);
```
在有些业务场景中会出现如下异常:Duplicate key ,map的key重复,如上的 QuestionCategoryEntity::getCategoryCode。
```java
java.lang.IllegalStateException: Duplicate key 专项考试
at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
at java.util.HashMap.merge(HashMap.java:1245)
at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
... ...
```
解决方法:
使用toMap()的重载方法,如果已经存在则不再修改,直接使用上一个数据。
```java
//查询
List list = questionCategoryService.selectList(entityWrapper);
Map<String, String> categoryMap = list.stream().collect(
Collectors.toMap(
QuestionCategoryEntity::getCategoryCode,
QuestionCategoryEntity::getCategoryName,
(entity1, entity2) -> entity1
)
);
```
等效于
```java
questionCategoryService.selectList(entityWrapper);
Map<String, String> categoryMap = list.stream().collect(
Collectors.toMap(
QuestionCategoryEntity::getCategoryCode,
QuestionCategoryEntity::getCategoryName,
(entity1, entity2) {
return entity1
}
)
);
```
(entity1, entity2) -> entity1 这里使用的箭头函数,也就是说当出现了重复key的数据时,会回调这个方法,可以在这个方法里处理重复Key数据问题,这里粗暴点,直接使用了上一个数据。
## SpringCloud
### Feign
feign调用服务超时feign.RetryableException: Read timed out,需要适当延长远程调用等待时间
添加yml配置
```yml
ribbon:
ReadTimeout: 60000
ConnectTimeout: 60000
```
版权声明:如无特别声明,本站收集的文章归 HuaJi66/Others 所有。 如有侵权,请联系删除。
联系邮箱: GenshinTimeStamp@outlook.com
本文标题:《 开发日志 》
本文链接:/%E5%BC%80%E5%8F%91%E7%9B%B8%E5%85%B3/%E9%94%A6%E5%9B%8A/%E5%BC%80%E5%8F%91%E6%97%A5%E5%BF%97.html