使用SpringCloud Alibaba结合Springboot3.0 搭建一个demo
Springboot 3 出来也有一段时间了，Springcloud alibaba也在适配当中，本demo使用Springboot 3 + Spingcloud alibaba + nacos等组件，完成一个项目工程的简单搭建组件Nacos : 2.0.3Maven：3.0.8依赖版本springboot :3.0.4springcloud:2022.0.1springcloud alibaba :2.2.10-RC1spring-cloud-starter-alibaba-nacos-discovery:2022.0.0.0-RC1spring-cloud-starter-alibaba-nacos-config：2022.0.0.0-RC1springcloud alibaba 2.2.10-RC1 版本提供的Nacos依赖，经测试无法成功注册，故使用2022.0.0.0-RC1版本项目POM<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.4</version> <relativePath/> </parent> <groupId>com.piemoe</groupId> <artifactId>springboot3</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot3</name> <description>springboot3</description> <properties> <java.version>17</java.version> </properties> ​ <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2022.0.1</version> <type>pom</type> <scope>import</scope> </dependency> ​ <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.10-RC1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> ​ <dependencies> ​ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> ​ <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2022.0.0.0-RC1</version> </dependency> ​ ​ <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2022.0.0.0-RC1</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> ​ ​ </dependencies> ​ <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> ​ </project> ​启动类@SpringBootApplication public class Springboot3Application { ​ public static void main(String[] args) { SpringApplication.run(Springboot3Application.class, args); } ​ }配置文件bootstrap.ymlserver: port: 13000 ​ spring: main: banner-mode: off application: name: demo profiles: active: local ​ rpc: addr: 127.0.0.1:8848 ​ logging: file: name: logs/${spring.application.name}/all.logbootstrap-local.ymlspring: cloud: nacos: discovery: server-addr: ${rpc.addr} namespace: ${spring.profiles.active} config: server-addr: ${rpc.addr} namespace: ${spring.profiles.active} file-extension: ymlNacos配置demo: name: xxxxx项目已提交到github，有兴趣的可以去看下 仓库
前言数据脱敏是一种数据保护技术，它可以对敏感数据进行处理，使其失去原始意义，从而保护数据隐私。通俗地说，就是将原始数据中敏感信息的部分替换或删除，例如：手机号码中的一部分数字替换为星号，或是姓名中的一些字母用其他字母代替等等。数据脱敏可以保护个人隐私，防止个人信息被泄露、滥用，同时也符合现代法律法规对个人信息保护的要求，是网络安全和信息保护中不可或缺的一环。本文将通过使用注解完成对字段的自定义脱敏功能。正文本文将介绍2种脱敏的方式，分别为：注解方式脱敏和lambda方式脱敏注解脱敏定义脱敏注解@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @JacksonAnnotationsInside @JsonSerialize(using = SensitiveSerialize.class) public @interface Dm { /** * 脱敏信息类型 */ DmTypeEnum type(); /** * 前置不需要打码的长度 * 只有 {@link DmTypeEnum} 为 {@link DmTypeEnum#CUSTOMER} 时可用 */ int prefixNoMaskLen() default 0; /** * 后置不需要打码的长度 * 只有 {@link DmTypeEnum} 为 {@link DmTypeEnum#CUSTOMER} 时可用 */ int suffixNoMaskLen() default 0; /** * 用什么打码 * 只有 {@link DmTypeEnum} 为 {@link DmTypeEnum#CUSTOMER} 时可用 */ char symbol() default '*'; }type()方法为可以支持实现的脱敏信息类型，通过使用hutool工具，可以看到它支持以下枚举，故拉取出来形成类型枚举：public enum DmTypeEnum { /** * 用户自定义 */ CUSTOMER, /** * 用户id */ BACK_ID, /** * 中文名 */ CHINESE_NAME, /** * 身份证号 */ ID_CARD, /** * 座机号 */ FIXED_PHONE, /** * 手机号 */ MOBILE_PHONE, /** * 地址 */ ADDRESS, /** * 电子邮件 */ EMAIL, /** * 密码 */ PASSWORD, /** * 中国大陆车牌，包含普通车辆、新能源车辆 */ CAR_LICENSE, /** * 银行卡 */ BANK_CARD }关键语句在@JsonSerialize(using = SensitiveSerialize.class)上，@JsonSerialize 是一个注解，它可以用来指定 Java 对象序列化成 JSON 格式时的序列化规则。具体而言，它可以指定序列化时属性名的命名方式、日期格式化等等。它的作用是使得 Java 对象可以方便地转化为 JSON 格式，这对于 Web 开发、移动应用开发等场景非常有用，故此我们在这里自定义一个实现，并在注解中引用。@NoArgsConstructor @AllArgsConstructor @Slf4j public class SensitiveSerialize<T> extends JsonSerializer<T> implements ContextualSerializer { /** * 脱敏类型 */ private DmTypeEnum sensitiveTypeEnum; /** * 前几位不脱敏 */ private Integer prefixNoMaskLen; /** * 最后几位不脱敏 */ private Integer suffixNoMaskLen; /** * 打码字符 */ private char symbol; @Override public void serialize(final T value, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException { Object desensitized = DmUtils.desensitized(value.toString(), sensitiveTypeEnum, prefixNoMaskLen, suffixNoMaskLen, symbol); if (desensitized instanceof String) { jsonGenerator.writeString(desensitized.toString()); return; } if (desensitized instanceof Number) { jsonGenerator.writeNumber(DmUtils.backId()); return; } log.error("[数据脱敏]未知的信息类型：{}", sensitiveTypeEnum); jsonGenerator.writeString(StrUtil.EMPTY); } @Override public JsonSerializer<?> createContextual(final SerializerProvider serializerProvider, final BeanProperty beanProperty) throws JsonMappingException { if (beanProperty != null) { if ( Objects.equals(beanProperty.getType().getRawClass(), String.class) || Objects.equals(beanProperty.getType().getSuperClass().getRawClass(), Number.class) ) { Dm sensitive = beanProperty.getAnnotation(Dm.class); if (sensitive == null) { sensitive = beanProperty.getContextAnnotation(Dm.class); } if (sensitive != null) { return new SensitiveSerialize<T>(sensitive.type(), sensitive.prefixNoMaskLen(), sensitive.suffixNoMaskLen(), sensitive.symbol()); } } return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } return serializerProvider.findNullValueSerializer(null); } }脱敏工具类（对照hutool脱敏方法）@Slf4j public class DmUtils { /** * 脱敏，使用默认的脱敏策略 * * desensitized("100", DesensitizedUtils.DesensitizedType.USER_ID)) = "0" * desensitized("段正淳", DesensitizedUtils.DesensitizedType.CHINESE_NAME)) = "段**" * desensitized("51343620000320711X", DesensitizedUtils.DesensitizedType.ID_CARD)) = "5***************1X" * desensitized("09157518479", DesensitizedUtils.DesensitizedType.FIXED_PHONE)) = "0915*****79" * desensitized("18049531999", DesensitizedUtils.DesensitizedType.MOBILE_PHONE)) = "180****1999" * desensitized("北京市海淀区马连洼街道289号", DesensitizedUtils.DesensitizedType.ADDRESS)) = "北京市海淀区马********" * desensitized("duandazhi-jack@gmail.com.cn", DesensitizedUtils.DesensitizedType.EMAIL)) = "d*************@gmail.com.cn" * desensitized("1234567890", DesensitizedUtils.DesensitizedType.PASSWORD)) = "**********" * desensitized("苏D40000", DesensitizedUtils.DesensitizedType.CAR_LICENSE)) = "苏D4***0" * desensitized("11011111222233333256", DesensitizedUtils.DesensitizedType.BANK_CARD)) = "1101 **** **** **** 3256" * * * @param message 字符串 * @param desensitizedType 脱敏类型;可以脱敏：用户id、中文名、身份证号、座机号、手机号、地址、电子邮件、密码 * @param startInclude 开始排除 类型为 CUSTOMER 时可用 * @param endExclude 结束排除 类型为 CUSTOMER 时可用 * @param symbol 脱敏字符 默认为'*' 类型为 CUSTOMER 时可用 * @return 脱敏之后的字符串 */ public static Object desensitized(Object message, DmTypeEnum desensitizedType, int startInclude, int endExclude, char symbol) { if (message instanceof Number && StrUtil.isBlank(message.toString())) { return -1L; } if (StrUtil.isBlank(message.toString())) { return StrUtil.EMPTY; } String newStr; switch (desensitizedType) { case CUSTOMER: newStr = customer(String.valueOf(message), startInclude, endExclude, symbol); break; case BACK_ID: log.info("[数据脱敏]后台用户id脱敏，值为：{}", message); return backId(); case CHINESE_NAME: newStr = chineseName(String.valueOf(message)); break; case ID_CARD: newStr = idCardNum(String.valueOf(message), 1, 2); break; case FIXED_PHONE: newStr = fixedPhone(String.valueOf(message)); break; case MOBILE_PHONE: newStr = mobilePhone(String.valueOf(message)); break; case ADDRESS: newStr = address(String.valueOf(message), 8); break; case EMAIL: newStr = email(String.valueOf(message)); break; case PASSWORD: newStr = password(String.valueOf(message)); break; case CAR_LICENSE: newStr = carLicense(String.valueOf(message)); break; case BANK_CARD: newStr = bankCard(String.valueOf(message)); break; default: newStr = ""; } return newStr; } public static String replace(CharSequence str, int startInclude, int endExclude, char symbol) { return CharSequenceUtil.replace(str, startInclude, endExclude, symbol); } public static String customer(String customer, int startInclude, int endExclude, char replacedChar) { if (StrUtil.isBlank(customer)) { return ""; } if (startInclude > customer.length() || endExclude > customer.length() || (startInclude + endExclude) > customer.length()) { log.warn("[信息脱敏]自定义长度不正确"); return replace(customer, startInclude, customer.length() - 2, replacedChar); } return replace(customer, startInclude, endExclude, replacedChar); } /** * 【后台用户id】不对外提供backId * * @return 脱敏后的主键 */ public static Long backId() { return -1L; } /** * 【中文姓名】只显示第一个汉字，其他隐藏为2个星号，比如：李** * * @param fullName 姓名 * @return 脱敏后的姓名 */ public static String chineseName(String fullName) { if (StrUtil.isBlank(fullName)) { return StrUtil.EMPTY; } return StrUtil.hide(fullName, 1, fullName.length()); } /** * 【身份证号】前1位 和后2位 * * @param idCardNum 身份证 * @param front 保留：前面的front位数；从1开始 * @param end 保留：后面的end位数；从1开始 * @return 脱敏后的身份证 */ public static String idCardNum(String idCardNum, int front, int end) { //身份证不能为空 if (StrUtil.isBlank(idCardNum)) { return StrUtil.EMPTY; } //需要截取的长度不能大于身份证号长度 if ((front + end) > idCardNum.length()) { return StrUtil.EMPTY; } //需要截取的不能小于0 if (front < 0 || end < 0) { return StrUtil.EMPTY; } return StrUtil.hide(idCardNum, front, idCardNum.length() - end); } /** * 【固定电话 前四位，后两位 * * @param num 固定电话 * @return 脱敏后的固定电话； */ public static String fixedPhone(String num) { if (StrUtil.isBlank(num)) { return StrUtil.EMPTY; } return StrUtil.hide(num, 4, num.length() - 2); } /** * 【手机号码】前三位，后4位，其他隐藏，比如135****2210 * * @param num 移动电话； * @return 脱敏后的移动电话； */ public static String mobilePhone(String num) { if (StrUtil.isBlank(num)) { return StrUtil.EMPTY; } return StrUtil.hide(num, 3, num.length() - 4); } /** * 【地址】只显示到地区，不显示详细地址，比如：北京市海淀区**** * * @param address 家庭住址 * @param sensitiveSize 敏感信息长度 * @return 脱敏后的家庭地址 */ public static String address(String address, int sensitiveSize) { if (StrUtil.isBlank(address)) { return StrUtil.EMPTY; } int length = address.length(); return StrUtil.hide(address, length - sensitiveSize, length); } /** * 【电子邮箱】邮箱前缀仅显示第一个字母，前缀其他隐藏，用星号代替，@及后面的地址显示，比如：d**@126.com * * @param email 邮箱 * @return 脱敏后的邮箱 */ public static String email(String email) { if (StrUtil.isBlank(email)) { return StrUtil.EMPTY; } int index = StrUtil.indexOf(email, '@'); if (index <= 1) { return email; } return StrUtil.hide(email, 1, index); } /** * 【密码】密码的全部字符都用*代替，比如：****** * * @param password 密码 * @return 脱敏后的密码 */ public static String password(String password) { if (StrUtil.isBlank(password)) { return StrUtil.EMPTY; } return StrUtil.repeat('*', password.length()); } /** * 【中国车牌】车牌中间用*代替 * eg1：null -》 "" * eg1："" -》 "" * eg3：苏D40000 -》 苏D4***0 * eg4：陕A12345D -》 陕A1****D * eg5：京A123 -》 京A123 如果是错误的车牌，不处理 * * @param carLicense 完整的车牌号 * @return 脱敏后的车牌 */ public static String carLicense(String carLicense) { if (StrUtil.isBlank(carLicense)) { return StrUtil.EMPTY; } // 普通车牌 if (carLicense.length() == 7) { carLicense = StrUtil.hide(carLicense, 3, 6); } else if (carLicense.length() == 8) { // 新能源车牌 carLicense = StrUtil.hide(carLicense, 3, 7); } return carLicense; } /** * 银行卡号脱敏 * eg: 1101 **** **** **** 3256 * * @param bankCardNo 银行卡号 * @return 脱敏之后的银行卡号 */ public static String bankCard(String bankCardNo) { if (StrUtil.isBlank(bankCardNo)) { return bankCardNo; } bankCardNo = StrUtil.trim(bankCardNo); if (bankCardNo.length() < 9) { return bankCardNo; } final int length = bankCardNo.length(); final int midLength = length - 8; final StringBuilder buf = new StringBuilder(); buf.append(bankCardNo, 0, 4); for (int i = 0; i < midLength; ++i) { if (i % 4 == 0) { buf.append(CharUtil.SPACE); } buf.append('*'); } buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length); return buf.toString(); } }lambda脱敏注解方式脱敏的好处是可以在变量上更方便的进行脱敏，但是这种方式对数据格式支持不高，一般只支持基本的数据类型，无法对实体内嵌套的数据进行脱敏，故此可以通过lambda方式进行脱敏。首先自定义一个函数public interface DmParam<Parent, Children> extends Serializable { /** * 执行函数 * * @param parameter 参数 * @return 函数执行结果 * @throws Exception 自定义异常 */ Children call(Parent parameter) throws Exception; /** * 执行函数，异常包装为RuntimeException * * @param parameter 参数 * @return 函数执行结果 */ default Children callWithServiceException(Parent parameter) { try { return call(parameter); } catch (Exception e) { throw new ServiceException("信息脱敏失败"); } } }既然通过lambda方式获取字段，那么反射也是必须的public class FieldUtil { /** * 通过lambda获取字段 * * @param func 方法 * @return {@link Field} */ public static <T> Field getField(DmParam<T, ?> func) { FieldMeta fieldMeta = getFieldMeta(func); return ReflectUtil.getField(fieldMeta.getGetInstantiatedClass(), fieldMeta.getPropertyName()); } /** * 通过lambda设置字段名称 * * @param func 方法 * @param val 设置的值 */ public static <T> void setField(Object object, DmParam<T, ?> func, String val) { FieldMeta fieldMeta = getFieldMeta(func); ReflectUtil.setFieldValue(object, fieldMeta.getPropertyName(), val); } /** * 通过lambda获取字段名称 * * @param func 方法 * @return {@link String} */ public static <T> FieldMeta getFieldMeta(DmParam<T, ?> func) { // 反射读取 Method writeReplaceMethod; try { writeReplaceMethod = func.getClass().getDeclaredMethod("writeReplace"); // 设置可访问 writeReplaceMethod.setAccessible(true); SerializedLambda serializedLambda; serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(func); String implMethodName = serializedLambda.getImplMethodName(); String propertyName = methodToProperty(implMethodName); String clazzPath = serializedLambda.getImplClass().replace('/', '.'); Class<String> clazz = ClassUtil.getClass(clazzPath); return new FieldMeta(propertyName, implMethodName, clazz); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { log.error("[FieldUtil]反射获取字段失败", e); throw new ServiceException(e); } } /** * 通过lambda获取字段名称 * * @param func 方法 * @return {@link String} */ public static <T> String getFieldName(DmParam<T, ?> func) { // 反射读取 Method writeReplaceMethod; try { writeReplaceMethod = func.getClass().getDeclaredMethod("writeReplace"); // 设置可访问 writeReplaceMethod.setAccessible(true); SerializedLambda serializedLambda; serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(func); String implMethodName = serializedLambda.getImplMethodName(); return methodToProperty(implMethodName); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { log.error("[FieldUtil]反射获取字段失败", e); throw new ServiceException(e); } } /** * 根据方法名转换为字段 * * @param implMethodName 方法名 * @return {@link String} */ public static String methodToProperty(String implMethodName) { final String defaultIs = "is"; final String defaultGet = "get"; final String defaultSet = "set"; if (implMethodName.startsWith(defaultIs)) { implMethodName = implMethodName.substring(2); } else if (implMethodName.startsWith(defaultGet) || implMethodName.startsWith(defaultSet)) { implMethodName = implMethodName.substring(3); } else { throw new ServiceException("Error parsing property name '" + implMethodName + "'. Didn't start with 'is', 'get' or 'set'."); } if (implMethodName.length() == 1 || (implMethodName.length() > 1 && !Character.isUpperCase(implMethodName.charAt(1)))) { implMethodName = implMethodName.substring(0, 1).toLowerCase(Locale.ENGLISH) + implMethodName.substring(1); } return implMethodName; } }定义脱敏方法：对于单个实体public class DmLambdaWrapper<T> { T val; @SuppressWarnings("rawtype") public DmLambdaWrapper(T val) { this.val = val; } /** * 获取脱敏实例 * * @param val 待脱敏的实体 * @return {@link DmLambdaWrapper<T>} */ public static <T> DmLambdaWrapper<T> getInstance(T val) { return new DmLambdaWrapper<>(val); } /** * 关联单个字段 * * @param field 关联字段 * @return {@link DmLambdaWrapper<R>} */ public <R> DmLambdaWrapper<R> ivkOne(DmParam<T, R> field) { R call = field.callWithServiceException(val); return new DmLambdaWrapper<>(call); } /** * 关联集合 * * @param listMapper 关联集合字段 * @param predicate 脱敏预言 * @return {@link DmListLambdaWrapper} */ public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper, Predicate<C> predicate) { S cs = listMapper.callWithServiceException(val); List<C> collect = cs.stream().filter(predicate).collect(Collectors.toList()); return new DmListLambdaWrapper<>(collect); } /** * 关联集合-无预言（集合内所有对应字段都将脱敏） * * @param listMapper 关联集合字段 * @return {@link DmListLambdaWrapper} */ public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper) { S cs = listMapper.callWithServiceException(val); return new DmListLambdaWrapper<>(cs); } /** * 脱敏字符串 * * @param dmType 脱敏类型 * @param mapper 脱敏字段 * @return {@link DmLambdaWrapper<T>} */ public <R extends String> DmLambdaWrapper<T> dmStr(DmTypeEnum dmType, DmParam<T, R> mapper) { R call = mapper.callWithServiceException(val); if (null != call) { String desensitized = DmUtils.desensitized(call, dmType, 1, 3, '*').toString(); FieldUtil.setField(val, mapper, desensitized); // 反射获取字段名称 } return new DmLambdaWrapper<>(val); } /** * 脱敏多个字段 相同脱敏类型多字段性能要优于 {@link DmLambdaWrapper#dmStr(DmTypeEnum, DmParam)} * * @param dmType 脱敏类型 * @param mappers 脱敏字段 * @return {@link DmLambdaWrapper<T>} * @author Mc * @date 2022/12/16 17:32 */ @SafeVarargs public final <R extends String> DmLambdaWrapper<T> dmStr(DmTypeEnum dmType, final DmParam<T, R>... mappers) { for (DmParam<T, R> mapper : mappers) { R call = mapper.callWithServiceException(val); if (null != call) { String desensitized = DmUtils.desensitized(call, dmType, 1, 3, '*').toString(); FieldUtil.setField(val, mapper, desensitized); // 反射获取字段名称 } } return new DmLambdaWrapper<>(val); } }对于List集合脱敏@Slf4j public class DmListLambdaWrapper<T extends Collection<F>, F> { T listVal; public DmListLambdaWrapper(T listVal) { this.listVal = listVal; } /** * 关联集合 带过滤 * * @param listMapper 集合Mapper * @param predicate 过滤预言 * @return {@link DmListLambdaWrapper} * @author Mc * @date 2022/12/16 17:24 */ public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper, Predicate<C> predicate) { S cs = listMapper.callWithServiceException(listVal); List<C> collect = cs.stream().filter(predicate).collect(Collectors.toList()); return new DmListLambdaWrapper<>(collect); } /** * 挂念集合 不带过滤 * * @param listMapper 集合Mapper * @return {@link DmListLambdaWrapper} * @author Mc * @date 2022/12/16 17:26 */ public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper) { S cs = listMapper.callWithServiceException(listVal); return new DmListLambdaWrapper<>(cs); } /** * 脱敏字符串字段 * * @param dmType 脱敏类型 * @param mapper 脱敏字段 * @return {@link DmListLambdaWrapper} * @author Mc * @date 2022/12/16 17:26 */ public <C extends String> DmListLambdaWrapper<Collection<F>, F> dmList2Str(DmTypeEnum dmType, DmParam<F, C> mapper) { for (F f : listVal) { C message = mapper.callWithServiceException(f); if (null != message) { String desensitized = DmUtils.desensitized(message, dmType, 1, 3, '*').toString(); FieldUtil.setField(f, mapper, desensitized); // 反射获取字段名称 } } return new DmListLambdaWrapper<>(listVal); } /** * 脱敏多个字符串字段，相同类型性能优先于多次使用 {@link DmListLambdaWrapper#dmList2Str(DmTypeEnum, DmParam)} * * @param dmType 脱敏类型 * @param mappers 脱敏字段 * @return {@link DmListLambdaWrapper} * @author Mc * @date 2022/12/16 17:27 */ @SafeVarargs public final <C extends String> DmListLambdaWrapper<Collection<F>, F> dmList2Str(DmTypeEnum dmType, final DmParam<F, C>... mappers) { for (F f : listVal) { for (DmParam<F, C> mapper : mappers) { C message = mapper.callWithServiceException(f); if (null != message) { String desensitized = DmUtils.desensitized(message, dmType, 1, 3, '*').toString(); FieldUtil.setField(f, mapper, desensitized); // 反射获取字段名称 } } } return new DmListLambdaWrapper<>(listVal); } }其他的方式可以自由拓展用法：DmLambdaWrapper.getInstance(user.getUserDetail()) .dmStr(DmTypeEnum.MOBILE_PHONE, UserDetail::getMobile) .dmStr(DmTypeEnum.EMAIL, UserDetail::getEmail);上面的例子将user里的userDetail字段中的mobile、email进行了相对应的数据脱敏。
前言springcloud项目通常会遇到以下2个问题：配置项目redis组件时，有时会遇到一些序列化的问题，如当使用jackson的时，使用@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)注解，会报这个实体不是一个数组的问题集成通用redis依赖组件，可以让所有的项目共同依赖默认一个redis过期时间，更好的利用redis资源正文项目依赖Springboot : 2.6.3Springcloud: 2021.0.1Springcoud Alibaba: 2021.0.1.0redis依赖<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>framework-common</artifactId> <groupId>com.xxx</groupId> <version>2.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> ​ <artifactId>framework-common-redis</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies> </project>配置redis序列@Configuration @AutoConfigureBefore(RedisAutoConfiguration.class) public class BaseRedisConfig { ​ @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(factory); redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); redisTemplate.setValueSerializer(RedisSerializer.json()); redisTemplate.setHashValueSerializer(RedisSerializer.json()); return redisTemplate; } ​ } ​ /** * redis 默认缓存时间 */ @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); //设置Redis缓存有效期为7天 RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() // 使的reids注解缓存的key中 "::"2个冒号转换成":"一个冒号，统一redis key的风格 .computePrefixWith(name -> name + ":") .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(7)); return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration); } }代码解释@AutoConfigureBefore注解是Spring Boot的一个注解，用于指定一个配置类在另外一个配置类之前自动配置，以确保它们的顺序正确。它的作用是控制自动配置的顺序，以便在自动配置时确定配置类的优先级。使用@AutoConfigureBefore注解需要在配置类上加上该注解，并指定目标配置类的全限定名或Class对象。例如，我们可以在自定义的Redis配置类上使用@AutoConfigureBefore注解，指定目标为框架提供的默认Redis配置类，使自定义Redis配置类在框架默认Redis配置类之前进行自动配置。示例代码如下所示：@Configuration @AutoConfigureBefore(RedisAutoConfiguration.class) public class CustomRedisConfiguration { // 自定义 Redis 配置 }在上述代码中，我们在配置类CustomRedisConfiguration上使用@AutoConfigureBefore注解，指定目标为Spring Boot框架提供的默认Redis配置类RedisAutoConfiguration，以确保自定义的Redis配置类在框架默认Redis配置类之前自动配置。总而言之，@AutoConfigureBefore注解可以帮助我们控制自动配置的顺序，确保在自动配置时配置类的优先级正确。RedisSerializer.string()实际为StringRedisSerializer.UTF_8，获取一个简单的 String byte[]（和返回）序列化程序，使用 UTF-8 作为默认值，简单来讲，就是key必须是String类型，这个在使用BoundXXXOperations方法时，若key不为String类型，则会抛出错误。redisTemplate.setValueSerializer(RedisSerializer.json()); redisTemplate.setHashValueSerializer(RedisSerializer.json());以上两句代码指将普通数据类型的value和哈希结构的value值使用json序列化，看下具体内容：static RedisSerializer<Object> json() { return new GenericJackson2JsonRedisSerializer(); }实际上是通过了jackson来进行序列化和逆序列化。使用json序列化的好处是可以使用@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)来序列化bean，这个注解在入库或者缓存后，会加上@class，使得逆序列化更为准确，且可实现多态逆序列化。computePrefixWith(name -> name + ":")可以让使得springboot-data-reids注解缓存的key中的::两个冒号转换成:一个冒号，统一redis key的风格，更好的管理redis键值。xxx::date::1234 转变成了 xxx:data:1234添加spring.factoriesspring.factories 是 Spring 框架中的一个特殊文件，在 Spring Boot 中被广泛使用。它的作用是使开发者可以为自己的应用程序添加自定义配置，例如注册自定义的 Bean 或者自定义的 Starter。具体地说，spring.factories 文件中包含了多个键值对，其中键是某个接口或者类的全限定名，而值是对应该接口或类实现的工厂类的全限定名。当 Spring Boot 启动时，会扫描项目中的所有 spring.factories 文件，并读取其中的配置信息，最终根据这些配置信息初始化并启动Spring 上下文。Spring Boot 中的 Starter 就是通过使用 spring.factories 实现的。我们可以构建一个 Starter（一个所需依赖的组合）并定义其spring.factories 文件来注册自定义的自动配置。这样，当使用该 Starter 的应用程序启动时，Spring Boot就会根据 spring.factories 中的配置信息自动装配所需的 Bean，为提供了开发者非常方便的方式来为自己的应用程序添加自定义配置。在resource下新建META-INF目录，在META-INF目录下新建spring.factories文件，在文件里加入之前自定义的BaseRedisConfig.org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.xxx.common.redis.config.BaseRedisConfig打包maven clean install使用其他项目引入依赖framework-common-redisyml文件配置redis：spring: redis: database: 0 port: 6379 host: localhost password: L1lvlrGZV6Kl0bhS lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间（使用负值表示没有限制） max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0ps：若使用 Spring boot3则pom文件为spring: data: redis: database: 0 port: 6379 host: localhost password: L1lvlrGZV6Kl0bhS lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间（使用负值表示没有限制） max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0
1.服务生产方生产方方法示例@RequestMapping("/member/list") public R memberCoupon(){ CouponEntity couponEntity = new CouponEntity(); couponEntity.setCouponName("满一百减十"); return R.ok().put("coupons", Collections.singletonList(couponEntity)); }2.服务消费方2.1引入openfeign2.1.1 springbootApplication.java开启远程调用@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class GulimallMemberApplication { public static void main(String[] args) { SpringApplication.run(GulimallMemberApplication.class, args); } }2.1.2 创建接口编写一个接口，告诉 springcloud 这个接口需要远程服务@FeignClient("gulimall-coupon") 其中，gulimall-coupon 为被调用的服务名——spring.applicaiton.name@FeignClient("gulimall-coupon") //调用的服务名 public interface CouponFeignService { @RequestMapping("/coupon/coupon/member/list") public R memberCoupon(); }3.结果http://localhost:8000/member/member/coupons
0.优先级配置中心>本地1.pom<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>2.application.ymlspring: application: name: gulimall-coupon cloud: nacos: discovery: server-addr: http://nacos.chaospring.com/3. bootstrap.properties使用 bootstrap.properties 配置文件来配置Nacos Server 地址spring.application.name=gulimall-coupon #80端口不能省略 spring.cloud.nacos.config.server-addr=http://nacos.chaospring.com:804.controller使用 @RefreshScope 注解刷新@RefreshScope使用 @Value 获取配置内容@Value("${coupon.user.name}") private String name; @Value("${coupon.user.age}") private Integer age; @RequestMapping("/test") public R test() { return R.ok().put("name", name).put("age", age); }5.nacos配置若指定了开发环境，则名字为：gulimall-coupon-dev.properties6.环境配置隔离命名空间：配置隔离6.1新增命名空间6.2作用可以利用命名空间做环境隔离bootstrap.properties 下添加配置#命名空间 spring.cloud.nacos.config.namespace=68e1547d-d96c-4794-bbb5-de2061b135917.服务配置隔离7.1每个微服务创建一个命名空间7.2配置集配置集：所有的配置集合DataID:类似文件名配置分组：默认所有的配置集都属于：DEFAULT_GROUPbootstrap.properties 下添加配置spring.cloud.nacos.config.group=dev7.3拆分配置文件的配置集bootstrap.properties 下添加配置spring.cloud.nacos.config.extension-configs[0].data-id=datasource.yml spring.cloud.nacos.config.extension-configs[0].group=dev #是否动态刷新 默认是false spring.cloud.nacos.config.extension-configs[0].refresh=true spring.cloud.nacos.config.extension-configs[1].data-id=mybatis.yml spring.cloud.nacos.config.extension-configs[1].group=dev spring.cloud.nacos.config.extension-configs[1].refresh=true spring.cloud.nacos.config.extension-configs[2].data-id=other.yml spring.cloud.nacos.config.extension-configs[2].group=dev spring.cloud.nacos.config.extension-configs[2].refresh=true.问题nacos配置中心超时问题：java.net.ConnectException: no available server, currentServerAddr单独指定 nacos-client 版本<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.0.RELEASE</version> <type>pom</type> <scope>import</scope> <exclusions> <exclusion> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>1.2.1</version> </dependency>
1.依赖父pom<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.2.6.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR3</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build>子pom <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>2.application.ymlspring: application: name: xxx-coupon cloud: nacos: discovery: #域名末尾要加 / server-addr: http://nacos.chaospring.com/ management: endpoints: web: exposure: include: '*'3.springbootApplication.class@SpringBootApplication @EnableDiscoveryClient //注册注解 public class springbootApplication { public static void main(String[] args) { SpringApplication.run(GulimallCouponApplication.class, args); } }
openFeign是一个声明式的Web 服务客户端，让编写Web服务客户端变得非常容易，只需创建一个借口并在借口上添加注解即可。用于服务调用方1.pom<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>2.application.ymlserver: port: 80 spring: application: name: cloud-consumer-feign-order #设置feign客户端超时时间(OpenFeign默认支持ribbon) ribbon: #指的是建立连接所用的时间，适用于网络状况正常的情况下,两端连接所用的时间 ReadTimeout: 5000 #指的是建立连接后从服务器读取到可用资源所用的时间 ConnectTimeout: 5000 logging: level: # feign日志以什么级别监控哪个接口 org.mc.mycloud.service.PaymentFeignService: debug eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息，默认为true。单节点无所谓，集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: #单机版 # defaultZone: http://localhost:7001/eureka # 集群版 defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #主机名称 服务修改 instance: instance-id: order80 #访问路径可以显示IP地址 prefer-ip-address: true # #Eureka客户端向服务端发送心跳的时间间隔，单位为秒(默认是30秒) # #lease-renewal-interval-in-seconds: 1 # #Eureka服务端在收到最后一次心跳后等待时间上限，单位为秒(默认是90秒)，超时将剔除服务 # #lease-expiration-duration-in-seconds: 23.主启动@SpringBootApplication @EnableFeignClients //说明是feign 客户端 public class FeignOrderMain80 { public static void main(String[] args) { SpringApplication.run(FeignOrderMain80.class, args); } }4.service接口@Component //作为feign组件的接口 以及将要调用的服务名称 @FeignClient(value = "CLOUD-PAYMENT-SERVICE") public interface PaymentFeignService { @GetMapping(value = "/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id); @PostMapping(value = "/payment/creat") public CommonResult creat(@RequestBody Payment payment); }5.controller调用@RestController @Slf4j public class FeignOrderController { @Resource public PaymentFeignService paymentFeignService; @GetMapping(value = "/consumer/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") Long id) { return paymentFeignService.getPaymentById(id); } }6.配置详细日志FeignConfig.java@Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel(){ return Logger.Level.FULL; } }application.xmllogging: level: # feign日志以什么级别监控哪个接口 org.mc.mycloud.service.PaymentFeignService: debug
1.自定义ribbon 查询机制目录结构：org--mc----mycloud------OrderMain80.java //主启动类----myrule------MyRibbonRule.java //不能和主启动类在一个包或其子包下a.自定义MyRibbonRule.java@Configuration public class MyRibbonRule { @Bean public IRule myRule(){ return new RandomRule(); //随机规则 } }b.主启动类@SpringBootApplication @EnableEurekaClient @RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MyRibbonRule.class) //修改默认的ribbon规则 public class OrderMain80 { public static void main(String[] args) { SpringApplication.run(OrderMain80.class, args); } }ribbon 包括了 restTemplate 是restTemplate 的增强。
SpringCloud 学习笔记 02 --zookeeperSpringCloud 学习笔记 02 --zookeeper1. docker搭建zookeeper环境：centOS7a. 拉取 zookeeper 镜像docker pull zookeeperb. 部署1.将它部署在 /usr/local/zookeeper 目录下：cd /usr/local && mkdir zookeeper && cd zookeeper2.创建data目录，用于挂载容器中的数据目录：mkdir data3.部署命令docker run -d -e TZ="Asia/Shanghai" -p 2181:2181 -v $PWD/data:/data --name zookeeper --restart always zookeeper-e TZ="Asia/Shanghai" # 指定上海时区 -d # 表示在一直在后台运行容器 -p 2181:2181 # 对端口进行映射，将本地2181端口映射到容器内部的2181端口 --name # 设置创建的容器名称 -v # 将本地目录(文件)挂载到容器指定目录； --restart always #始终重新启动zookeeper查看容器启动情况：docker ps -a4.连接使用zk命令行客户端连接zkdocker run -it --rm --link zookeeper:zookeeper zookeeper zkCli.sh -server zookeeper说明：-server zookeeper是启动zkCli.sh的参数5.其他命令# 查看zookeeper容器实例进程信息 docker top zookeeper # 停止zookeeper实例进程 docker stop zookeeper # 启动zookeeper实例进程 docker start zookeeper # 重启zookeeper实例进程 docker restart zookeeper # 查看zookeeper进程日志 docker logs -f zookeeper # 杀死zookeeper实例进程 docker kill -s KILL zookeeper # 移除zookeeper实例 docker rm -f -v zookeeperc. 集群方式安装1.安装docker-composecurl -L https://github.com/docker/compose/releases/download/1.22.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose2.配置docker-compose编写配置文件，并将其命名为：docker-compose.yml（docker-compose默认配置文件名）配置文件内容：version: '2' services: zoo1: image: zookeeper restart: always container_name: zoo1 ports: - "2181:2181" environment: ZOO_MY_ID: 1 ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888 zoo2: image: zookeeper restart: always container_name: zoo2 ports: - "2182:2181" environment: ZOO_MY_ID: 2 ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888 zoo3: image: zookeeper restart: always container_name: zoo3 ports: - "2183:2181" environment: ZOO_MY_ID: 3 ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888此配置文件表示，Docker需要启动三个zookeeper实例，并将2181，2182，2183三个端口号映射到容器内的2181这个端口上。ZOO_MY_ID：表示zk服务的ID, 取值为1-255之间的整数，且必须唯一ZOO_SERVERS：表示zk集群的主机列表3.启动zookeeper集群docker-compose up -d该命令执行需要在docker-compose配置文件的目录下执行，结果如下：[root@izbp13xko46hud9vfr5s94z conf]# docker-compose up -d Starting zoo1 ... done Starting zoo2 ... done Starting zoo3 ... done [root@izbp13xko46hud9vfr5s94z conf]#4.查看zookeeper集群实例通过docker ps查看[root@izbp13xko46hud9vfr5s94z ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e53b4c838001 zookeeper "/docker-entrypoint.…" 4 minutes ago Up 44 seconds 2888/tcp, 0.0.0.0:2181->2181/tcp, 3888/tcp zoo1 19282fb6f9b4 zookeeper "/docker-entrypoint.…" 4 minutes ago Up 44 seconds 2888/tcp, 3888/tcp, 0.0.0.0:2182->2181/tcp zoo2 099b926fa2d3 zookeeper "/docker-entrypoint.…" 4 minutes ago Up 44 seconds 2888/tcp, 3888/tcp, 0.0.0.0:2183->2181/tcp zoo3通过docker-compose ps查看[root@izbp13xko46hud9vfr5s94z conf]# docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------ zoo1 /docker-entrypoint.sh zkSe ... Up 0.0.0.0:2181->2181/tcp, 2888/tcp, 3888/tcp zoo2 /docker-entrypoint.sh zkSe ... Up 0.0.0.0:2182->2181/tcp, 2888/tcp, 3888/tcp zoo3 /docker-entrypoint.sh zkSe ... Up 0.0.0.0:2183->2181/tcp, 2888/tcp, 3888/tcp [root@izbp13xko46hud9vfr5s94z conf]#注：这个命令需要在docker-compose配置文件下执行。5.管理docker-compose服务# 停止docker-compose服务 docker-compose stop # 启动docker-compose服务 docker-compose start # 重启docker-compose服务 docker-compose restart6.查看zookeeper集群节点主从关系[root@izbp13xko46hud9vfr5s94z conf]# docker exec -it zoo1 /bin/bash bash-4.4# ./bin/zkServer.sh status ZooKeeper JMX enabled by default Using config: /conf/zoo.cfg Mode: follower bash-4.4#2. 服务提供者子模块a. pom<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mc.mycloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> <exclusions> <exclusion> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.9</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>b. applicaiton.ymlserver: port: 8004 spring: application: name: cloud-provider-zookeeper cloud: zookeeper: connect-string: ***.***.***.***:2181 #zookeeper ip地址和端口号c. 主启动@SpringBootApplication @EnableDiscoveryClient //该注解 用于向使用consul 或者 zookeeper 作为注册中心时注册服务 public class ZookeeperMain8004 { public static void main(String[] args) { SpringApplication.run(ZookeeperMain8004.class, args); } }d. 流程代码@RestController @Slf4j public class PaymentController { //查询8004 能不能进入到zookeeper 注册中心 @Value("${server.port}") private String serverPort; @RequestMapping("/payment/zk") public String paymentZk() { return "springCloud whit zookeeper:" + serverPort + "\t" + UUID.randomUUID().toString(); } }3. 消费者子模块a. pom<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> <exclusions> <exclusion> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.9</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>b. application.ymlserver: port: 80 spring: application: name: cloud-consumerzk-order cloud: zookeeper: connect-string: ***.***.***.***:2181 #zookeeper ip地址和端口号c. 主启动@SpringBootApplication public class OrderZkMain80 { public static void main(String[] args) { SpringApplication.run(OrderZkMain80.class, args); } }d. 开启负载均衡@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }e. 流程代码@RestController @Slf4j public class OrderZkController { public static final String INVOKE_URI = "http://cloud-provider-zookeeper"; @Resource private RestTemplate restTemplate; @GetMapping("consumer/payment/zk") public String paymentInfo() { String result = restTemplate.getForObject(INVOKE_URI + "/payment/zk", String.class); return result; } }4.总结a.zookeeper是临时还是持久节点？零时b.版本依赖报错：​ SpringBoot整合zookeeper客户端，先排除自带的zookeeper，再添加相应的版本。 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> <exclusions> <exclusion> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.9</version> </dependency>c.zookeeper和eureka的区别单独的zookeeper配置，没有默认自我开启的保护机制。配置相对eureka简单。服务名称既spring配置文件的应用名称spring: application: name: cloud-consumerzk-order #zookeeper和eureka 都使用此name作为服务名称
1.win10使用consula.官网下载地址b.安装使用教程c.使用方法2.Linux使用consul3.服务提供者子模块a.pom<dependencies> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>RELEASE</version> <scope>test</scope> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>RELEASE</version> <scope>test</scope> </dependency> </dependencies>b.applicaiton.yml###consul服务端口号 server: port: 8006 spring: application: name: consul-provider-payment ####consul注册中心地址 cloud: consul: host: localhost port: 8500 discovery: #hostname: 127.0.0.1 service-name: ${spring.application.name}c.主启动@SpringBootApplication @EnableDiscoveryClient //用于zookeeper 和 consul 服务发现 public class ConsulMain8006 { public static void main(String[] args) { SpringApplication.run(ConsulMain8006.class, args); } }d.流程代码@RestController public class PaymentController { @Value("${server.port}") private String serverPort; @RequestMapping("/payment/consul") public String paymentConsul() { return "springCloud whit consul:" + serverPort + "\t" + UUID.randomUUID().toString(); } }4.消费者子模块a.pom<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>b.applicaiton.yml###consul服务端口号 server: port: 80 spring: application: name: cloud-consumer-order ####consul注册中心地址 cloud: consul: host: localhost port: 8500 discovery: #hostname: 127.0.0.1 service-name: ${spring.application.name}c.主启动@SpringBootApplication @EnableDiscoveryClient //用于zookeeper 和 consul 服务发现 eureka消费者模块不用 public class OrderConsulMain80 { public static void main(String[] args) { SpringApplication.run(OrderConsulMain80.class, args); } }d.开启负载均衡@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced //开启负载均衡 public RestTemplate getRestTemplate() { return new RestTemplate(); } }e.流程代码@RestController @Slf4j public class OrderController { public static final String INVOKE_URI = "http://consul-provider-payment"; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/payment/consul") public String paymentInfo() { return restTemplate.getForObject(INVOKE_URI + "/payment/consul", String.class); } }5.总结a. consul zookeeper eureka 区别组件语言CAP服务健康检查对外暴露接口SpringCloud集成EurekaJavaAP可配支持HTTP已集成ConsulGoCP支持HTTP/DNS已集成ZookeeperJavaCP支持客户端已集成b.CAPCConsistency（强一致性）AAvailability (可用性)PPartition tolerance (分区容错性)CAP理论关注粒度是数据，而不是整体系统设计。c. consul zookeeper eureka 的CAP选型最多只能同时较好的满足两个。CAP理论的核心是：一个分布式系统不可能同时很好的满足一致性，可用性和分区容错性这三个需求，因此，根据CAP 原理将NoSQL 数据库分成了满足CA原则、满足CP原则和满足AP原则三大类：CA单点集群，满足一致性，可用性的系统，通常在可扩展性上不太强大。CP满足一致性，分区容忍性的系统，通常性能不是特别高AP满足可用性，分区容忍性的系统，通常可能对一致性要求低一些。