能力说明:
掌握封装、继承和多态设计Java类的方法,能够设计较复杂的Java类结构;能够使用泛型与集合的概念与方法,创建泛型类,使用ArrayList,TreeSet,TreeMap等对象掌握Java I/O原理从控制台读取和写入数据,能够使用BufferedReader,BufferedWriter文件创建输出、输入对象。
暂时未有相关云产品技术能力~
在实际项目中,对于敏感数据的保护显得十分重要,数据脱敏又称数据去隐私化或数据变形,是在给定的规则、策略下对敏感数据进行变换、修改的技术机制,能够在很大程度上解决敏感数据在非可信环境中使用的问题。本文使用自定义注解,在返回数据给前端的时候,根据给定的脱敏规则实现敏感数据脱敏操作,实现过程非常简单,一起来看看吧!1、引入依赖<!-- hutool工具类 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.6.7</version> </dependency> 2、自定义注解 - Desensitizeimport com.asurplus.common.enums.DesensitizeRuleEnums; import com.asurplus.common.jackson.SensitiveJsonSerializer; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 数据脱敏注解 * * @Author Asurplus */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @JacksonAnnotationsInside @JsonSerialize(using = SensitiveJsonSerializer.class) public @interface Desensitize { /** * 脱敏规则 */ DesensitizeRuleEnums rule(); }3、脱敏规则枚举类import cn.hutool.core.util.DesensitizedUtil; import lombok.AllArgsConstructor; import java.util.function.Function; /** * 脱敏策略 * * @author Asurplus */ @AllArgsConstructor public enum DesensitizeRuleEnums { /** * 用户id脱敏 */ USER_ID(s -> String.valueOf(DesensitizedUtil.userId())), /** * 中文姓名脱敏 */ CHINESE_NAME(DesensitizedUtil::chineseName), /** * 身份证脱敏 */ ID_CARD(s -> DesensitizedUtil.idCardNum(s, 3, 4)), /** * 固定电话 */ FIXED_PHONE(DesensitizedUtil::fixedPhone), /** * 手机号脱敏 */ MOBILE_PHONE(DesensitizedUtil::mobilePhone), /** * 地址脱敏 */ ADDRESS(s -> DesensitizedUtil.address(s, 8)), /** * 电子邮箱脱敏 */ EMAIL(DesensitizedUtil::email), /** * 密码脱敏 */ PASSWORD(DesensitizedUtil::password), /** * 中国车牌脱敏 */ CAR_LICENSE(DesensitizedUtil::carLicense), /** * 银行卡脱敏 */ BANK_CARD(DesensitizedUtil::bankCard); /** * 可自行添加其他脱敏策略 */ private final Function<String, String> desensitize; public Function<String, String> desensitize() { return desensitize; } }这其中的脱敏规则全都依赖 hutool 的 DesensitizedUtil 工具类,有其它的脱敏规则可以自定义实现4、数据脱敏 JSON 序列化工具import com.asurplus.common.annotation.Desensitize; import com.asurplus.common.enums.DesensitizeRuleEnums; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.ContextualSerializer; import java.io.IOException; import java.util.Objects; /** * 数据脱敏JSON序列化工具 * * @Author Asurplus */ public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer { private DesensitizeRuleEnums rule; @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeString(rule.desensitize().apply(value)); } @Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { Desensitize annotation = property.getAnnotation(Desensitize.class); if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) { this.rule = annotation.rule(); return this; } return prov.findValueSerializer(property.getType(), property); } }SpringBoot 中默认使用 jackson 作为 JSON 序列化工具,我们获取到自定义的脱敏注解(Desensitize),就对该数据实现我们的脱敏操作,就完成了我们的敏感数据脱敏操作。5、测试5.1 测试对象@Data static class UserInfo { // 用户id private Long id; // 姓名 @Desensitize(rule = DesensitizeRuleEnums.CHINESE_NAME) private String name; // 邮箱 @Desensitize(rule = DesensitizeRuleEnums.EMAIL) private String email; // 电话 @Desensitize(rule = DesensitizeRuleEnums.MOBILE_PHONE) private String phone; }定义了一个 UserInfo 对象,并对敏感数据进行了脱敏操作5.2 接口提供@GetMapping("test") public UserInfo test() { UserInfo userInfo = new UserInfo(); userInfo.setId(1004L); userInfo.setName("张三"); userInfo.setEmail("1859656863@qq.com"); userInfo.setPhone("15286535426"); return userInfo; }5.3 测试结果访问接口:http://localhost:8080/test得到数据:可以看出,对应的敏感数据都被进行脱敏操作了,我们的脱敏注解也就成功了
Google 身份验证器 Google Authenticator 是谷歌推出的基于时间的一次性密码 (Time-based One-time Password,简称 TOTP),只需要在手机上安装该 APP,就可以生成一个随着时间变化的一次性密码,用于帐户验证。Google 身份验证器是一款基于时间与哈希的一次性密码算法的两步验证软件令牌,此软件用于 Google 的认证服务。此项服务所使用的算法已列于 RFC 6238 和 RFC 4226 中。1、安装谷歌身份验证器苹果用户在App Store搜索google authenticator安卓用户http://s.downpp.com//apk9/googlesfyzq_5.10_2265.com.apk2、引入 maven 依赖<!-- 加密工具 --> <dependency> <groupId>top.lrshuai.encryption</groupId> <artifactId>encryption-tools</artifactId> <version>1.0.0</version> </dependency> <!-- 二维码依赖 --> <dependency> <groupId>org.iherus</groupId> <artifactId>qrext4j</artifactId> <version>1.3.1</version> </dependency>谷歌身份验证器工具类import org.apache.commons.codec.binary.Base32; import org.apache.commons.codec.binary.Hex; import org.springframework.util.StringUtils; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; /** * 谷歌身份验证器工具类 */ public class GoogleAuthenticator { /** * 时间前后偏移量 * 用于防止客户端时间不精确导致生成的TOTP与服务器端的TOTP一直不一致 * 如果为0,当前时间为 10:10:15 * 则表明在 10:10:00-10:10:30 之间生成的TOTP 能校验通过 * 如果为1,则表明在 * 10:09:30-10:10:00 * 10:10:00-10:10:30 * 10:10:30-10:11:00 之间生成的TOTP 能校验通过 * 以此类推 */ private static int WINDOW_SIZE = 0; /** * 加密方式,HmacSHA1、HmacSHA256、HmacSHA512 */ private static String CRYPTO = "HmacSHA1"; /** * 生成密钥,每个用户独享一份密钥 * * @return */ public static String getSecretKey() { SecureRandom random = new SecureRandom(); byte[] bytes = new byte[20]; random.nextBytes(bytes); Base32 base32 = new Base32(); String secretKey = base32.encodeToString(bytes); // make the secret key more human-readable by lower-casing and // inserting spaces between each group of 4 characters return secretKey.toUpperCase(); } /** * 生成二维码内容 * * @param secretKey 密钥 * @param account 账户名 * @param issuer 网站地址(可不写) * @return */ public static String getQrCodeText(String secretKey, String account, String issuer) { String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase(); try { return "otpauth://totp/" + URLEncoder.encode((!StringUtils.isEmpty(issuer) ? (issuer + ":") : "") + account, "UTF-8").replace("+", "%20") + "?secret=" + URLEncoder.encode(normalizedBase32Key, "UTF-8").replace("+", "%20") + (!StringUtils.isEmpty(issuer) ? ("&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20")) : ""); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e); } } /** * 获取验证码 * * @param secretKey * @return */ public static String getCode(String secretKey) { String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase(); Base32 base32 = new Base32(); byte[] bytes = base32.decode(normalizedBase32Key); String hexKey = Hex.encodeHexString(bytes); long time = (System.currentTimeMillis() / 1000) / 30; String hexTime = Long.toHexString(time); return TOTP.generateTOTP(hexKey, hexTime, "6", CRYPTO); } /** * 检验 code 是否正确 * * @param secret 密钥 * @param code code * @param time 时间戳 * @return */ public static boolean checkCode(String secret, long code, long time) { Base32 codec = new Base32(); byte[] decodedKey = codec.decode(secret); // convert unix msec time into a 30 second "window" // this is per the TOTP spec (see the RFC for details) long t = (time / 1000L) / 30L; // Window is used to check codes generated in the near past. // You can use this value to tune how far you're willing to go. long hash; for (int i = -WINDOW_SIZE; i <= WINDOW_SIZE; ++i) { try { hash = verifyCode(decodedKey, t + i); } catch (Exception e) { // Yes, this is bad form - but // the exceptions thrown would be rare and a static // configuration problem // e.printStackTrace(); throw new RuntimeException(e.getMessage()); } if (hash == code) { return true; } } return false; } /** * 根据时间偏移量计算 * * @param key * @param t * @return * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ private static long verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException { byte[] data = new byte[8]; long value = t; for (int i = 8; i-- > 0; value >>>= 8) { data[i] = (byte) value; } SecretKeySpec signKey = new SecretKeySpec(key, CRYPTO); Mac mac = Mac.getInstance(CRYPTO); mac.init(signKey); byte[] hash = mac.doFinal(data); int offset = hash[20 - 1] & 0xF; // We're using a long because Java hasn't got unsigned int. long truncatedHash = 0; for (int i = 0; i < 4; ++i) { truncatedHash <<= 8; // We are dealing with signed bytes: // we just keep the first byte. truncatedHash |= (hash[offset + i] & 0xFF); } truncatedHash &= 0x7FFFFFFF; truncatedHash %= 1000000; return truncatedHash; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { String secretKey = getSecretKey(); System.out.println("secretKey:" + secretKey); String code = getCode(secretKey); System.out.println("code:" + code); boolean b = checkCode(secretKey, Long.parseLong(code), System.currentTimeMillis()); System.out.println("isSuccess:" + b); } } }验证码生成工具类import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.lang.reflect.UndeclaredThrowableException; import java.math.BigInteger; import java.security.GeneralSecurityException; /** * 验证码生成工具类 */ public class TOTP { // 0 1 2 3 4 5 6 7 8 private static final int[] DIGITS_POWER = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000}; /** * This method uses the JCE to provide the crypto algorithm. HMAC computes a * Hashed Message Authentication Code with the crypto hash algorithm as a * parameter. * * @param crypto : the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512) * @param keyBytes : the bytes to use for the HMAC key * @param text : the message or text to be authenticated */ private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) { try { Mac hmac; hmac = Mac.getInstance(crypto); SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW"); hmac.init(macKey); return hmac.doFinal(text); } catch (GeneralSecurityException gse) { throw new UndeclaredThrowableException(gse); } } /** * This method converts a HEX string to Byte[] * * @param hex : the HEX string * @return: a byte array */ private static byte[] hexStr2Bytes(String hex) { // Adding one byte to get the right conversion // Values starting with "0" can be converted byte[] bArray = new BigInteger("10" + hex, 16).toByteArray(); // Copy all the REAL bytes, not the "first" byte[] ret = new byte[bArray.length - 1]; System.arraycopy(bArray, 1, ret, 0, ret.length); return ret; } /** * This method generates a TOTP value for the given set of parameters. * * @param key : the shared secret, HEX encoded * @param time : a value that reflects a time * @param returnDigits : number of digits to return * @param crypto : the crypto function to use * @return: a numeric String in base 10 that includes */ public static String generateTOTP(String key, String time, String returnDigits, String crypto) { int codeDigits = Integer.decode(returnDigits); String result = null; // Using the counter // First 8 bytes are for the movingFactor // Compliant with base RFC 4226 (HOTP) while (time.length() < 16) time = "0" + time; // Get the HEX in a Byte[] byte[] msg = hexStr2Bytes(time); byte[] k = hexStr2Bytes(key); byte[] hash = hmac_sha(crypto, k, msg); // put selected bytes into result int int offset = hash[hash.length - 1] & 0xf; int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); int otp = binary % DIGITS_POWER[codeDigits]; result = Integer.toString(otp); while (result.length() < codeDigits) { result = "0" + result; } return result; } }因为谷歌身份验证器支持两种方式添加,扫码和输入密钥添加,所以两种方式我们都实现了3、项目启动类import com.asurplus.common.google.GoogleAuthenticator; import com.asurplus.common.utils.QRCodeUtil; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; /** * 项目启动类 * * @Author Asurplus */ @RestController @SpringBootApplication public class AsurplusApplication { public static void main(String[] args) { SpringApplication.run(AsurplusApplication.class, args); } /** * 生成 Google 密钥,两种方式任选一种 */ @GetMapping("getSecret") public String getSecret() { return GoogleAuthenticator.getSecretKey(); } /** * 生成二维码,APP直接扫描绑定,两种方式任选一种 */ @GetMapping("getQrcode") public void getQrcode(String name, HttpServletResponse response) throws Exception { // 生成二维码内容 String qrCodeText = GoogleAuthenticator.getQrCodeText(GoogleAuthenticator.getSecretKey(), name, ""); // 生成二维码输出 new SimpleQrcodeGenerator().generate(qrCodeText).toStream(response.getOutputStream()); } /** * 获取code */ @GetMapping("getCode") public String getCode(String secretKey) { return GoogleAuthenticator.getCode(secretKey); } /** * 验证 code 是否正确 */ @GetMapping("checkCode") public String checkCode(String secret, String code) { boolean b = GoogleAuthenticator.checkCode(secret, Long.parseLong(code), System.currentTimeMillis()); if (b) { return "success"; } return "error"; } }4、测试1、获取密钥http://localhost:8080/getSecret我们就可以打开谷歌身份验证器APP,选择”输入设置密钥“,输入:账户名,密钥即可2、获取二维码http://localhost:8080/getQrcode?name=test打开谷歌身份验证器APP,选择“扫描二维码”,则会自动添加3、身份验证http://localhost:8080/checkCode?secret=BSW5QUVXPQ7QIDQ4R2JW7MFZPQJPGQPD&code=699118secret 为密钥,code 为APP生成的验证码5、设计优化我们在实际的开发过程中,每一个用户需要对应唯一的密钥,在 user 表中应该有一个 secret 字段,保存该用户的密钥,用户需要绑定该密钥,从而实现二步验证6、源码本文章中源码地址:https://gitee.com/asurplus/google-auth
接入微信小程序消息推送服务,可以3种方式选择其一:1、开发者服务器接收消息推送2、云函数接收消息推送3、微信云托管服务接收消息推送开发者服务器接收消息推送,开发者需要按照如下步骤完成:1、填写服务器配置2、验证服务器地址的有效性3、据接口文档实现业务逻辑,接收消息和事件1、引入 WxJava 依赖<!-- web支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 微信小程序开发 --> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>wx-java-miniapp-spring-boot-starter</artifactId> <version>4.2.0</version> </dependency>2、申请微信小程序https://mp.weixin.qq.com/使用邮箱注册一个微信小程序账号,一个邮箱仅能注册一个微信小程序账号3、微信小程序配置信息登录微信小程序后,在:开发–》开发管理 找到小程序的基本信息:将 AppID,AppSecret 配置在项目配置文件中# 微信开发配置 wx: # 微信小程序开发 miniapp: appid: xxxxxxxxxx secret: xxxxxxxxxx # 配置消息推送需要 token: aesKey: msgDataFormat: # 存储类型 config-storage: type: redistemplate配置了 config-storage.type 决定我们获取的 AccessToken 存放的位置,默认存放在本地缓存中,可选存在 redis 中,我们可以存放在 redis 中进行可视化管理。4、消息推送配置在开发管理页面往下滑,找到 “消息推送” 模块,启用 “消息推送”这里我已经启用了,我们点击修改,重新配置我们的消息推送配置1、URL,即微信推送消息的时候,调用你的 api 接口地址2、Token,这个为自定义 token,做参数校验使用的3、EncodingAESKey,消息加密密钥,我们可以选择随机生成4、消息加密方式,我们为了数据安全,选择 “安全模式”5、数据格式,我们选择 JSON 或 XML 都行对应的后台配置文件配置为:# 微信开发配置 wx: # 微信小程序开发 miniapp: appid: xxxxxxxxxx secret: xxxxxxxxxx # 配置消息推送需要 token: asurplus_token aesKey: PeQ3KmxFbhko0FdR5WG6Hn8wOuKuhQfr6ZNl7ykRGaM msgDataFormat: JSON # 存储类型 config-storage: type: redistemplate5、接收消息推送的 APIimport cn.binarywang.wx.miniapp.api.WxMaService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 微信小程序消息推送 */ @Slf4j @RestController @RequestMapping("wx/ma/welcome") public class WxMaMsgController { @Autowired private WxMaService wxMaService; /** * 消息校验,确定是微信发送的消息 * * @param signature * @param timestamp * @param nonce * @param echostr * @return * @throws Exception */ @GetMapping public String doGet(String signature, String timestamp, String nonce, String echostr) { // 消息合法 if (wxMaService.checkSignature(timestamp, nonce, signature)) { log.info("-------------微信小程序消息验证通过"); return echostr; } // 消息签名不正确,说明不是公众平台发过来的消息 return null; } }第四步配置的 URL 应为:http://lizhou.4kb.cn/wx/ma/welcome6、消息推送测试启动本地服务,在第四步的页面,点击 “确定”,得到如下结果:表示,消息推送配置成功,看后台日志:微信服务器推送的消息,通过了校验,表示确实是微信服务器发送的消息
我们在实际的项目应用中,Redis一般都是用来缓存热点数据,一台服务器可能部署了多个应用,应用直接的 Redis 数据需要加上前缀区分开来,我们可以使用序列化的方式,统一为所有的 key 加上前缀一、关于在 SpringBoot 中整合 Redishttps://lizhou.blog.csdn.net/article/details/98358258二、关于在 SpringBoot 中整合 Redis 实现序列化存储Java对象https://lizhou.blog.csdn.net/article/details/109236398三、基于第二步更改了 Redis 本身的序列化方式的基础上,我们可以自定义序列化的方式1、自定义序列化方式package com.asurplus.common.redis; import com.asurplus.common.consts.SystemConst; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.serializer.RedisSerializer; import java.nio.charset.Charset; /** * 为redis key 统一加上前缀 */ @Slf4j public class RedisKeySerializer implements RedisSerializer<String> { /** * 编码格式 */ private final Charset charset; /** * 前缀 */ private final String PREFIX_KEY = "prefix:"; public RedisKeySerializer() { this(Charset.forName("UTF8")); } public RedisKeySerializer(Charset charset) { this.charset = charset; } @Override public String deserialize(byte[] bytes) { String saveKey = new String(bytes, charset); int indexOf = saveKey.indexOf(PREFIX_KEY); if (indexOf > 0) { log.error("key缺少前缀"); } else { saveKey = saveKey.substring(indexOf); } return (saveKey.getBytes() == null ? null : saveKey); } @Override public byte[] serialize(String string) { String key = PREFIX_KEY + string; return (key == null ? null : key.getBytes(charset)); } }2、更改 key 的序列化方式@Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { // 配置redisTemplate RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置序列化 Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jackson2JsonRedisSerializer.setObjectMapper(om); // key序列化,使用自定义序列化方式 redisTemplate.setKeySerializer(new RedisKeySerializer()); // value序列化 redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // Hash key序列化,使用自定义序列化方式 redisTemplate.setHashKeySerializer(new RedisKeySerializer()); // Hash value序列化 redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; }3、使用方式存入:redisTemplate.opsForValue().set("test", "asurplus");在 Redis 数据库中我们就能看到存储的 key 是:‘prefix:test’,它的 value 是 ‘asurplus’取出:redisTemplate.opsForValue().get("test");获取到的 value是: ‘asurplus’至此,我们就实现了为 Redis 的 key 统一加上前缀,这样我们多个项目使用同一个 Redis 库时,就方便区分数据了
Sa-Token 支持 Redis、Memcached 等专业的缓存中间件中, 做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性一、引入Maven依赖<!-- springboot集成redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>以下两种选择一种即可:1、jdk默认序列化方式<!-- Sa-Token 整合 Redis (使用jdk默认序列化方式) --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-dao-redis</artifactId> <version>1.24.0</version> </dependency>优点:兼容性好缺点:Session序列化后基本不可读,对开发者来讲等同于乱码2、jackson序列化方式<!-- Sa-Token整合redis (使用jackson序列化方式) --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-dao-redis-jackson</artifactId> <version>1.22.0</version> </dependency>优点:Session序列化后可读性强,可灵活手动修改缺点:兼容性稍差我选用的是 jackson 序列化方式无论使用哪种序列化方式,你都必须为项目提供一个Redis实例化方案,例如:<!-- 提供Redis连接池 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>二、Redis 配置信息# Redis配置 redis: host: ${server.host} port: 6379 password: database: 0 jedis: pool: # 连接池最大连接数(使用负值表示没有限制) max-active: 50 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: 3000 # 连接池中的最大空闲连接数 max-idle: 20 # 连接池中的最小空闲连接数 min-idle: 5 # 连接超时时间(毫秒) timeout: 5000集成 Redis 后,Sa-Token 的相关数据就会自动保存到 Redis 中,例如:StpUtil.getTokenSession().set(); 方法
在 Sa-Token 的登录,授权,验证过程中,会抛出很多的异常,我们不能将这些异常信息直接返回给用户,因为用户是看不懂这些异常信息的,我们就需要对这些异常信息进行处理,处理之后再返回展示给前端用户1、统一异常处理package com.asurplus.common.satoken; import cn.dev33.satoken.exception.*; import com.asurplus.common.utils.RES; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理 * * @Author Lizhou */ @RestControllerAdvice public class SaTokenExceptionHandler { @ExceptionHandler(NotLoginException.class) public RES handlerNotLoginException(NotLoginException nle) { // 不同异常返回不同状态码 String message = ""; if (nle.getType().equals(NotLoginException.NOT_TOKEN)) { message = "未提供Token"; } else if (nle.getType().equals(NotLoginException.INVALID_TOKEN)) { message = "未提供有效的Token"; } else if (nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) { message = "登录信息已过期,请重新登录"; } else if (nle.getType().equals(NotLoginException.BE_REPLACED)) { message = "您的账户已在另一台设备上登录,如非本人操作,请立即修改密码"; } else if (nle.getType().equals(NotLoginException.KICK_OUT)) { message = "已被系统强制下线"; } else { message = "当前会话未登录"; } // 返回给前端 return RES.no(401, message); } @ExceptionHandler public RES handlerNotRoleException(NotRoleException e) { return RES.no(403, "无此角色:" + e.getRole()); } @ExceptionHandler public RES handlerNotPermissionException(NotPermissionException e) { return RES.no(403, "无此权限:" + e.getCode()); } @ExceptionHandler public RES handlerDisableLoginException(DisableLoginException e) { return RES.no(401, "账户被封禁:" + e.getDisableTime() + "秒后解封"); } @ExceptionHandler public RES handlerNotSafeException(NotSafeException e) { return RES.no(401, "二级认证异常:" + e.getMessage()); } }Sa-Token 会抛出的异常大概就有这些,你可以根据你实际的业务需求,对不用的异常场景返回不同的业务信息,方便前端开发人员做不同的处理
我们已经实现了 Sa-Token 的登录方法,并在登录成功后,返回给前端 token 信息,本篇文章介绍在 Sa-Token 中的一些个性化配置参数配置参数我们可以写在 SpringBoot 的配置文件中,也可以通过配置类,来配置 Sa-Token 的参数信息,本片文章主要讲配置类的方式1、配置文件 方式server: # 端口 port: 8080 # Sa-Token配置 sa-token: # token名称 (同时也是cookie名称) token-name: satoken # token有效期,单位s 默认30天, -1代表永不过期 timeout: 2592000 # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 activity-timeout: -1 # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) is-share: false # token风格 token-style: uuid # 是否输出操作日志 is-log: false 2、配置类方式package com.asurplus.common.satoken; import cn.dev33.satoken.config.SaTokenConfig; import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; /** * Sa-Token代码方式进行配置 */ @Configuration public class SaTokenConfigure { /** * 配置参数 * * @return */ @Bean @Primary public SaTokenConfig getSaTokenConfigPrimary() { SaTokenConfig config = new SaTokenConfig(); // token名称 (同时也是cookie名称) config.setTokenName("Authorization"); // token风格 config.setTokenStyle("tik"); // token前缀 config.setTokenPrefix("Bearer"); // token有效期,单位s 默认30天,不支持自动续签 config.setTimeout(30 * 24 * 60 * 60); // token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒,支持自动续签 config.setActivityTimeout((30 * 60); // 自动续签,指定时间内有操作,则会自动续签 config.setAutoRenew(true); // 是否尝试从header里读取token config.setIsReadHead(true); // 是否尝试从cookie里读取token config.setIsReadCookie(false); // 是否尝试从请求体里读取token config.setIsReadBody(false); // 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) config.setIsConcurrent(false); // 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) config.setIsShare(false); // 是否在初始化配置时打印版本字符画 config.setIsPrint(true); // 是否输出操作日志 config.setIsLog(true); return config; } } 前后端直接传递用户身份信息有三种方式,1、cookie 的方式2、head 的方式3、body 的方式3、配置信息说明我们配置了以在 head 中传递 token 的方式实现前后端分离开发,token 的生成规则为 tik 方式,生成效果如下:VW_aA7q8lPRkRn91S_2h06K63wHy6ekBYH__我们还配置了 token 的前缀为 Bearer,在 head 中传递的 key 为 Authorization配置了 token 的过期时间为 30 * 24 * 60 * 60 秒,也就是 30 天,临时有效期为 30 * 60 秒,也就是 30 分钟,怎么理解呢?也就是说后端生成的 token,即使用户一直操作,在 30 天后一定会过期,要求用户重新登录临时有效期 30 分钟,也就是如果用户超过 30 分钟不操作,此 token 就会过期,便会要求用户重新登录有效期不支持自动续签临时有效期支持自动续签其他的配置信息,请参考开发文档或阅读源码
一般我们的 session 会话过期时间默认为 30 分钟,有的用户认为 30 分钟太短了,有时候临时有事出去了,回来已经过期了,工作还没完成就只能登出了,非常不方便,于是要求我们改变 session 的过期时间1、指定本系统 sessionid/** * 指定本系统sessionid, 问题: 与servlet容器名冲突, 如jetty, tomcat 等默认jsessionid, * 当跳出shiro servlet时如error-page容器会为jsessionid重新分配值导致登录会话丢失! * * @return */ @Bean public SimpleCookie sessionIdCookie() { SimpleCookie simpleCookie = new SimpleCookie("shiro.session"); // 防止xss攻击,窃取cookie内容 simpleCookie.setHttpOnly(true); return simpleCookie; }指定了本地的 session 的 key 为 shiro.session,便于单独管理2、配置 session 管理器/** * 会话管理 * 默认使用容器session,这里改为自定义session * session的全局超时时间默认是30分钟 * * @return */ @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); // 会话超时时间,单位:毫秒 sessionManager.setGlobalSessionTimeout(60 * 60 * 1000); // 定时清理失效会话, 清理用户直接关闭浏览器造成的孤立会话 sessionManager.setSessionValidationInterval(60 * 60 * 1000); // 是否开启定时清理失效会话 sessionManager.setSessionValidationSchedulerEnabled(true); // 指定sessionid sessionManager.setSessionIdCookie(sessionIdCookie()); // 是否允许将sessionId 放到 cookie 中 sessionManager.setSessionIdCookieEnabled(true); // 是否允许将 sessionId 放到 Url 地址拦中 sessionManager.setSessionIdUrlRewritingEnabled(false); // 默认使用MemerySessionDao,设置为EnterpriseCacheSessionDAO以配合ehcache实现分布式集群缓存支持 sessionManager.setSessionDAO(new EnterpriseCacheSessionDAO()); return sessionManager; }设置了过期时间为 60 * 60 * 1000 = 60 分钟,即表示用户 60 分钟无操作后,会话就会过期3、配置安全管理器/** * 安全管理器 */ @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置自定义的relam securityManager.setRealm(loginRelam()); // 设置session管理器 securityManager.setSessionManager(sessionManager()); return securityManager; }
1、MySQL 自带功能创建时间可以设置默认值为 CURRENT_TIMESTAMP,在 MySQL 5.7 以上版本支持更新时间可以设置默认值为 CURRENT_TIMESTAMP,并勾选根据当前时间戳更新,在 MySQL 5.7 以上版本支持以上是 MySQL 提供的默认值操作和自动更新操作,需要主要 MySQL 的版本号2、MyBatis-Plus 实现自动填充1、首先实体类需要加上注解@ApiModelProperty(value = "创建者") @TableField("create_user") private Integer createUser; @ApiModelProperty(value = "创建时间") @TableField(value = "create_time", fill = FieldFill.INSERT) private Date createTime; @ApiModelProperty(value = "更新人") @TableField("update_user") private Integer updateUser; @ApiModelProperty(value = "更新时间") @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) private Date updateTime; @ApiModelProperty(value = "删除状态(0--未删除1--已删除)") @TableField("del_flag") @TableLogic private Integer delFlag;创建时间字段加上:fill = FieldFill.INSERT,表示在 INSERT 操作时自动填充更新时间字段加上:fill = FieldFill.INSERT_UPDATE,表示在 INSERT 和 UPDATE 操作时自动填充2、实现自定义填充处理类package com.asurplus.common.mybatis; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.util.Date; /** * 自动填充策略 */ @Slf4j @Component public class MyMetaObjectHandler implements MetaObjectHandler { /** * 插入时的填充策略 * * @param metaObject */ @Override public void insertFill(MetaObject metaObject) { this.setFieldValByName("createTime", new Date(), metaObject); this.setFieldValByName("updateTime", new Date(), metaObject); } /** * 更新时的填充策略 * * @param metaObject */ @Override public void updateFill(MetaObject metaObject) { this.setFieldValByName("updateTime", new Date(), metaObject); } }我们指定了在 INSERT 操作时填充 createTime、updateTime 字段的 值为当前时间 new Date()在 UPDATE 操作时填充 updateTime 字段的 值为当前时间 new Date()这样我们就完成了自动填充的配置,当使用 MyBatis-Plus 进行 INSERT 或 UPDATE 操作时,都会自动填充 createTime、updateTime 字段内容注意:在 xml 文件中写的 INSERT 或 UPDATE 操作无效
一、简介Aspose 是 .NET 和 Java 开发组件以及为 Microsoft SQL Server Reporting Services 和 JasperReports 等平台提供渲染扩展的领先供应商。它的核心重点是提供最完整和最强大的文件管理产品。Aspose 产品支持一些商业上最流行的文件格式,包括:Word 文档、Excel 电子表格、PowerPoint 演示文稿、PDF 文档、Flash 演示文稿和项目文件。二、下载下载 Aspose 的依赖 Jar 包可以通过一下仓库下载:Aspose 依赖下载https://repository.aspose.com/repo/com/aspose/本次我们需要使用的 Jar 包如下:1、aspose-words,word 转 pdf 使用2、aspose-cells,excel 转 pdf 使用3、aspose-slides,ppt 转 pdf 使用三、整合 Aspose1、将下载下来的 Jar 包依赖放在 resources/lib/ 目录下2、pom.xml 文件引入<!-- 转化pdf start --> <dependency> <groupId>aspose-words</groupId> <artifactId>aspose-words</artifactId> <version>15.8.0</version> <scope>system</scope> <systemPath>${project.basedir}/src/main/resources/lib/aspose-words-15.8.0-jdk16.jar</systemPath> </dependency> <dependency> <groupId>aspose-cells</groupId> <artifactId>aspose-cells</artifactId> <version>8.8.0</version> <scope>system</scope> <systemPath>${project.basedir}/src/main/resources/lib/aspose-cells-8.8.0.jar</systemPath> </dependency> <dependency> <groupId>aspose-slides</groupId> <artifactId>aspose-slides</artifactId> <version>20.4</version> <scope>system</scope> <systemPath>${project.basedir}/src/main/resources/lib/aspose-slides-20.4-jdk16.jar</systemPath> </dependency> <!-- 转化pdf end -->四、自定义工具类我们将转化的方法放在一个工具类中,PdfUtils.java,内容如下:package com.asurplus.common.office; import com.aspose.cells.Workbook; import com.aspose.slides.Presentation; import com.aspose.words.Document; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.UUID; /** * 文件转PDF * <p> * Aspose下载地址:https://repository.aspose.com/repo/com/aspose/ */ public class PdfUtils { /** * word 转为 pdf 输出 * * @param inPath word文件 * @param outPath pdf 输出文件目录 */ public static String word2pdf(String inPath, String outPath) { // 验证License if (!isWordLicense()) { return null; } FileOutputStream os = null; try { String path = outPath.substring(0, outPath.lastIndexOf(File.separator)); File file = new File(path); // 创建文件夹 if (!file.exists()) { file.mkdirs(); } // 新建一个空白pdf文档 file = new File(outPath); os = new FileOutputStream(file); // Address是将要被转化的word文档 Document doc = new Document(inPath); // 全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF, doc.save(os, com.aspose.words.SaveFormat.PDF); os.close(); } catch (Exception e) { if (os != null) { try { os.close(); } catch (IOException e1) { e1.printStackTrace(); } } e.printStackTrace(); } return outPath; } /** * excel 转为 pdf 输出 * * @param inPath excel 文件 * @param outPath pdf 输出文件目录 */ public static String excel2pdf(String inPath, String outPath) { // 验证License if (!isWordLicense()) { return null; } FileOutputStream os = null; try { String path = outPath.substring(0, outPath.lastIndexOf(File.separator)); File file = new File(path); // 创建文件夹 if (!file.exists()) { file.mkdirs(); } // 新建一个空白pdf文档 file = new File(outPath); os = new FileOutputStream(file); // Address是将要被转化的excel表格 Workbook workbook = new Workbook(new FileInputStream(getFile(inPath))); workbook.save(os, com.aspose.cells.SaveFormat.PDF); os.close(); } catch (Exception e) { if (os != null) { try { os.close(); } catch (IOException e1) { e1.printStackTrace(); } } e.printStackTrace(); } return outPath; } /** * ppt 转为 pdf 输出 * * @param inPath ppt 文件 * @param outPath pdf 输出文件目录 */ public static String ppt2pdf(String inPath, String outPath) { // 验证License if (!isWordLicense()) { return null; } FileOutputStream os = null; try { String path = outPath.substring(0, outPath.lastIndexOf(File.separator)); File file = new File(path); // 创建文件夹 if (!file.exists()) { file.mkdirs(); } // 新建一个空白pdf文档 file = new File(outPath); os = new FileOutputStream(file); // Address是将要被转化的PPT幻灯片 Presentation pres = new Presentation(new FileInputStream(getFile(inPath))); pres.save(os, com.aspose.slides.SaveFormat.Pdf); os.close(); } catch (Exception e) { if (os != null) { try { os.close(); } catch (IOException e1) { e1.printStackTrace(); } } e.printStackTrace(); } return outPath; } /** * 验证 Aspose.word 组件是否授权 * 无授权的文件有水印和试用标记 */ public static boolean isWordLicense() { boolean result = false; try { // 避免文件遗漏 String licensexml = "<License>\n" + "<Data>\n" + "<Products>\n" + "<Product>Aspose.Total for Java</Product>\n" + "<Product>Aspose.Words for Java</Product>\n" + "</Products>\n" + "<EditionType>Enterprise</EditionType>\n" + "<SubscriptionExpiry>20991231</SubscriptionExpiry>\n" + "<LicenseExpiry>20991231</LicenseExpiry>\n" + "<SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber>\n" + "</Data>\n" + "<Signature>sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=</Signature>\n" + "</License>"; InputStream inputStream = new ByteArrayInputStream(licensexml.getBytes()); com.aspose.words.License license = new com.aspose.words.License(); license.setLicense(inputStream); result = true; } catch (Exception e) { e.printStackTrace(); } return result; } /** * OutputStream 转 InputStream */ public static ByteArrayInputStream parse(OutputStream out) { ByteArrayOutputStream baos = (ByteArrayOutputStream) out; ByteArrayInputStream swapStream = new ByteArrayInputStream(baos.toByteArray()); return swapStream; } /** * InputStream 转 File */ public static File inputStreamToFile(InputStream ins, String name) throws Exception { File file = new File(System.getProperty("java.io.tmpdir") + File.separator + name); if (file.exists()) { return file; } OutputStream os = new FileOutputStream(file); int bytesRead; int len = 8192; byte[] buffer = new byte[len]; while ((bytesRead = ins.read(buffer, 0, len)) != -1) { os.write(buffer, 0, bytesRead); } os.close(); ins.close(); return file; } /** * 根据网络地址获取 File 对象 */ public static File getFile(String url) throws Exception { String suffix = url.substring(url.lastIndexOf(".")); HttpURLConnection httpUrl = (HttpURLConnection) new URL(url).openConnection(); httpUrl.connect(); return PdfUtils.inputStreamToFile(httpUrl.getInputStream(), UUID.randomUUID().toString() + suffix); } }1、我们需要通过 isWordLicense() 方法验证 Aspose.word 组件是否授权,如果未授权,转化出来的文件会带有水印和使用标记,影响阅读,因为 Aspose.word 是一个商用版本,目前 word 转 pdf 正常,excel 转 pdf 会带有水印但不影响阅读,ppt 转 pdf 会有严重水印,影响阅读2、根据自定义的 pdf 输出目录,新建一个空白的 pdf 文件,然后将空白的 pdf 文件转化为 文件输出流 FileOutputStream3、Document doc = new Document(inPath);,doc 就是将要被转化的 word 文档4、doc.save(os, com.aspose.words.SaveFormat.PDF);,将转化的 word 文档写入空白的 pdf 文件中,就得到了我们的 pdf 文件5、os.close();,别忘记关闭输出流噢6、excel,ppt 的转化原理也是一致五、测试我们通过 API 的形式来,测试能不能将文件抓为 PDF 文件实现在线预览1、开放 APIpackage com.asurplus.api.controller; import com.asurplus.common.office.PdfUtils; import io.swagger.annotations.Api; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.UUID; /** * 在线office预览 * * @Author admin **/ @Api(tags = "在线office预览") @Controller @RequestMapping("api/office") public class OfficeApiController { @GetMapping("previewPdf") public void pdf(String url, HttpServletResponse response) throws Exception { if (StringUtils.isBlank(url)) { return; } File file = null; // 文件后缀 String suffix = url.substring(url.lastIndexOf(".") + 1); // 如果是PDF if ("pdf".equals(suffix)) { HttpURLConnection httpUrl = (HttpURLConnection) new URL(url).openConnection(); httpUrl.connect(); file = PdfUtils.inputStreamToFile(httpUrl.getInputStream(), UUID.randomUUID().toString() + ".pdf"); response.setContentType("application/pdf"); } // 如果是文本 else if ("txt".equals(suffix)) { HttpURLConnection httpUrl = (HttpURLConnection) new URL(url).openConnection(); httpUrl.connect(); file = PdfUtils.inputStreamToFile(httpUrl.getInputStream(), UUID.randomUUID().toString() + ".txt"); response.setContentType("text/html"); } // 如果是doc else if ("doc".equals(suffix) || "docx".equals(suffix)) { file = new File(PdfUtils.word2pdf(url, System.getProperty("user.dir") + UUID.randomUUID().toString() + ".pdf")); response.setContentType("application/pdf"); } // 如果是excel else if ("xls".equals(suffix) || "xlsx".equals(suffix)) { file = new File(PdfUtils.excel2pdf(url, System.getProperty("user.dir") + UUID.randomUUID().toString() + ".pdf")); response.setContentType("application/pdf"); } // 如果是ppt else if ("ppt".equals(suffix) || "pptx".equals(suffix)) { file = new File(PdfUtils.ppt2pdf(url, System.getProperty("user.dir") + UUID.randomUUID().toString() + ".pdf")); response.setContentType("application/pdf"); } // 如果文件为空 if (null == file) { return; } try { response.setCharacterEncoding("UTF-8"); InputStream stream = new FileInputStream(file); ServletOutputStream out = response.getOutputStream(); byte buff[] = new byte[1024]; int length = 0; while ((length = stream.read(buff)) > 0) { out.write(buff, 0, length); } stream.close(); out.close(); out.flush(); } catch (IOException e) { e.printStackTrace(); } } }我们需要传入需要转化的文件的在线地址,通过 Aspose 将文件转化为 PDF 文件输出到页面,实现在线预览2、测试准备一个 word 文件,内容如下:在线转化后:通过访问 API,成功将 word 文件转化为 pdf 文件,实现了在线预览
一、引入达梦数据库驱动与 MySQL 同样如此,也需要驱动包来连接 MySQL,只不过 SpringBoot 对 MySQL 做了集成,没有对达梦数据库做集成,所以,我们需要自己引入驱动包,这个驱动包通过 maven 仓库是下载不了的由于我们之前是安装了达梦数据库(DM8)的,然后我们在其安装目录下是可以找到驱动包的D:\dmdbms\drivers\jdbc安装包在此目录下,如图所示:这三个驱动包分别对应的是 JDK 的版本,我用的是 JDK 1.8 的,所以我选择的是 DmJdbcDriver18.jar 驱动包,将其放在 resources\lib\ 目录下然后我们在 pom.xml 文件中,引入该文件<!-- 达梦数据库驱动 --> <dependency> <groupId>com.dm</groupId> <artifactId>DmJdbcDriver18</artifactId> <version>1.8</version> <scope>system</scope> <systemPath>${project.basedir}/src/main/resources/lib/DmJdbcDriver18.jar</systemPath> </dependency>二、配置达梦数据库信息与 MySQL 一样,我们也需要配置达梦数据的连接信息,在 application.yml 文件中,配置信息如下:spring: # Mysql配置 datasource: driver-class-name: dm.jdbc.driver.DmDriver url: jdbc:dm://127.0.0.1:5236/TEST?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=UTF-8 # springboot2.0整合了hikari ,据说这是目前性能最好的java数据库连接池 hikari: username: SYSDBA password: SYSDBA idle-timeout: 60000 maximum-pool-size: 30 minimum-idle: 10 max-lifetime: 30000 connection-test-query: SELECT 1与 MySQL 配置信息不同如下:driver之前:com.mysql.cj.jdbc.Driver现在:dm.jdbc.driver.DmDriverurl之前:jdbc:mysql://127.0.0.1:3306/test?现在:jdbc:dm://127.0.0.1:5236/TEST?username之前:root现在:SYSDBApassword之前:123456现在:SYSDBA然后在项目中的使用和 MySQL 完全一致三、自定义 SQL 语句我们在 xml 文件中自己写的 SQL 语句和 MySQL 有些不同,因为达梦数据库是由 Oracle 而来的,在语法上参照 Oracle 语法即可,我说一个我在使用中遇到的错误吧:MySQL 语句:SELECT id, `name`, sex, `status`, create_time FROM sys_user_infoDM8 语句:SELECT ID, NAME, SEX, STATUS, CREATE_TIME FROM SYS_USER_INFO也就是说,在达梦数据库中写 SQL 语句对于 name,status 这种关键字上,不需要加引号,否则会报错MyBatisPlus 针对达梦数据库默认会将表名和列名大写,所以我们不需要做任何改变,即可切换达梦数据库
在连接数据库实例的时候,出现了如下异常信息登录服务器失败,提示:错误号:6001错误信息:网络通信异常1、首先,检查,是否存在此端口的数据库实例,如果没有此端口的实例,肯定连接失败2、检查该实例是否处于 “启动” 状态,具体做法,找到 DM服务查看器工具点击打开、可以看出 DmServiceTEST 处于 “停止” 状态,将其启动即可启动成功,再次连接连接成功
之前文章已经介绍了 MinIO 的环境搭建,已经对文件的上传下载方法,本篇文章一起与大家来学习图片压缩上传的方法1、背景最近客户总抱怨 APP 中图片显示较慢, 升级服务器带宽又没有多的预算。查看原因,是因为现在大家都是用的智能手机拍照,拍出来的照片小则 2-3 M,大则十几 M,所以导致图片显示较慢。思考再三,决定将图片进行压缩再上传图片服务器来解决图片显示慢的问题2、开发前戏1、引入 maven 依赖<!-- 图片压缩 --> <dependency> <groupId>net.coobird</groupId> <artifactId>thumbnailator</artifactId> <version>0.4.8</version> </dependency>本次我们选择了使用 thumbnailator 来作为压缩的工具2、thumbnailator 简介Thumbnailator 是一个用来生成图像缩略图的 Java 类库,通过很简单的代码即可生成图片缩略图,也可直接对一整个目录的图片生成缩略图支持图片缩放,区域裁剪,水印,旋转,保持比例3、压缩前戏判断是否是图片方法/** * 判断文件是否为图片 */ public boolean isPicture(String imgName) { boolean flag = false; if (StringUtils.isBlank(imgName)) { return false; } String[] arr = {"bmp", "dib", "gif", "jfif", "jpe", "jpeg", "jpg", "png", "tif", "tiff", "ico"}; for (String item : arr) { if (item.equals(imgName)) { flag = true; break; } } return flag; }3、压缩上传/** * 上传文件 * * @param file 文件 * @return */ public JSONObject uploadFile(MultipartFile file) throws Exception { JSONObject res = new JSONObject(); res.put("code", 500); // 判断上传文件是否为空 if (null == file || 0 == file.getSize()) { res.put("msg", "上传文件不能为空"); return res; } // 判断存储桶是否存在 if (!client.bucketExists("test")) { client.makeBucket("test"); } // 拿到文件后缀名,例如:png String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".") + 1); // UUID 作为文件名 String uuid = String.valueOf(UUID.randomUUID()); // 新的文件名 String fileName = DateUtils.getYyyymmdd() + "/" + uuid + "." + suffix; /** * 判断是否是图片 * 判断是否超过了 100K */ if (isPicture(suffix) && (1024 * 1024 * 0.1) <= file.getSize()) { // 在项目根目录下的 upload 目录中生成临时文件 File newFile = new File(ClassUtils.getDefaultClassLoader().getResource("upload").getPath() + uuid + "." + suffix); // 小于 1M 的 if ((1024 * 1024 * 0.1) <= file.getSize() && file.getSize() <= (1024 * 1024)) { Thumbnails.of(file.getInputStream()).scale(1f).outputQuality(0.3f).toFile(newFile); } // 1 - 2M 的 else if ((1024 * 1024) < file.getSize() && file.getSize() <= (1024 * 1024 * 2)) { Thumbnails.of(file.getInputStream()).scale(1f).outputQuality(0.2f).toFile(newFile); } // 2M 以上的 else if ((1024 * 1024 * 2) < file.getSize()) { Thumbnails.of(file.getInputStream()).scale(1f).outputQuality(0.1f).toFile(newFile); } // 获取输入流 FileInputStream input = new FileInputStream(newFile); // 转为 MultipartFile MultipartFile multipartFile = new MockMultipartFile("file", newFile.getName(), "text/plain", input); // 开始上传 client.putObject("test", fileName, multipartFile.getInputStream(), file.getContentType()); // 删除临时文件 newFile.delete(); // 返回状态以及图片路径 res.put("code", 200); res.put("msg", "上传成功"); res.put("url", minioProp.getEndpoint() + "/" + "test" + "/" + fileName); } // 不需要压缩,直接上传 else { // 开始上传 client.putObject("test", fileName, file.getInputStream(), file.getContentType()); // 返回状态以及图片路径 res.put("code", 200); res.put("msg", "上传成功"); res.put("url", minioProp.getEndpoint() + "/" + "test" + "/" + fileName); } return res; }这里我们判断了当文件为图片的时候,且当它大小超过了 (1024 * 1024 * 0.1),约为 100K 的时候,才进行压缩我们首先在根目录下的 upload 目录中创建了一个临时文件 newFileThumbnails.of(file.getInputStream()).scale(1f).outputQuality(0.3f).toFile(newFile);将压缩后的文件输出到临时文件中然后将 FileInputStream 转为 MultipartFile 上传最后删除临时文件 newFile.delete();完成图片压缩上传4、测试原图 706K压缩后 120K5、总结综合以上代码,可以看出 Thumbnails 对图片的处理是很方便的,且代码量也非常少通过测试,可以看出压缩后的图片质量也很高thumbnailator 对图片的处理支持全面,缩放,裁剪等
1、获取文件对象我们在 MinIO 工具类中,获取文件对象的方法,即获取文件的输入流对象/** * 获取文件 * * @param bucketName bucket名称 * @param objectName 文件名称 * @return 二进制流 */ @SneakyThrows public InputStream getObject(String bucketName, String objectName) { return client.getObject(bucketName, objectName); }bucketName,是指存储桶的名称objectName,是指文件的路径,即存储桶下文件的相对路径例如,图片的地址为http://127.0.0.1:9000/bucketName/20200806/1596681603481809.png那么 objectName 就为20200806/1596681603481809.png2、下载文件我们需要编写一个 API 来进行访问从而下载文件/** * 获取文件 * * @param bucketName bucket名称 * @param objectName 文件名称 * @return 二进制流 */ @SneakyThrows public InputStream getObject(String bucketName, String objectName) { return client.getObject(bucketName, objectName); } /** * 下载文件 * * @param fileUrl 文件绝对路径 * @param response * @throws IOException */ @GetMapping("downloadFile") public void downloadFile(String fileUrl, HttpServletResponse response) throws IOException { if (StringUtils.isBlank(fileUrl)) { response.setHeader("Content-type", "text/html;charset=UTF-8"); String data = "文件下载失败"; OutputStream ps = response.getOutputStream(); ps.write(data.getBytes("UTF-8")); return; } try { // 拿到文件路径 String url = fileUrl.split("9000/")[1]; // 获取文件对象 InputStream object = minioUtils.getObject(MinioConst.MINIO_BUCKET, url.substring(url.indexOf("/") + 1)); byte buf[] = new byte[1024]; int length = 0; response.reset(); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(url.substring(url.lastIndexOf("/") + 1), "UTF-8")); response.setContentType("application/octet-stream"); response.setCharacterEncoding("UTF-8"); OutputStream outputStream = response.getOutputStream(); // 输出文件 while ((length = object.read(buf)) > 0) { outputStream.write(buf, 0, length); } // 关闭输出流 outputStream.close(); } catch (Exception ex) { response.setHeader("Content-type", "text/html;charset=UTF-8"); String data = "文件下载失败"; OutputStream ps = response.getOutputStream(); ps.write(data.getBytes("UTF-8")); } }这里传入的参数 fileUrl 为文件的绝对路径,即可以直接访问的路径,还需要通过此路径,截取得到文件的相对路径(即去掉 IP 地址和端口,去掉存储桶名称的路径)3、测试通过访问 APIhttp://127.0.0.1/minio/downloadFile?fileUrl=http://127.0.0.1:9000/bucketName/20200806/1596681603481809.png便能成功下载文件了
三、配合数据字典导出上面介绍了数据的简单导出,下面介绍配合数据字典导出数据1、@Excel 注解与上面注解相比,我们需要多加一个属性,dicCode,如下@Excel(name = "性别", width = 15, dicCode = "sex") @ApiModelProperty(value = "性别(0--未知1--男2--女)") @TableField("sex") @Dict(dictCode = "sex") private Integer sex;@Excel(name = “性别”, width = 15, dicCode = “sex”)name:表头width:列宽度dicCode :字典类型这样,我们就为这个字段注入了一个字典类型,这样就能翻译成文本了2、配置类要配合数据字典导出,我们需要配置 autopoi 的配置类 AutoPoiConfig.javaimport org.jeecgframework.core.util.ApplicationContextUtil; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * autopoi 配置类 * * @Author Lizhou */ @Configuration public class AutoPoiConfig { /** * excel注解字典参数支持(导入导出字典值,自动翻译) * 举例: @Excel(name = "性别", width = 15, dicCode = "sex") * 1、导出的时候会根据字典配置,把值1,2翻译成:男、女; * 2、导入的时候,会把男、女翻译成1,2存进数据库; * @return */ @Bean public ApplicationContextUtil applicationContextUtil() { return new org.jeecgframework.core.util.ApplicationContextUtil(); } }3、翻译规则我们可以根据自己项目中的字典翻译规则,来重写 autopoi 的字典翻译规则 AutoPoiDictService.javaimport com.zyxx.sys.entity.SysDictDetail; import com.zyxx.sys.mapper.SysDictDetailMapper; import lombok.extern.slf4j.Slf4j; import org.jeecgframework.dict.service.AutoPoiDictServiceI; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; /** * 描述:AutoPoi Excel注解支持字典参数设置 * 举例: @Excel(name = "性别", width = 15, dicCode = "sex") * 1、导出的时候会根据字典配置,把值1,2翻译成:男、女; * 2、导入的时候,会把男、女翻译成1,2存进数据库; * * @Author lizhou */ @Slf4j @Service public class AutoPoiDictService implements AutoPoiDictServiceI { @Autowired private SysDictDetailMapper sysDictDetailMapper; /** * 通过字典翻译字典文本 * * @Author lizhou */ @Override public String[] queryDict(String dicTable, String dicCode, String dicText) { List<String> dictReplaces = new ArrayList<>(); List<SysDictDetail> dictList = sysDictDetailMapper.queryDictItemsByCode(dicCode); for (SysDictDetail t : dictList) { if (t != null) { dictReplaces.add(t.getName() + "_" + t.getCode()); } } if (dictReplaces != null && dictReplaces.size() != 0) { return dictReplaces.toArray(new String[dictReplaces.size()]); } return null; } }实现了 AutoPoiDictServiceI 接口,重写 queryDict 方法,这里我只使用了 dicCode 来查询字典列表,这样就能配合数据字典导出了4、导出数据导出数据如图所示可以看出,数据已经成功导出,性别、状态等魔法值已经被翻译成文本,这样,我们的字典翻译是成功的四、总结以上介绍了 JeecgBoot 中的 Autopoi 导出 Excel 的方法,还有配合数据字典导出等操作,可以看出,比以往我们使用的 poi、jsxl 使用方便,导出方便,大大提高了我们的工作效率
说到导出 Excel,我们首先会想到 poi、jsxl 等,使用这些工具会显得笨重,学习难度大。今天学习使用 JeecgBoot 中的 Autopoi 导出 Excel,底层基于 easypoi,使用简单,还支持数据字典方式一、开发前戏1、引入 maven 依赖<!-- AutoPoi Excel工具类--> <dependency> <groupId>org.jeecgframework</groupId> <artifactId>autopoi-web</artifactId> <version>1.1.1</version> <exclusions> <exclusion> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </exclusion> </exclusions> </dependency>exclusions 是将 commons-codec 从 autopoi 中排除,避免冲突2、切换 Jeecg 镜像以下代码放在 pom.xml 文件中的 parent 标签下面<repositories> <repository> <id>aliyun</id> <name>aliyun Repository</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>jeecg</id> <name>jeecg Repository</name> <url>http://maven.jeecg.org/nexus/content/repositories/jeecg</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>可以看到,这里我们配置了 aliyun 的国内镜像,还配置了 jeecg 的镜像,这样方便我们下载依赖文件3、导出工具类我们把导出 Excel 通用方法写在 ExcelUtils.java 文件中import org.jeecgframework.poi.excel.def.NormalExcelConstants; import org.jeecgframework.poi.excel.entity.ExportParams; import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; import org.springframework.web.servlet.ModelAndView; import java.util.List; /** * 导出excel工具类 * * @author lizhou */ public class ExcelUtils { /** * 导出excel * * @param title 文件标题 * @param clazz 实体类型 * @param exportList 导出数据 * @param <T> * @return */ public static <T> ModelAndView export(String title, Class<T> clazz, List<T> exportList) { ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); mv.addObject(NormalExcelConstants.FILE_NAME, title); mv.addObject(NormalExcelConstants.CLASS, clazz); mv.addObject(NormalExcelConstants.PARAMS, new ExportParams(title, title)); mv.addObject(NormalExcelConstants.DATA_LIST, exportList); return mv; } }这样我们导出数据的时候,只需要传入文件的标题(标题同样作为表格的标题)、数据类型、数据集合,就可以导出数据了二、开始导出1、给实体类加注解我们将需要导出的实体类或 VO 类中的属性加上注解 @Excelpackage com.zyxx.sys.entity; import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.extension.activerecord.Model; import com.zyxx.common.annotation.Dict; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import org.jeecgframework.poi.excel.annotation.Excel; import java.io.Serializable; /** * <p> * 用户信息表 * </p> * * @author lizhou * @since 2020-07-06 */ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("sys_user_info") @ApiModel(value = "SysUserInfo对象", description = "用户信息表") public class SysUserInfo extends Model<SysUserInfo> { @ApiModelProperty(value = "ID") @TableId(value = "id", type = IdType.AUTO) private Long id; @Excel(name = "账号", width = 15) @ApiModelProperty(value = "登录账号") @TableField("account") private String account; @ApiModelProperty(value = "登录密码") @TableField("password") private String password; @Excel(name = "姓名", width = 15) @ApiModelProperty(value = "姓名") @TableField("name") private String name; @Excel(name = "电话", width = 15) @ApiModelProperty(value = "电话") @TableField("phone") private String phone; @ApiModelProperty(value = "头像") @TableField("avatar") private String avatar; @Excel(name = "性别", width = 15) @ApiModelProperty(value = "性别(0--未知1--男2--女)") @TableField("sex") private Integer sex; @Excel(name = "状态", width = 15) @ApiModelProperty(value = "状态(0--正常1--冻结)") @TableField("status") private Integer status; @Excel(name = "创建时间", width = 30) @ApiModelProperty(value = "创建时间") @TableField("create_time") private String createTime; }@Excel(name = “性别”, width = 15)name:表头width:列宽度导出 Excel 时,只会导出加了 @Excel 注解的字段,不然不会导出2、导出数据@ApiOperation(value = "导出用户信息", notes = "导出用户信息") @GetMapping(value = "/export") public ModelAndView exportXls(SysUserInfo sysUserInfo) { return ExcelUtils.export("用户信息统计报表", SysUserInfo.class, sysUserInfoService.list(1, Integer.MAX_VALUE, sysUserInfo).getData()); }我们传入了文件的标题,类型为 SysUserInfo,传入了数据的集合,这样我们请求这个 API 就能导出数据了可以看出数据已经成功导出,但是性别、状态这些属性值还属于魔法值,我们需要自己写 SQL 来翻译这些值,或者配合数据字典来翻译这些值
四、开发实现1、创建自定义注解我们创建一个自定义注解 @Dict 来实现数据字典import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 数据字典注解 * * @author Tellsea * @date 2020/6/23 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Dict { /** * 字典类型 * * @return */ String dictCode(); /** * 返回属性名 * * @return */ String dictText() default ""; }2、注解实现我们使用 aop 切面来实现什么的自定义注解 @Dictimport com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.zyxx.common.annotation.Dict; import com.zyxx.common.utils.LayTableResult; import com.zyxx.common.utils.ObjConvertUtils; import com.zyxx.sbm.entity.SysDictDetail; import com.zyxx.sbm.service.SysDictService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * 数据字典切面 * * @author Tellsea * @date 2020/6/23 */ @Aspect @Component @Slf4j public class DictAspect { /** * 字典后缀 */ private static String DICT_TEXT_SUFFIX = "Text"; @Autowired private SysDictService sysDictService; /** * 切点,切入 controller 包下面的所有方法 */ @Pointcut("execution( * com.zyxx.*.controller.*.*(..))") public void dict() { } @Around("dict()") public Object doAround(ProceedingJoinPoint pjp) throws Throwable { long time1 = System.currentTimeMillis(); Object result = pjp.proceed(); long time2 = System.currentTimeMillis(); log.debug("获取JSON数据 耗时:" + (time2 - time1) + "ms"); long start = System.currentTimeMillis(); this.parseDictText(result); long end = System.currentTimeMillis(); log.debug("解析注入JSON数据 耗时" + (end - start) + "ms"); return result; } private void parseDictText(Object result) { if (result instanceof LayTableResult) { List<JSONObject> items = new ArrayList<>(); LayTableResult rr = (LayTableResult) result; if (rr.getCount() > 0) { List<?> list = (List<?>) rr.getData(); for (Object record : list) { ObjectMapper mapper = new ObjectMapper(); String json = "{}"; try { // 解决@JsonFormat注解解析不了的问题详见SysAnnouncement类的@JsonFormat json = mapper.writeValueAsString(record); } catch (JsonProcessingException e) { log.error("Json解析失败:" + e); } JSONObject item = JSONObject.parseObject(json); // 解决继承实体字段无法翻译问题 for (Field field : ObjConvertUtils.getAllFields(record)) { //解决继承实体字段无法翻译问题 // 如果该属性上面有@Dict注解,则进行翻译 if (field.getAnnotation(Dict.class) != null) { // 拿到注解的dictDataSource属性的值 String dictType = field.getAnnotation(Dict.class).dictCode(); // 拿到注解的dictText属性的值 String text = field.getAnnotation(Dict.class).dictText(); //获取当前带翻译的值 String key = String.valueOf(item.get(field.getName())); //翻译字典值对应的text值 String textValue = translateDictValue(dictType, key); // DICT_TEXT_SUFFIX的值为,是默认值: // public static final String DICT_TEXT_SUFFIX = "_dictText"; log.debug("字典Val: " + textValue); log.debug("翻译字典字段:" + field.getName() + DICT_TEXT_SUFFIX + ": " + textValue); //如果给了文本名 if (!StringUtils.isBlank(text)) { item.put(text, textValue); } else { // 走默认策略 item.put(field.getName() + DICT_TEXT_SUFFIX, textValue); } } // date类型默认转换string格式化日期 if ("java.util.Date".equals(field.getType().getName()) && field.getAnnotation(JsonFormat.class) == null && item.get(field.getName()) != null) { SimpleDateFormat aDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); item.put(field.getName(), aDate.format(new Date((Long) item.get(field.getName())))); } } items.add(item); } rr.setData(items); } } } /** * 翻译字典文本 * * @param dictType * @param key * @return */ private String translateDictValue(String dictType, String key) { if (ObjConvertUtils.isEmpty(key)) { return null; } StringBuffer textValue = new StringBuffer(); String[] keys = key.split(","); for (String k : keys) { if (k.trim().length() == 0) { continue; } /** * 根据 dictCode 和 code 查询字典值,例如:dictCode:sex,code:1,返回:男 * 应该放在redis,提高响应速度 */ SysDictDetail dictData = sysDictService.getDictDataByTypeAndValue(dictType, key); if (dictData.getName() != null) { if (!"".equals(textValue.toString())) { textValue.append(","); } textValue.append(dictData.getName()); } log.info("数据字典翻译: 字典类型:{},当前翻译值:{},翻译结果:{}", dictType, k.trim(), dictData.getName()); } return textValue.toString(); } }上面用到的 ObjConvertUtils 类import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 对象转换工具类 * * @Author Lizhou */ @SuppressWarnings("ALL") public class ObjConvertUtils { /** * 获取类的所有属性,包括父类 * * @param object * @return */ public static Field[] getAllFields(Object object) { Class<?> clazz = object.getClass(); List<Field> fieldList = new ArrayList<>(); while (clazz != null) { fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields()))); clazz = clazz.getSuperclass(); } Field[] fields = new Field[fieldList.size()]; fieldList.toArray(fields); return fields; } public static boolean isEmpty(Object object) { if (object == null) { return (true); } if ("".equals(object)) { return (true); } if ("null".equals(object)) { return (true); } return (false); } }3、注解使用我们只需要在实体类的属性上加入我们实现的自定义注解即可@ApiModelProperty(value = "性别(0--未知1--男2--女)") @TableField("sex") @Dict(dictCode = "sex") private Integer sex; @ApiModelProperty(value = "状态(0--正常1--冻结)") @TableField("status") @Dict(dictCode = "status") private Integer status;我们对 sex,status 都加入了 @Dict(dictCode = “”) 注解,那么我们在获取用户信息的时候,就能获取到对应的字典值了五、测试1、编写 API 查询我们在 controller 层开放一个 API 实现查询用户列表/** * 分页查询 */ @PostMapping("list") @ResponseBody public LayTableResult list(Integer page, Integer limit, SysUserInfo userInfo) { QueryWrapper<SysUserInfo> queryWrapper = new QueryWrapper<>(); if (StringUtils.isNotBlank(userInfo.getName())) { queryWrapper.like("name", userInfo.getName()); } if (null != userInfo.getSex()) { queryWrapper.eq("sex", userInfo.getSex()); } if (null != userInfo.getStatus()) { queryWrapper.eq("status", userInfo.getStatus()); } queryWrapper.orderByDesc("create_time"); IPage<SysUserInfo> iPage = sysUserInfoService.page(new Page<>(page, limit), queryWrapper); return new LayTableResult<>(iPage.getTotal(), iPage.getRecords()); }注意: 这里我们使用了 LayTableResult 作为相应实体类,与上面我们编写的返回通用实体类是一致的,必须一直,才能实现数据字典功能2、调用 API返回结果如下:{ "code": 0, "msg": null, "count": 3, "data": [{ "id": 2, "account": "15286779045", "name": "周杰伦", "sex": 1, "sexText": "男", "status": 0, "statusText": "正常" }, { "id": 1, "name": "超级管理员", "account": "15286779044", "sex": 1, "sexText": "男", "status": 0, "statusText": "正常" }] }可以看出,返回的数据中,多出了 sexText,statusText,两个属性,也就证明我们的字典功能已经实现成功六、总结1、优点1、在一定程度上,通过系统维护人员即可改变系统的行为(功能),不需要开发人员的介入。使得系统的变化更快,能及时响应客户和市场的需求。2、提高了系统的灵活性、通用性,减少了主体和属性的耦合度 3、简化了主体类的业务逻辑 4、能减少对系统程序的改动,使数据库、程序和页面更稳定。特别是数据量大的时候,能大幅减少开发工作量5、使数据库表结构和程序结构条理上更清楚,更容易理解,在可开发性、可扩展性、可维护性、系统强壮性上都有优势。2、缺点1、数据字典是通用的设计,在系统效率上会低一些。 2、程序算法相对复杂一些。 3、对于开发人员,需要具备一定抽象思维能力,所以对开发人员的要求较高。3、优化我们的数据字典数据应该存放在 redis 中,减少与数据库的交互次数,提高响应速度
一、简介1、定义 数据字典是指对数据的数据项、数据结构、数据流、数据存储、处理逻辑等进行定义和描述,其目的是对数据流程图中的各个元素做出详细的说明,使用数据字典为简单的建模项目。简而言之,数据字典是描述数据的信息集合,是对系统中使用的所有数据元素的定义的集合。 数据字典(Data dictionary)是一种用户可以访问的记录数据库和应用程序元数据的目录。主动数据字典是指在对数据库或应用程序结构进行修改时,其内容可以由DBMS自动更新的数据字典。被动数据字典是指修改时必须手工更新其内容的数据字典。2、理解数据字典是一种通用的程序设计思想,将主体与分支存于两张数据表中,他们之间靠着唯一的 code 相互联系,且 code 是唯一存在的,分支依附主体而存在,每一条分支都有它唯一对应的属性值例如:性别(sex),分为(0–保密1–男2–女),那么数据字典的设计就应该是主表:{ "code": "sex", "name": "性别" }副表:[{ "dictCode": "sex", "code": "0", "text": "保密" }, { "dictCode": "sex", "code": "1", "text": "男" }, { "dictCode": "sex", "code": "2", "text": "女" } ]那么我们在使用数据字典的时候,只需要知道 dictCode,再使用 code 找到唯一的字典值二、数据表设计1、数据表设计主表:drop table if exists sys_dict; /*==============================================================*/ /* Table: sys_dict */ /*==============================================================*/ create table sys_dict ( id bigint(20) not null auto_increment comment '主键id', code varchar(32) comment '编码', name varchar(32) comment '名称', descript varchar(64) comment '描述', status tinyint(1) default 0 comment '状态(0--正常1--冻结)', create_time datetime comment '创建时间', create_user bigint(20) comment '创建人', del_flag tinyint(1) default 0 comment '删除状态(0,正常,1已删除)', primary key (id) ) type = InnoDB; alter table sys_dict comment '字典管理表'; 副表:drop table if exists sys_dict_detail; /*==============================================================*/ /* Table: sys_dict_detail */ /*==============================================================*/ create table sys_dict_detail ( id bigint(20) not null comment '主键id', dict_code varchar(32) comment '字典编码', code varchar(32) comment '编码', name varchar(32) comment '名称', primary key (id) ) type = InnoDB; alter table sys_dict_detail comment '字典配置表'; 它们的关系如图所示:2、数据字典配置三、开发前戏1、引入 maven 依赖<!-- web支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- thymeleaf模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- aop依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- lombok插件 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>我们引入了 aop 切面所需依赖,我们的数据字典也是基于 aop 切面实现的2、创建实体类用户信息表 SysUserInfo.java:import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.extension.activerecord.Model; import com.zyxx.common.annotation.Dict; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import java.io.Serializable; /** * <p> * 用户信息表 * </p> * * @author lizhou * @since 2020-07-06 */ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) @TableName("sys_user_info") @ApiModel(value="SysUserInfo对象", description="用户信息表") public class SysUserInfo extends Model<SysUserInfo> { @ApiModelProperty(value = "ID") @TableId(value = "id", type = IdType.AUTO) private Long id; @ApiModelProperty(value = "登录账号") @TableField("account") private String account; @ApiModelProperty(value = "登录密码") @TableField("password") private String password; @ApiModelProperty(value = "姓名") @TableField("name") private String name; @ApiModelProperty(value = "性别(0--未知1--男2--女)") @TableField("sex") @Dict(dictCode = "sex") private Integer sex; @ApiModelProperty(value = "状态(0--正常1--冻结)") @TableField("status") @Dict(dictCode = "status") private Integer status; }3、返回结果通用实体类返回结果通用实体类 LayTableResult.java:import lombok.Getter; import lombok.Setter; import java.util.List; /** * @param <T> 返回的实体类 * @author lizhou * @描述 后台返回给LayUI的数据格式 */ @Getter @Setter public class LayTableResult<T> { /** * 接口状态 */ private Integer code; /** * 提示信息 */ private String msg; /** * 接口数据长度 */ private Long count; /** * 接口数据 */ private List<T> data; /** * 无参构造函数 */ public LayTableResult() { super(); } /** * 返回数据给表格 */ public LayTableResult(Long count, List<T> data) { super(); this.count = count; this.data = data; this.code = 0; } }由于我用的是 layui 前端框架,我写了一个返给 layui 表格的通用实体类,这是在实现数据字典需要用到的,判断响应返回实体类的类型来判断是否需要注入字典
执行SQL报错:The used SELECT statements have a different number of columns以上翻译:使用的SELECT语句具有不同数量的列原因:我们在 SQL 语句中使用了 UNION 连接两张表时,查询字段数量不一致导致# 效果展示:我们需要将数据展示如上图所示# 错误案例:SELECT a.quantity AS in_quantity, a.price AS in_price, (a.quantity * a.price) AS in_amount, 0 AS out_quantity, 0 AS out_price, 0 AS out_amount FROM store_in_detail a WHERE a.sku_id = 1345 UNION ALL SELECT b.quantity AS out_quantity, b.price AS out_price, (b.quantity * b.price) AS out_amount FROM store_out_detail b WHERE b.sku_id = 1345我们通过入库表 连接 出库表,得出商品 id = 1345 的出入库情况执行SQL报错:The used SELECT statements have a different number of columns (使用的SELECT语句具有不同数量的列)# 原因分析:我们在查询入库单,查询了四个字段:入库数量,入库单价,入库金额,出库数量(默认0),出库单价(默认0),出库金额(默认0)而查询出库单,只查询了两个字段:出库数量,出库单价,出库金额两次查询的字段数量不一致,导致 SQL 异常# 正确实例:SELECT a.quantity AS in_quantity, a.price AS in_price, (a.quantity * a.price) AS in_amount, 0 AS out_quantity, 0 AS out_price, 0 AS out_amount FROM store_in_detail a WHERE a.sku_id = 1345 UNION ALL SELECT 0 AS in_quantity, 0 AS in_price, 0 AS in_amount, b.quantity AS out_quantity, b.price AS out_price, (b.quantity * b.price) AS out_amount FROM store_out_detail b WHERE b.sku_id = 1345SQL 执行成功
一、准备工作1、登录 百度开发者中心官网地址:https://developer.baidu.com/注册账号,登录官网2、注册成为“百度开发者”在页面底部找到“应用管理”,当然,我们还需要申请成为“百度开发者”,填写信息点击提交后,3、创建应用填入应用的名称我们点击“安全设置”,填写应用高级信息4、将应用信息保存到项目中由于我使用的是 SpringBoot 项目,我放在了 application.yml 文件中二、开始开发1、引入 Maven 依赖<!-- 网络请求 --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.6</version> </dependency> <!-- alibaba的fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.51</version> </dependency>这里主要用到了网络请求,已经 JSON 转换工具,其余的依赖请自行加入2、在页面放置 “百度(baidu)” 授权登录的 DOM 元素<a th:href="@{baidu/auth}" class="link" title="百度登录"><i class="iconfont icon-baidu"></i></a>这里使用的是阿里的 iconfont 图标三、接口类创建 “百度(baidu)” 授权登录的 Controller,BaiduController.java1、从配置文件中获取 “baidu” 配置信息/** * 百度授权中提供的 apikey 和 secretkey */ @Value("${baidu.oauth.apikey}") public String APIKEY; @Value("${baidu.oauth.secretkey}") public String SECRETKEY; @Value("${baidu.oauth.callback}") public String URL;2、页面登录按钮点击后的接口/** * 请求授权页面 */ @GetMapping(value = "/auth") public String qqAuth(HttpSession session) { // 用于第三方应用防止CSRF攻击 String uuid = UUID.randomUUID().toString().replaceAll("-", ""); session.setAttribute("state", uuid); // Step1:获取Authorization Code String url = "http://openapi.baidu.com/oauth/2.0/authorize?response_type=code" + "&client_id=" + APIKEY + "&redirect_uri=" + URLEncoder.encode(URL) + "&state=" + uuid + "&scope=basic,super_msg"; return PasswordUtils.redirectTo(url); }注意:scope 参数的值可多选basic:用户基本权限,可以获取用户的基本信息 。 super_msg:往用户的百度首页上发送消息提醒,相关API任何应用都能使用,但要想将消息提醒在百度首页显示,需要第三方在注册应用时额外填写相关信息。 netdisk:获取用户在个人云存储中存放的数据。多个值用逗号隔开即可接口文档中建议我们在授权登录时传入一个加密的数据防止被攻击,我们传入了UUID,最后重定向到授权页面3、当该用户点击“授权”按钮,同意授权后,就会回调到我们在应用中填写的回调地址里去/** * 授权回调 */ @GetMapping(value = "/callback") public String qqCallback(HttpServletRequest request) throws Exception { HttpSession session = request.getSession(); // 得到Authorization Code String code = request.getParameter("code"); // 我们放在地址中的状态码 String state = request.getParameter("state"); String uuid = (String) session.getAttribute("state"); // 验证信息我们发送的状态码 if (null != uuid) { // 状态码不正确,直接返回登录页面 if (!uuid.equals(state)) { return PasswordUtils.redirectTo("/login"); } } // Step2:通过Authorization Code获取Access Token String url = "https://openapi.baidu.com/oauth/2.0/token?grant_type=authorization_code" + "&code=" + code + "&client_id=" + APIKEY + "&client_secret=" + SECRETKEY + "&redirect_uri=" + URL; JSONObject accessTokenJson = BaiduHttpClient.getAccessToken(url); // Step3: 获取用户信息 url = "https://openapi.baidu.com/rest/2.0/passport/users/getInfo?access_token=" + accessTokenJson.get("access_token"); JSONObject jsonObject = BaiduHttpClient.getUserInfo(url); /** * TODO 获取到用户信息之后,你自己的业务逻辑 * 判断数据库是否有次用户,有---登录成功,无---保存用户至数据库,登录成功 */ return PasswordUtils.redirectTo("/success"); }四、网络请求方法上面回调方法中所用到的网络接口方法,我放在了 BaiduHttpClient.java 文件中,主要有两个方法1、使用 code 获取Access Token/** * 获取Access Token * post */ public static JSONObject getAccessToken(String url) throws IOException { HttpClient client = HttpClients.createDefault(); HttpPost httpPost = new HttpPost(url); HttpResponse response = client.execute(httpPost); HttpEntity entity = response.getEntity(); if (null != entity) { String result = EntityUtils.toString(entity, "UTF-8"); return JSONObject.parseObject(result); } httpPost.releaseConnection(); return null; }2、使用 Access Token 获取用户信息/** * 获取用户信息 * get */ public static JSONObject getUserInfo(String url) throws IOException { CloseableHttpClient client = HttpClients.createDefault(); HttpGet httpGet = new HttpGet(url); HttpResponse response = client.execute(httpGet); HttpEntity entity = response.getEntity(); if (entity != null) { String result = EntityUtils.toString(entity, "UTF-8"); return JSONObject.parseObject(result); } httpGet.releaseConnection(); return null; }最终我们获取到一个 JSON 对象,该对象包含了用户的信息,例如:id,username,birthday,sex等等。返回用户详情信息,详情见 返回用户详细资料API文档https://developer.baidu.com/wiki/index.php?title=docs/oauth/rest/file_data_apis_list#.E8.BF.94.E5.9B.9E.E7.94.A8.E6.88.B7.E8.AF.A6.E7.BB.86.E8.B5.84.E6.96.993、官方 OAuth API 文档https://developer.baidu.com/wiki/index.php?title=docs/oauth/authorization五、总结该授权认证过程符合 OAuth2 认证基本流程,对于应用而言,其流程由获取Authorization Code和通过Authorization Code获取Access Token这2步组成,如图所示:
一、准备工作1、登录 码云官网官网地址:https://gitee.com/注册、登录我们的账号2、创建应用在右上角菜单找到 “设置” 选项在 “安全设置” 下找到 “第三方应用”点击 “创建应用” 开始创建第三方应用按照要求填写应用信息即可3、将应用信息保存到项目中由于我使用的是 SpringBoot 项目,我放在了 application.yml 文件中二、开始开发1、引入 Maven 依赖<!-- 网络请求 --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.6</version> </dependency> <!-- alibaba的fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.51</version> </dependency>其余的依赖请自行加入2、在页面放置 “码云(Gitee)” 授权登录的 DOM 元素<a th:href="@{gitee/auth}" class="link" title="Gitee登录"><i class="iconfont icon-gitee"></i></a>这里使用的是阿里的 iconfont 图标三、接口类创建 “码云(Gitee)” 授权登录的 Controller,GiteeController.java1、从配置文件中获取 “码云(Gitee)” 配置信息/** * gitee授权中提供的 appid 和 appkey */ @Value("${gitee.oauth.clientid}") public String CLIENTID; @Value("${gitee.oauth.clientsecret}") public String CLIENTSECRET; @Value("${gitee.oauth.callback}") public String URL;2、页面登录按钮点击后的接口/** * 请求授权页面 */ @GetMapping(value = "/auth") public String qqAuth(HttpSession session) { // 用于第三方应用防止CSRF攻击 String uuid = UUID.randomUUID().toString().replaceAll("-", ""); session.setAttribute("state", uuid); // Step1:获取Authorization Code String url = "https://gitee.com/oauth/authorize?response_type=code" + "&client_id=" + CLIENTID + "&redirect_uri=" + URLEncoder.encode(URL) + "&state=" + uuid + "&scope=user_info"; return PasswordUtils.redirectTo(url); }接口文档中建议我们在授权登录时传入一个加密的数据防止被攻击,我们传入了UUID,最后重定向到授权页面3、当该用户点击“授权”按钮,同意授权后,就会回调到我们在应用中填写的回调地址里去/** * 授权回调 */ @GetMapping(value = "/callback") public String qqCallback(HttpServletRequest request) throws Exception { HttpSession session = request.getSession(); // 得到Authorization Code String code = request.getParameter("code"); // 我们放在地址中的状态码 String state = request.getParameter("state"); String uuid = (String) session.getAttribute("state"); // 验证信息我们发送的状态码 if (null != uuid) { // 状态码不正确,直接返回登录页面 if (!uuid.equals(state)) { return PasswordUtils.redirectTo("/login"); } } // Step2:通过Authorization Code获取Access Token String url = "https://gitee.com/oauth/token?grant_type=authorization_code" + "&client_id=" + CLIENTID + "&client_secret=" + CLIENTSECRET + "&code=" + code + "&redirect_uri=" + URL; JSONObject accessTokenJson = GiteeHttpClient.getAccessToken(url); // Step3: 获取用户信息 url = "https://gitee.com/api/v5/user?access_token=" + accessTokenJson.get("access_token"); JSONObject jsonObject = GiteeHttpClient.getUserInfo(url); /** * 获取到用户信息之后,就该写你自己的业务逻辑了 */ return PasswordUtils.redirectTo("/success"); }四、网络请求方法上面回调方法中所用到的网络接口方法,我放在了 GiteeHttpClient.java 文件中,主要有两个方法1、网络接口/** * 获取Access Token * post */ public static JSONObject getAccessToken(String url) throws IOException { HttpClient client = HttpClients.createDefault(); HttpPost httpPost = new HttpPost(url); httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"); HttpResponse response = client.execute(httpPost); HttpEntity entity = response.getEntity(); if (null != entity) { String result = EntityUtils.toString(entity, "UTF-8"); return JSONObject.parseObject(result); } httpPost.releaseConnection(); return null; } /** * 获取用户信息 * get */ public static JSONObject getUserInfo(String url) throws IOException { JSONObject jsonObject = null; CloseableHttpClient client = HttpClients.createDefault(); HttpGet httpGet = new HttpGet(url); httpGet.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"); HttpResponse response = client.execute(httpGet); HttpEntity entity = response.getEntity(); if (entity != null) { String result = EntityUtils.toString(entity, "UTF-8"); jsonObject = JSONObject.parseObject(result); } httpGet.releaseConnection(); return jsonObject; }分别就是使用 code 获取 token,在使用 token 获取 用户信息注意:我们需要在请求时加上请求头User-Agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.362、官方 OAuth 文档https://gitee.com/api/v5/oauth_doc#/五、总结该授权认证过程符合 OAuth2 认证基本流程,流程如下:1、用户点击页面登录按钮,请求授权页面,用户在此页面登录账号并同意授权2、用户同意授权后,回调至我们项目中,首先验证 state 是否一致3、使用上一步拿到的 code 请求 access_token4、使用 access_token 请求 用户信息,完成授权登录过程
2022年11月