commit 98c057de11d989a9d6a02fb91ddb94c8d4cecc26 Author: Eric <01714308@yto.net.cn> Date: Fri Jan 16 18:51:16 2026 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82490ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/vcs.xml +.idea/misc.xml +.idea/encodings.xml +.idea/CoolRequestCommonStatePersistent.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +### cache file ### +/apolloConfig/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100644 index 0000000..1f822d8 --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,15 @@ + + + + 10.26.1 + JavaOnly + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/demo/lingniu-framework-demo/pom.xml b/demo/lingniu-framework-demo/pom.xml new file mode 100644 index 0000000..0b75550 --- /dev/null +++ b/demo/lingniu-framework-demo/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + + + lingniu-framework-demo + jar + + + + cn.lingniu.framework + lingniu-framework-plugin-apollo + + + cn.lingniu.framework + lingniu-framework-plugin-redisson + + + cn.lingniu.framework + lingniu-framework-plugin-jetcache + + + + cn.lingniu.framework + lingniu-framework-plugin-xxljob + + + + cn.lingniu.framework + lingniu-framework-plugin-mybatis + + + + com.h2database + h2 + 2.1.214 + + + + cn.lingniu.framework + lingniu-framework-plugin-rocketmq + + + + + cn.lingniu.framework + lingniu-framework-plugin-easyexcel + + + + + cn.lingniu.framework + lingniu-framework-plugin-prometheus + + + + + cn.lingniu.framework + lingniu-framework-plugin-web + + + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-common + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-nacos + + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + com.alibaba + fastjson + + + ch.qos.logback + logback-classic + + + + + + + diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/DemoApplication.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/DemoApplication.java new file mode 100644 index 0000000..1f9f6b9 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/DemoApplication.java @@ -0,0 +1,16 @@ +package cn.lingniu.demo; + +import com.alicp.jetcache.anno.config.EnableMethodCache; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@ComponentScan(basePackages = {"cn.lingniu.*"}) +//jetcache 注解 +@EnableMethodCache(basePackages = "cn.lingniu") +@SpringBootApplication +public class DemoApplication { + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloConfigChangeDemoListener.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloConfigChangeDemoListener.java new file mode 100644 index 0000000..d57e4a6 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloConfigChangeDemoListener.java @@ -0,0 +1,31 @@ +package cn.lingniu.demo.apollo; + +import com.ctrip.framework.apollo.model.ConfigChange; +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +@Slf4j +public class ApolloConfigChangeDemoListener { + + + @ApolloConfigChangeListener() + public void onChange(ConfigChangeEvent changeEvent) { + for (String changedKey : changeEvent.changedKeys()) { + ConfigChange changeInfo = changeEvent.getChange(changedKey); + if(StringUtils.isEmpty(changeInfo)){continue;} + log.info("apollo 配置变更 \r\n\t命名空间:{} \r\n\t属性:{} \r\n\t原值:{}\r\n\t新值:{}\r\n\t变更类型:{}" + ,changeInfo.getNamespace() + ,changeInfo.getPropertyName() + ,changeInfo.getOldValue() + ,changeInfo.getNewValue() + ,changeInfo.getChangeType() + ); + } + } + + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloController.java new file mode 100644 index 0000000..a62a99b --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ApolloController.java @@ -0,0 +1,26 @@ +package cn.lingniu.demo.apollo; + +import com.alibaba.fastjson.JSONObject; +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(value = "/demo/apollo") +public class ApolloController { + + + @Autowired + private ObjectDemoConfig objectDemoConfig; + + @GetMapping("/apolloDemoConfig") + public ObjectDemoConfig apolloDemoConfig() { + System.out.println("apolloDemoConfig:" + JSONObject.toJSONString(objectDemoConfig)); + return objectDemoConfig; + } + + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjcetDemoConfiguration.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjcetDemoConfiguration.java new file mode 100644 index 0000000..1052819 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjcetDemoConfiguration.java @@ -0,0 +1,15 @@ +package cn.lingniu.demo.apollo; + +import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableApolloConfig +public class ObjcetDemoConfiguration { + + @Bean + public ObjectDemoConfig apolloDemoConfig() { + return new ObjectDemoConfig(); + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjectDemoConfig.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjectDemoConfig.java new file mode 100644 index 0000000..f697aa4 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/apollo/ObjectDemoConfig.java @@ -0,0 +1,39 @@ +package cn.lingniu.demo.apollo; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +@Data +@ConfigurationProperties(prefix = "apollo.demo") +public class ObjectDemoConfig implements Serializable { + + private String stringDemo = ""; + + private Boolean booleanDemo = true; + + private List listDemo = Lists.newArrayList(); + + private Set setDemo = Sets.newHashSet(); + + private Map hashDemo = Maps.newHashMap(); + + private Map hashObjectDemo = Maps.newHashMap(); + + private ObjectDemo objectDemo = new ObjectDemo(); + + + @Data + public static class ObjectDemo { + private String testObjcet; + } + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperController.java new file mode 100644 index 0000000..03e51a2 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperController.java @@ -0,0 +1,49 @@ +package cn.lingniu.demo.db; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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; + +import java.util.Date; + +@RestController +@RequestMapping("/demo/db") +@Slf4j +public class JobZookeeperController { + + @Autowired + JobZookeeperServerMapper jobZookeeperServerMapper; + + @GetMapping("/page") + public Page getJobZookeeperServerPage() { + Page jobZookeeperServerPage = jobZookeeperServerMapper.selectPage(Page.of(1, 1), + new LambdaQueryWrapper().select(JobZookeeperServer::getName, JobZookeeperServer::getAddress, JobZookeeperServer::getDigest, + JobZookeeperServer::getJmxEnable, JobZookeeperServer::getJmxPort, JobZookeeperServer::getMessage, JobZookeeperServer::getCreateTime, + JobZookeeperServer::getUpdateTime, JobZookeeperServer::getStatus, JobZookeeperServer::getContactPhone, JobZookeeperServer::getUserName)); + log.info("jobZookeeperServerPage:{}", jobZookeeperServerPage); + return jobZookeeperServerPage; + } + + @GetMapping("/add") + public JobZookeeperServer addJobZookeeperServer() { + JobZookeeperServer server = new JobZookeeperServer(); + server.setName("test"); + server.setAddress("127.0.0.1:2181"); + server.setDigest(""); + server.setJmxEnable(1); + server.setJmxPort(""); + server.setMessage(""); + server.setCreateTime(new Date()); + server.setUpdateTime(new Date()); + server.setStatus(1); + server.setContactPhone(""); + server.setUserName(""); + jobZookeeperServerMapper.insert(server); + log.info("Added new JobZookeeperServer: {}", server); + return server; + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServer.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServer.java new file mode 100644 index 0000000..5308261 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServer.java @@ -0,0 +1,60 @@ +package cn.lingniu.demo.db; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + + +/** + * h2 建表语句: + + CREATE TABLE job_zk_namespace ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255), + address VARCHAR(255), + digest VARCHAR(255), + jmx_enable INTEGER DEFAULT 1, + jmx_port VARCHAR(255), + message VARCHAR(255), + create_time TIMESTAMP, + update_time TIMESTAMP, + status INTEGER DEFAULT 1, + contact_phone VARCHAR(255), + user_name VARCHAR(255) + ); + + */ +@Data +@TableName("job_zk_namespace") +public class JobZookeeperServer implements Serializable { + + @TableId(type = IdType.AUTO) + private Long id; + + private String name; + + private String address; + + private String digest; + + private Integer jmxEnable = 1; + + private String jmxPort; + + private String message; + + private Date createTime; + + private Date updateTime; + + private Integer status = 1; + + private String contactPhone; + + private String userName; + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServerMapper.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServerMapper.java new file mode 100644 index 0000000..4356382 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/db/JobZookeeperServerMapper.java @@ -0,0 +1,8 @@ +package cn.lingniu.demo.db; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface JobZookeeperServerMapper extends BaseMapper { +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/DemoData.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/DemoData.java new file mode 100644 index 0000000..0de9a0c --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/DemoData.java @@ -0,0 +1,26 @@ +package cn.lingniu.demo.file; + +import com.alibaba.excel.annotation.ExcelIgnore; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +@Getter +@Setter +@EqualsAndHashCode +public class DemoData { + @ExcelProperty("字符串标题") + private String string; + @ExcelProperty("日期标题") + private Date date; + @ExcelProperty("数字标题") + private Double doubleData; + /** + * 忽略这个字段 + */ + @ExcelIgnore + private String ignore; +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileController.java new file mode 100644 index 0000000..f28e9d2 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileController.java @@ -0,0 +1,79 @@ +package cn.lingniu.demo.file; + + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.util.ListUtils; +import com.alibaba.excel.write.metadata.WriteSheet; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Date; +import java.util.List; + +/** + * todo https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write + * easyexcel 性能好,api简单,上手更简单 + */ +@Slf4j +@RestController +@RequestMapping("/demo/file") +public class FileController { + + @GetMapping("/test1") + public String test1() { + simpleWrite(); + return "success"; + } + + /** + * 最简单的写 + *

+ * 1. 创建excel对应的实体对象 参照{@link DemoData} + *

+ * 2. 直接写即可 + */ + public void simpleWrite() { + // 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入 + + // 写法1 JDK8+ + // since: 3.0.0-beta1 + String fileName = FileTestUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx"; + // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 + // 如果这里想使用03 则 传入excelType参数即可 + EasyExcel.write(fileName, DemoData.class) + .sheet("模板") + .doWrite(() -> { + // 分页查询数据 + return data(); + }); + + // 写法2 + fileName = FileTestUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx"; + // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 + // 如果这里想使用03 则 传入excelType参数即可 + EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data()); + + // 写法3 + fileName = FileTestUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx"; + // 这里 需要指定写用哪个class去写 + try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) { + WriteSheet writeSheet = EasyExcel.writerSheet("模板").build(); + excelWriter.write(data(), writeSheet); + } + } + + private List data() { + List list = ListUtils.newArrayList(); + for (int i = 0; i < 10; i++) { + DemoData data = new DemoData(); + data.setString("字符串" + i); + data.setDate(new Date()); + data.setDoubleData(0.56); + list.add(data); + } + return list; + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileTestUtil.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileTestUtil.java new file mode 100644 index 0000000..849b020 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/file/FileTestUtil.java @@ -0,0 +1,76 @@ +package cn.lingniu.demo.file; + +import org.springframework.util.CollectionUtils; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +public class FileTestUtil { + + public static InputStream getResourcesFileInputStream(String fileName) { + return Thread.currentThread().getContextClassLoader().getResourceAsStream("" + fileName); + } + + public static String getPath() { + return FileTestUtil.class.getResource("/").getPath(); + } + + public static TestPathBuild pathBuild() { + return new TestPathBuild(); + } + + public static File createNewFile(String pathName) { + File file = new File(getPath() + pathName); + if (file.exists()) { + file.delete(); + } else { + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + } + return file; + } + + public static File readFile(String pathName) { + return new File(getPath() + pathName); + } + + public static File readUserHomeFile(String pathName) { + return new File(System.getProperty("user.home") + File.separator + pathName); + } + + /** + * build to test file path + **/ + public static class TestPathBuild { + private TestPathBuild() { + subPath = new ArrayList<>(); + } + + private final List subPath; + + public TestPathBuild sub(String dirOrFile) { + subPath.add(dirOrFile); + return this; + } + + public String getPath() { + if (CollectionUtils.isEmpty(subPath)) { + return FileTestUtil.class.getResource("/").getPath(); + } + if (subPath.size() == 1) { + return FileTestUtil.class.getResource("/").getPath() + subPath.get(0); + } + StringBuilder path = new StringBuilder(FileTestUtil.class.getResource("/").getPath()); + path.append(subPath.get(0)); + for (int i = 1; i < subPath.size(); i++) { + path.append(File.separator).append(subPath.get(i)); + } + return path.toString(); + } + + } + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/User.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/User.java new file mode 100644 index 0000000..bb7b6ad --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/User.java @@ -0,0 +1,24 @@ +package cn.lingniu.demo.jetcache.annotationDemo; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class User implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + private String name; + private String email; + + public User() { + } + + public User(Long id, String name, String email) { + this.name = name; + this.email = email; + this.id = id; + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserDao.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserDao.java new file mode 100644 index 0000000..1421ea7 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserDao.java @@ -0,0 +1,53 @@ +package cn.lingniu.demo.jetcache.annotationDemo; + +import org.springframework.stereotype.Repository; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Repository +public class UserDao { + + // 模拟数据库存储 + private final ConcurrentHashMap userDatabase = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(1); + + /** + * 根据ID获取用户信息,使用缓存 + * @param id 用户ID + * @return 用户信息 + */ + public User getUserById(Long id) { + return userDatabase.get(id); + } + + /** + * 创建新用户 + * @param user 用户信息 + * @return 创建后的用户信息 + */ + public User createUser(User user) { + Long id = idGenerator.getAndIncrement(); + user.setId(id); + userDatabase.put(id, user); + return user; + } + + /** + * 更新用户信息并更新缓存 + * @param user 更新后的用户信息 + * @return 更新后的用户信息 + */ + public User updateUser(User user) { + userDatabase.put(user.getId(), user); + return user; + } + + /** + * 删除用户并清除缓存 + * @param id 用户ID + */ + public void deleteUser(Long id) { + userDatabase.remove(id); + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserService.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserService.java new file mode 100644 index 0000000..1ba6099 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/annotationDemo/UserService.java @@ -0,0 +1,59 @@ +package cn.lingniu.demo.jetcache.annotationDemo; + +import com.alicp.jetcache.anno.CacheInvalidate; +import com.alicp.jetcache.anno.CacheType; +import com.alicp.jetcache.anno.CacheUpdate; +import com.alicp.jetcache.anno.Cached; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + + @Autowired + private UserDao userDao; + + /** + * 根据ID获取用户信息,使用缓存 + * @param id 用户ID + * @return 用户信息 + */ + @Cached(area = "ca2", name = "user:", key = "#id", expire = 3600, localExpire = 600, cacheType = CacheType.BOTH) + public User getUserById(Long id) { + // 模拟数据库查询延迟 + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return userDao.getUserById(id); + } + + /** + * 更新用户信息并更新缓存 + * @param user 更新后的用户信息 + * @return 更新后的用户信息 + */ + @CacheUpdate(area = "ca2", name = "user:", key = "#user.id", value = "#user") + public User updateUser(User user) { + userDao.updateUser(user); + return user; + } + + /** + * 删除用户并清除缓存 + * @param id 用户ID + */ + @CacheInvalidate(area = "ca2", name = "user:", key = "#id") + public void deleteUser(Long id) { + userDao.deleteUser(id); + } + + + + public User createUser(User user) { + return userDao.createUser(user); + } + + +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/JetCacheController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/JetCacheController.java new file mode 100644 index 0000000..56ddc07 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/JetCacheController.java @@ -0,0 +1,71 @@ +package cn.lingniu.demo.jetcache.controller; + +import cn.lingniu.demo.jetcache.annotationDemo.User; +import cn.lingniu.demo.jetcache.annotationDemo.UserService; +import com.alicp.jetcache.Cache; +import com.alicp.jetcache.SimpleCacheManager; +import com.alicp.jetcache.anno.CachePenetrationProtect; +import com.alicp.jetcache.anno.CacheRefresh; +import com.alicp.jetcache.anno.CacheType; +import com.alicp.jetcache.anno.Cached; +import com.alicp.jetcache.template.QuickConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +@Slf4j +@RestController +@RequestMapping("/demo/cache") +public class JetCacheController { + + @Resource + private SimpleCacheManager jcCacheManager; + @Resource + private UserService userService; //todo 可以直接使用 + + @GetMapping + public String cache( + @RequestParam String localArea, + @RequestParam String remoteArea) { + Cache localCache = jcCacheManager.getOrCreateCache(QuickConfig.newBuilder(localArea, "user:").cacheType(CacheType.LOCAL).build()); + localCache.PUT("key1", "value3"); + Object value = localCache.GET("key1").getValue(); + log.info("value: {}", value); + + Cache remoteCache = jcCacheManager.getOrCreateCache(QuickConfig.newBuilder(remoteArea, "user:").build()); + remoteCache.PUT("key1", "value4"); + Object value1 = remoteCache.GET("key1").getValue(); + log.info("value1: {}", value1); + + return "success"; + } + + @GetMapping("/anno") + @CachePenetrationProtect + @Cached(name = "orderCache:", key = "#orderId", expire = 10, cacheType = CacheType.BOTH) + @CacheRefresh(refresh = 10, timeUnit = TimeUnit.MINUTES) + public KeyValueEntity getOrderById(String orderId) { + KeyValueEntity keyValueEntity = new KeyValueEntity(); + keyValueEntity.setId(orderId); + keyValueEntity.setKey("key"); + keyValueEntity.setValue("value"); + return keyValueEntity; + } + + + + @GetMapping("/userTest") + public String userTest() { + userService.createUser(new User(1L, "create", "create" )); + User temp1 = userService.getUserById(1L); + userService.updateUser(new User(1L, "update", "update")); + User temp2 = userService.getUserById(1L); + userService.deleteUser(1L); + return "success"; + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/KeyValueEntity.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/KeyValueEntity.java new file mode 100644 index 0000000..dd35c90 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/jetcache/controller/KeyValueEntity.java @@ -0,0 +1,18 @@ +package cn.lingniu.demo.jetcache.controller; + +import lombok.Data; + +import java.io.Serializable; + +/** + * + */ +@Data +public class KeyValueEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + String id; + String key; + String value; +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/job/SampleXxlJob.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/job/SampleXxlJob.java new file mode 100644 index 0000000..a5bca66 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/job/SampleXxlJob.java @@ -0,0 +1,63 @@ +package cn.lingniu.demo.job; + +import com.xxl.job.core.context.XxlJobHelper; +import com.xxl.job.core.handler.annotation.XxlJob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * XxlJob开发示例(Bean模式) + *

+ * 开发步骤: + * 1、任务开发:在Spring Bean实例中,开发Job方法; + * 2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。 + * 3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志; + * 4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果; + * + */ +@Component +public class SampleXxlJob { + private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class); + + + /** + * 1、简单任务示例(Bean模式) + */ + @XxlJob("demoJobHandler") + public void demoJobHandler() throws Exception { + XxlJobHelper.log("XXL-JOB, Hello World."); + + for (int i = 0; i < 5; i++) { + XxlJobHelper.log("beat at:" + i); + TimeUnit.SECONDS.sleep(2); + } + // default success + } + + + /** + * 2、分片广播任务 + */ + @XxlJob("shardingJobHandler") + public void shardingJobHandler() throws Exception { + + // 分片参数 + int shardIndex = XxlJobHelper.getShardIndex(); + int shardTotal = XxlJobHelper.getShardTotal(); + + XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal); + + // 业务逻辑 + for (int i = 0; i < shardTotal; i++) { + if (i == shardIndex) { + XxlJobHelper.log("第 {} 片, 命中分片开始处理", i); + } else { + XxlJobHelper.log("第 {} 片, 忽略", i); + } + } + + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/CloudController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/CloudController.java new file mode 100644 index 0000000..2da8cd3 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/CloudController.java @@ -0,0 +1,30 @@ +package cn.lingniu.demo.microservice; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/demo/cloud") +public class CloudController { + + @Autowired + private DemoFeignClient demoFeignClient; + + @GetMapping("/contributors") + public CommonResult> contributors() { + return demoFeignClient.contributors("OpenFeign", "feign"); + } + + @GetMapping("/reposNotFound") + public CommonResult> reposNotFound() { + return demoFeignClient.reposNotFound("OpenFeign", "feign"); + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/Contributor.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/Contributor.java new file mode 100644 index 0000000..77be830 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/Contributor.java @@ -0,0 +1,9 @@ +package cn.lingniu.demo.microservice; + +import lombok.Data; + +@Data +public class Contributor { + String login; + int contributions; +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/DemoFeignClient.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/DemoFeignClient.java new file mode 100644 index 0000000..aa2020d --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/DemoFeignClient.java @@ -0,0 +1,25 @@ +package cn.lingniu.demo.microservice; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@FeignClient(name = "lingniu-framework-provider-demo" +//外部接口配置url: @FeignClient(name = "github-api", url = "https://api.github.com" +// 方式1 配置fallback +// , fallback = GithubApiClientFallBack.class +// 方式2 配置fallbackFactory +// , fallbackFactory = GithubApiClientFallbackFactory.class +) +public interface DemoFeignClient { + + @GetMapping("/repos/{owner}/{repo}/contributors") + CommonResult> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo); + + @GetMapping("/reposNotFound") + CommonResult> reposNotFound(@RequestParam("owner") String owner, @RequestParam("repo") String repo); +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallBack.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallBack.java new file mode 100644 index 0000000..eff427e --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallBack.java @@ -0,0 +1,24 @@ +package cn.lingniu.demo.microservice; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + + +@Slf4j +@Component +public class GithubApiClientFallBack implements DemoFeignClient { + + @Override + public CommonResult> contributors(String owner, String repo) { + throw new RuntimeException("contributors no fallback"); + } + + @Override + public CommonResult> reposNotFound(String owner, String repo) { + log.info("reposNotFoundWithFallBack"); + return CommonResult.success(null); + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallbackFactory.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallbackFactory.java new file mode 100644 index 0000000..78e7b7f --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/microservice/GithubApiClientFallbackFactory.java @@ -0,0 +1,21 @@ +package cn.lingniu.demo.microservice; + +import cn.lingniu.framework.plugin.core.exception.ServerException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; + +/** + * + */ +@Slf4j +@Component +public class GithubApiClientFallbackFactory implements FallbackFactory { + @Override + public GithubApiClientFallBack create(Throwable cause) { + if (cause instanceof ServerException) { + return new GithubApiClientFallBack(); + } + return null; + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/monitor/PrometheusController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/monitor/PrometheusController.java new file mode 100644 index 0000000..7185f79 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/monitor/PrometheusController.java @@ -0,0 +1,82 @@ +package cn.lingniu.demo.monitor; + + +import cn.lingniu.framework.plugin.prometheus.PrometheusCounter; +import cn.lingniu.framework.plugin.prometheus.PrometheusMetrics; +import cn.lingniu.framework.plugin.prometheus.PrometheusSummary; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Metrics; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * http://localhost:30290/actuator + * 访问 http://localhost:30290/actuator/metrics/metricName 查看指标 + */ +@RestController +@RequestMapping("/demo/prometheus") +public class PrometheusController { + + /** + * + Counter(计数器) + Counter 是最简单的指标类型,它是一个单调递增的计数器,只能增加或重置为 0。通常用于记录请求总数、任务完成数、错误数等累计数据 */ + @GetMapping("/prometheusCounter") + @PrometheusCounter( labels = {"prometheusCounter","test"}, memo = "prometheusCounter") + public String prometheusCounter() { + return "prometheusCounter"; + } + + + /** + DistributionSummary(分布摘要) + DistributionSummary 与 Histogram 类似,也用于测量数值的分布情况,但它专门用于跟踪事件的大小或持续时间等非时间性指标 + */ + @GetMapping("/prometheusSummary") + @PrometheusSummary(labels = {"prometheusSummary","test"}, memo = "prometheusSummary") + public String prometheusSummary() { + return "prometheusSummary"; + } + + + /** + Histogram(直方图) + Histogram 用于测量数据的分布情况,如请求延迟、响应大小等。它会统计样本数据的分布情况,并提供分位数计算功能。 + */ + @GetMapping("/PrometheusMetrics") + @PrometheusMetrics(name = "prometheusMetrics") + public String prometheusMetrics() { + return "prometheusMetrics"; + } + + + /** + * 自定义埋点 + */ + @GetMapping("/selfMonitor") + public String selfMonitor() { + long startTime = System.currentTimeMillis(); + Counter counter = Metrics.counter("selfMonitorCount", "selfMonitor", "selfMonitorCount"); + counter.increment(); + + DistributionSummary distributionSummary = Metrics.summary("selfMonitorSummary", "selfMonitor", "selfMonitorSummary"); + distributionSummary.record(System.currentTimeMillis() - startTime); + + + return "selfMonitor"; + } + + + + /** + * throw 异常 测试 + */ + @GetMapping("/throw") + public String throwEx() { + throw new RuntimeException("test"); + } + + +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/LockActionDemo.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/LockActionDemo.java new file mode 100644 index 0000000..19d8730 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/LockActionDemo.java @@ -0,0 +1,88 @@ +package cn.lingniu.demo.redisson; + +import cn.lingniu.framework.plugin.redisson.RedissonLockAction; +import cn.lingniu.framework.plugin.redisson.RedissonLockType; +import org.springframework.core.annotation.Order; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class LockActionDemo { + + + @RedissonLockAction(redissonName = "r1", lockType = RedissonLockType.REENTRANT_LOCK, + waitTime = 3000L, leaseTime = 30000L, unit = TimeUnit.MILLISECONDS, + throwException = true) + public void testLockAction(String test) { + + } + + /** + * 使用 #orderId 参数作为锁的key + */ + @PostMapping("/process/{orderId}") + @RedissonLockAction( + redissonName = "r1", + key = "#orderId", + lockType = RedissonLockType.REENTRANT_LOCK, + waitTime = 3000L, leaseTime = 30000L, unit = TimeUnit.MILLISECONDS, + throwException = true + ) + public ResponseEntity processOrder(@PathVariable String orderId) { + // 业务逻辑 + return ResponseEntity.ok("Order processed: " + orderId); + } + + /** + * 使用请求参数 + */ + @GetMapping("/query") + @RedissonLockAction( + redissonName = "defaultRedisson", + key = "'query:' + #userId + ':' + #status", + throwException = true + ) + public ResponseEntity> queryOrders( + @RequestParam String userId, + @RequestParam String status) { + // 业务逻辑 + return ResponseEntity.ok(new ArrayList<>()); + } + + /** + * 使用多个参数组合生成key + */ + @PostMapping("/update") + @RedissonLockAction( + redissonName = "defaultRedisson", + key = "'order:' + #orderDTO.orderId + ':user:' + #orderDTO.userId", + waitTime = 5000L + ) + public ResponseEntity updateOrder(@RequestBody OrderDTO orderDTO) { + // 业务逻辑 + return ResponseEntity.ok("Order updated"); + } + + + + public class OrderDTO { + private String orderId; + private String userId; + private String status; + + // getters and setters + public String getOrderId() { return orderId; } + public void setOrderId(String orderId) { this.orderId = orderId; } + public String getUserId() { return userId; } + public void setUserId(String userId) { this.userId = userId; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/RedissonLockControllerDemo.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/RedissonLockControllerDemo.java new file mode 100644 index 0000000..3793871 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/redisson/RedissonLockControllerDemo.java @@ -0,0 +1,100 @@ +package cn.lingniu.demo.redisson; + +import cn.lingniu.framework.plugin.redisson.RedissonClientFactory; +import cn.lingniu.framework.plugin.redisson.RedissonClusterLockerService; +import cn.lingniu.framework.plugin.redisson.RedissonLockAction; +import org.redisson.api.RBucket; +import org.redisson.api.RLock; +import org.redisson.api.RTopic; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import java.util.concurrent.TimeUnit; + +/** + * 包含分布式锁和集群操作 + */ +@RestController +@RequestMapping("/demo/redisson") +public class RedissonLockControllerDemo { + + @Autowired + RedissonClusterLockerService redissonClusterLockerService; + //指定数据源:r1 + private final RedissonClient redissonClient; + + public RedissonLockControllerDemo() { + this.redissonClient = RedissonClientFactory.getRedissonClient("r1"); + } + + @GetMapping("/lock") + public String acquireLock( + @RequestParam String lockName, + @RequestParam long leaseTime + ) { + RLock lock = redissonClient.getLock(lockName); + try { + boolean isLocked = lock.tryLock(5, leaseTime, TimeUnit.SECONDS); + if (isLocked) { + return "锁获取成功"; + } else { + return "锁获取失败"; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return "锁获取异常"; + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + @GetMapping("/bucket/set") + public String setBucketValue( + @RequestParam String key, + @RequestParam String value + ) { + RBucket bucket = redissonClient.getBucket(key); + bucket.set(value); + return "键值设置成功"; + } + + @GetMapping("/bucket/get/{key}") + public String getBucketValue(@PathVariable String key) { + RBucket bucket = redissonClient.getBucket(key); + return "结果:key:" + key + ",value:=" + bucket.get(); + } + + @GetMapping("/topic/publish") + public String publishMessage( @RequestParam String topicName, + @RequestParam String message + ) { + RTopic topic = redissonClient.getTopic(topicName); + topic.publish(message); + return "消息发布成功"; + } + + @GetMapping("/lockAction") + @RedissonLockAction(redissonName = "r1") + public String redisson() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "lockAction"; + } + + @GetMapping("/redissonClusterLockerService") + public String redissonClusterLockerService() { + return redissonClusterLockerService.wrapLock(this, + x -> "r1", x -> "key1", + x -> "success"); + } + +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/RocketMqController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/RocketMqController.java new file mode 100644 index 0000000..4acde78 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/RocketMqController.java @@ -0,0 +1,58 @@ +package cn.lingniu.demo.rocketmq; + +import cn.lingniu.demo.jetcache.controller.KeyValueEntity; +import cn.lingniu.framework.plugin.rocketmq.core.producer.RocketMqSendMsgBody; +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnFail; +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnSuccess; +import org.apache.rocketmq.client.producer.SendResult; +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; + +import java.util.UUID; + +@RestController +@RequestMapping("/demo/rocketmq") +public class RocketMqController { + + @Autowired + TestMqProducer testProducer; + + @Autowired + TestTransMqProducer testTransProducer; + + @GetMapping("/test") + public String test() { + for (int i = 0; i < 10; i++) { + KeyValueEntity body = new KeyValueEntity(); + String id = UUID.randomUUID().toString(); + body.setId(id); + body.setKey("key" + id); + body.setValue("value" + id); + + RocketMqSendMsgBody message = new RocketMqSendMsgBody<>(); + message.setBody(body); + if (i > 5) { + message.setDelayLevel(1); + } + SendResult sendResult = testProducer.syncSend(message); + System.out.println(sendResult); + + testProducer.asyncSend(message, new SendMessageOnSuccess() { + @Override + public void call(KeyValueEntity message, SendResult result) { + System.out.println(result); + } + }, new SendMessageOnFail() { + @Override + public void call(KeyValueEntity message, Throwable ex) { + System.out.println(ex); + } + }); + + testTransProducer.syncSendInTransaction(message, null); + } + return "over"; + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestC1Consumer.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestC1Consumer.java new file mode 100644 index 0000000..82f7e8f --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestC1Consumer.java @@ -0,0 +1,21 @@ +package cn.lingniu.demo.rocketmq; + +import cn.lingniu.demo.jetcache.controller.KeyValueEntity; +import cn.lingniu.framework.plugin.rocketmq.RocketMqConsumer; +import cn.lingniu.framework.plugin.rocketmq.core.consumer.listener.RocketMqListener; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.common.message.MessageExt; + +/** + * 消费测试 + */ +@Slf4j +@RocketMqConsumer("consumer1") +public class TestC1Consumer implements RocketMqListener { + + @Override + public void onMessage(KeyValueEntity message, MessageExt messageExt) { + log.info("message{}", message); + log.info("messageExt{}", messageExt); + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestMqProducer.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestMqProducer.java new file mode 100644 index 0000000..34ce2d3 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestMqProducer.java @@ -0,0 +1,12 @@ +package cn.lingniu.demo.rocketmq; + +import cn.lingniu.demo.jetcache.controller.KeyValueEntity; +import cn.lingniu.framework.plugin.rocketmq.RocketMqProducer; +import org.springframework.stereotype.Component; + +/** + * 普通生产者 + */ +@Component +public class TestMqProducer extends RocketMqProducer { +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestTransMqProducer.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestTransMqProducer.java new file mode 100644 index 0000000..62718ee --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/rocketmq/TestTransMqProducer.java @@ -0,0 +1,49 @@ +package cn.lingniu.demo.rocketmq; + +import cn.lingniu.demo.jetcache.controller.KeyValueEntity; +import cn.lingniu.framework.plugin.rocketmq.RocketMqProducer; +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 事物消息 + */ +@Component +public class TestTransMqProducer extends RocketMqProducer implements TransactionListener { + + private AtomicInteger transactionIndex = new AtomicInteger(0); + + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + + @Override + public LocalTransactionState executeLocalTransaction(Message message, Object o) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + localTrans.put(message.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt messageExt) { + Integer status = localTrans.get(messageExt.getTransactionId()); + if (null != status) { + switch (status) { + case 0 : + return LocalTransactionState.UNKNOW; + case 1 : + return LocalTransactionState.COMMIT_MESSAGE; + case 2 : + return LocalTransactionState.ROLLBACK_MESSAGE; + default: + return LocalTransactionState.COMMIT_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebController.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebController.java new file mode 100644 index 0000000..9b4d76e --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebController.java @@ -0,0 +1,55 @@ +package cn.lingniu.demo.web; + + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.web.apilog.ApiAccessLog; +import org.checkerframework.checker.units.qual.A; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/demo/web") +public class WebController { + + + @GetMapping("/errorResult") + public WebEntity testRrrorResult() { + WebEntity webEntity = new WebEntity(); + webEntity.setId(1L); + webEntity.setName("error"); + webEntity.setEmail(""); + return webEntity; + } + + @GetMapping("/commonResult") + public CommonResult test() { + WebEntity webEntity = new WebEntity(); + webEntity.setId(1L); + webEntity.setName("lingniu"); + webEntity.setEmail(""); + return CommonResult.success(webEntity); + } + + + @ApiAccessLog(responseEnable = true) + @GetMapping("/apiLog") + public CommonResult apiLog() { + WebEntity webEntity = new WebEntity(); + webEntity.setId(1L); + webEntity.setName("lingniu"); + webEntity.setEmail(""); + return CommonResult.success(webEntity); + } + + + @ApiAccessLog(responseEnable = true) + @GetMapping("/apiEncrypt") + public CommonResult apiEncrypt() { + WebEntity webEntity = new WebEntity(); + webEntity.setId(1L); + webEntity.setName("lingniu"); + webEntity.setEmail(""); + return CommonResult.success(webEntity); + } +} diff --git a/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebEntity.java b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebEntity.java new file mode 100644 index 0000000..e31f033 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/java/cn/lingniu/demo/web/WebEntity.java @@ -0,0 +1,10 @@ +package cn.lingniu.demo.web; + +import lombok.Data; + +@Data +public class WebEntity { + private Long id; + private String name; + private String email; +} diff --git a/demo/lingniu-framework-demo/src/main/resources/application-db.yml b/demo/lingniu-framework-demo/src/main/resources/application-db.yml new file mode 100644 index 0000000..edf0b77 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-db.yml @@ -0,0 +1,12 @@ +framework: + lingniu: + # 框架数据源配置 + datasource: + # 开启框架 sql执行分析 + # dev,local,lpt,fat,test,uat,sit 这些环境可开启 + sqlLog: true + # druid 后台地址:http://localhost:8080/druid/login.html + # druid 账号,默认值为 SpringApplicationProperties#name + druidUsername: "your_druid_username" + # druid 密码,默认值为 SpringApplicationProperties#name + "123" + druidPassword: "your_druid_password" diff --git a/demo/lingniu-framework-demo/src/main/resources/application-jetcache.yml b/demo/lingniu-framework-demo/src/main/resources/application-jetcache.yml new file mode 100644 index 0000000..d1c4fe8 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-jetcache.yml @@ -0,0 +1,58 @@ +framework: + lingniu: + jetcache: + # 需要隐藏的包路径(数组),默认为空 + hiddenPackages: [ ] + # 统计信息输出间隔(分钟),默认为 0(不输出) + statIntervalMinutes: 10 + # 是否在缓存名称中包含区域(area)信息,默认为 true + areaInCacheName: true + # 是否开启缓存穿透保护,默认为 false + penetrationProtect: false + # 本地缓存配置 + local: + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + ca2: + # 本地缓存实现类类型,支持:linkedhashmap/caffeine + type: linkedhashmap + # 缓存key数量限制, 默认100 + limit: 100 + default: + # 本地缓存实现类类型,支持:linkedhashmap/caffeine + type: caffeine + # 缓存key数量限制, 默认100 + limit: 100 + # 过期时间,0为不失效 + expireAfterAccessInMillis: 0 + # key 转换器,用于序列化,目前支持:NONE、FASTJSON、JACKSON、FASTJSON2 + keyConvertor: FASTJSON2 + remote: + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + ca2: + # 远程缓存实现类类型,目前支持:redisson + type: redisson + # 远程缓存客户端名称,默认r1 -- todo 需要在application-redisson.yml中配置 + redissonClient: r1 + # key 前缀 + keyPrefix: "cacheKeyPrefix:" + # 连接超时时间ms + timeout: 2000 + # 重试次数 + maxAttempt: 5 + # 连接池配置 + poolConfig: + maxTotal: 8 + maxIdle: 8 + minIdle: 0 + # key 转换器,用于序列化,目前支持:NONE、FASTJSON、JACKSON、FASTJSON2 + keyConvertor: FASTJSON2 + # JAVA, KRYO,KRYO5,自定义spring beanName + valueEncoder: JAVA + # JAVA, KRYO,KRYO5,自定义spring beanName + valueDecoder: JAVA + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + default: + # 远程缓存实现类类型,目前支持:redisson + type: redisson + # 远程缓存客户端名称,默认r1 -- todo 需要在application-redisson.yml中配置 + redissonClient: r1 diff --git a/demo/lingniu-framework-demo/src/main/resources/application-prometheus.yml b/demo/lingniu-framework-demo/src/main/resources/application-prometheus.yml new file mode 100644 index 0000000..dc56806 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-prometheus.yml @@ -0,0 +1,6 @@ +framework: + lingniu: + prometheus: + enabled: true + port: 30290 + allowAssignPort: false diff --git a/demo/lingniu-framework-demo/src/main/resources/application-redisson.yml b/demo/lingniu-framework-demo/src/main/resources/application-redisson.yml new file mode 100644 index 0000000..35c4c2b --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-redisson.yml @@ -0,0 +1,44 @@ +framework: + lingniu: + redisson: + remote: + r1: + # 应用id需在cachecloud申请 + redisAddresses: 10.130.36.242:6387 + # 客户端名称,默认为应用名 + IP + clientName: "" + # 单节点 Redis 默认数据库,默认为 0 + database: 0 + # 连接池最小空闲连接数,默认为 12 + connectionMinimumIdleSize: 12 + # 连接池最大连接数,默认为 32 + connectionPoolSize: 32 + # 空闲连接超时时间(毫秒),默认为 10000 + idleConnectionTimeout: 10000 + # 连接超时时间(毫秒),默认为 2000 + connectTimeout: 2000 + # 操作超时时间(毫秒),默认为 2000 + timeout: 2000 + # 操作重试次数,默认为 4 + retryAttempts: 4 + # 集群模式下是否检查 Slots 覆盖,默认为 true + checkSlotsCoverage: true + # 读取模式,默认为 "SLAVE" + # 可选值: + # - SLAVE: 从节点读取 + # - MASTER: 主节点读取 + # - MASTER_SLAVE: 主从节点读取 + readMode: "SLAVE" + # 存储类型,默认为 "CLUSTER" + # 可选值: + # - CLUSTER: 集群模式 + # - SINGLE: 单节点模式 + # - SENTINEL: 哨兵模式 + # - REPLICATED: 复制模式 + storageType: "CLUSTER" + # 实例名称 + r2: + # 应用id需在cachecloud申请 + redisAddresses: 10.130.36.242:6387 + # 单节点 Redis 默认数据库,默认为 0 + database: 0 diff --git a/demo/lingniu-framework-demo/src/main/resources/application-rocketmq.yml b/demo/lingniu-framework-demo/src/main/resources/application-rocketmq.yml new file mode 100644 index 0000000..67e461e --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-rocketmq.yml @@ -0,0 +1,64 @@ + +framework: + lingniu: + # rocketmq所有配置示例 + rocketmq: + # 是否开启数据源加密 + encryptEnabled: false + # 消费者配置 + consumers: + # 消费者bean名称 + TestC1Consumer: + nameServer: mq_test_n1.tst.mid:9876 + # 消费组 + consumerGroup: consumer1 + # 消费对象名字 + consumerBeanName: TestC1Consumer + # Topic + topic: test1-yw-topic + # 订阅表达式,默认为 * + selectorExpress: "*" + # 消费者最小线程数 + consumeThreadMin: 20 + # 消费者最大线程数 + consumeThreadMax: 32 + # 批量消费数量 + consumeMessageBatchMaxSize: 1 + # 批量拉取消息数量 + pullBatchSize: 32 + # 消息的最大重试次数 + maxRetryTimes: 16 + + # 生产者配置 + producers: + # 生产者的bean名称 + TestMqProducer: + nameServer: mq_test_n1.tst.mid:9876 + # 生产者组 + group: test1-yw-topic-producer + # 生产端Topic + topic: test1-yw-topic + # 发送超时(ms),默认 3000 + sendMsgTimeout: 3000 + # 消息体压缩阀值(kb),默认 4096 + compressMsgBodyOverHowmuch: 4096 + # 同步发送消息失败,是否重新发送,默认 2 + retryTimesWhenSendFailed: 2 + # 异步发送消息失败,是否重新发送,默认 2 + retryTimesWhenSendAsyncFailed: 2 + # 重试另一个Broker当发送消息失败, 默认 false + retryAnotherBrokerWhenNotStoreOk: false + # 默认不启用延迟容错,通过统计每个队列的发送耗时情况来计算broker是否可用 + sendLatencyFaultEnable: false + TestTransMqProducer: + group: test1-yw-trans-topic-producer + topic: test1-yw-trans-topic + # 是否事务型 + isTransactionMQ: true + # 事务消息线程配置 + transaction: + corePoolSize: 5 + # 本地事务执行结果查询线程池配置 + maximumPoolSize: 10 + keepAliveTime: 200 + queueCapacity: 2000 diff --git a/demo/lingniu-framework-demo/src/main/resources/application-web.yml b/demo/lingniu-framework-demo/src/main/resources/application-web.yml new file mode 100644 index 0000000..0ed62e3 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-web.yml @@ -0,0 +1,12 @@ +framework: + lingniu: + web: + apiLog: + #开关 + enable: true + apiEncrypt: + #开关 + enable: false + algorithm: "AES" + requestKey: "xxx" + responseKey: "xxx" diff --git a/demo/lingniu-framework-demo/src/main/resources/application-xxljob.yml b/demo/lingniu-framework-demo/src/main/resources/application-xxljob.yml new file mode 100644 index 0000000..bcedbe6 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application-xxljob.yml @@ -0,0 +1,21 @@ +framework: + lingniu: + # XXL-JOB 配置 + xxljob: + # 调度中心配置 + admin: + # 调度中心部署根地址(多个地址用逗号分隔,为空则关闭自动注册) + adminAddresses: "http://10.130.119.181:8099/xxl-job-admin" + # 调度中心通讯TOKEN(非空时启用) + accessToken: "default_token" + + # 执行器配置 + executor: + # 执行器IP(默认为空表示自动获取IP) + ip: "" + # 执行器端口号(小于等于0则自动获取,默认9999) + port: 9999 + # 执行器日志文件保存天数(大于等于3时生效,-1表示关闭自动清理,默认7天) + logRetentionDays: 7 + # 执行器运行日志文件存储磁盘路径(需对该路径拥有读写权限,为空则使用默认路径) + logPath: logs/applogs/xxl-job/jobhandler \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/resources/application.yml b/demo/lingniu-framework-demo/src/main/resources/application.yml new file mode 100644 index 0000000..5953fde --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/application.yml @@ -0,0 +1,48 @@ +spring: + application: + name: lingniu-framework-demo + profiles: + active: dev,redisson,jetcache,xxljob,db,rocketmq,prometheus,web + cloud: + nacos: + enabled: true #开启微服务自定义注册,自动获取当前框架信息,启动时间等到metadata中 + discovery: + server-addr: http://nacos-uat-new-inter.xx.net.cn:8848 #注册中心地址 + username: nacos_test #注册中心用户名 + password: nacos_test #注册中心密码 + + #todo db基本配置,连接池请参考:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter + datasource: + dynamic: + primary: master + datasource: + #如需连接mysql,需修改下面的配置 + master: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb + username: sa + password: sa + slaver: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb + username: sa + password: sa + # http://localhost:8080/h2-console 访问 h2 数据库 账号: sa/sa + h2: + console: + enabled: true + path: /h2-console + + +# apollo 配置 +framework: + lingniu: + apollo: + #namespaces必须配置,配置文件列表,以逗号分割 + namespaces: application,applicationTest + # 是否开启 Apollo 配置,默认true + enabled: true + # appId 等于 spring.application.name,无需配置 + appId: lingniu-framework-demo + # meta 地址,框架自动获取,无需配置 + meta: http://10.130.36.237:8180 diff --git a/demo/lingniu-framework-demo/src/main/resources/applicationTest.yml b/demo/lingniu-framework-demo/src/main/resources/applicationTest.yml new file mode 100644 index 0000000..3eb9989 --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/applicationTest.yml @@ -0,0 +1,22 @@ +apollo: + demo: + string-demo: "示例字符串" + boolean-demo: true + list-demo: + - "元素1" + - "元素2" + - "元素3" + set-demo: + - "集合元素1" + - "集合元素2" + hash-demo: + key1: "值1" + key2: "值2" + key3: "值3" + hash-object-demo: + obj1: + test-objcet: "对象示例1" + obj2: + test-objcet: "对象示例2" + object-demo: + test-objcet: "对象示例" \ No newline at end of file diff --git a/demo/lingniu-framework-demo/src/main/resources/logback-spring.xml b/demo/lingniu-framework-demo/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b78b63c --- /dev/null +++ b/demo/lingniu-framework-demo/src/main/resources/logback-spring.xml @@ -0,0 +1,65 @@ + + + + + + + + logback + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + true + + + + ${log.path}/log-%d{yyyy-MM-dd-HH}.%i.log + + 30MB + + + 7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/lingniu-framework-provider-demo/pom.xml b/demo/lingniu-framework-provider-demo/pom.xml new file mode 100644 index 0000000..4a76911 --- /dev/null +++ b/demo/lingniu-framework-provider-demo/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + + + lingniu-framework-provider-demo + jar + + + + + + + cn.lingniu.framework + lingniu-framework-plugin-web + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-common + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-nacos + + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + com.alibaba + fastjson + + + ch.qos.logback + logback-classic + + + + + + + diff --git a/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/ProviderDemoApplication.java b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/ProviderDemoApplication.java new file mode 100644 index 0000000..e23b4a4 --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/ProviderDemoApplication.java @@ -0,0 +1,13 @@ +package cn.lingniu.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@ComponentScan(basePackages = {"cn.lingniu.*"}) +@SpringBootApplication +public class ProviderDemoApplication { + public static void main(String[] args) { + SpringApplication.run(ProviderDemoApplication.class, args); + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/Contributor.java b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/Contributor.java new file mode 100644 index 0000000..269265b --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/Contributor.java @@ -0,0 +1,11 @@ +package cn.lingniu.demo.web; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class Contributor { + String login; + int contributions; +} \ No newline at end of file diff --git a/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/GithubApiController.java b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/GithubApiController.java new file mode 100644 index 0000000..a249e6c --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/java/cn/lingniu/demo/web/GithubApiController.java @@ -0,0 +1,23 @@ +package cn.lingniu.demo.web; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +@RestController +@RequestMapping +public class GithubApiController { + + @GetMapping("/repos/{owner}/{repo}/contributors") + public CommonResult> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo) { + // 模拟返回贡献者列表 + Contributor contributor1 = new Contributor("user1", 10); + Contributor contributor2 = new Contributor("user2", 5); + List response = new ArrayList<>(); + response.add(contributor1); + response.add(contributor2); + return CommonResult.success(response); + } +} \ No newline at end of file diff --git a/demo/lingniu-framework-provider-demo/src/main/resources/application-web.yml b/demo/lingniu-framework-provider-demo/src/main/resources/application-web.yml new file mode 100644 index 0000000..17044d8 --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/resources/application-web.yml @@ -0,0 +1,9 @@ +framework: + lingniu: + web: + apiLog: + #开关 + enable: true + spring: + cloud: + isOkHttp: false \ No newline at end of file diff --git a/demo/lingniu-framework-provider-demo/src/main/resources/application.yml b/demo/lingniu-framework-provider-demo/src/main/resources/application.yml new file mode 100644 index 0000000..4ca9a8b --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/resources/application.yml @@ -0,0 +1,15 @@ +server: + port: 8081 +spring: + application: + name: lingniu-framework-provider-demo + profiles: + active: dev,web + cloud: + nacos: + enabled: true #开启微服务自定义注册,自动获取当前框架信息,启动时间等到metadata中 + discovery: + server-addr: http://nacos-uat-new-inter.xx.net.cn:8848 #注册中心地址 + username: nacos_test #注册中心用户名 + password: nacos_test #注册中心密码 + diff --git a/demo/lingniu-framework-provider-demo/src/main/resources/logback-spring.xml b/demo/lingniu-framework-provider-demo/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b78b63c --- /dev/null +++ b/demo/lingniu-framework-provider-demo/src/main/resources/logback-spring.xml @@ -0,0 +1,65 @@ + + + + + + + + logback + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + true + + + + ${log.path}/log-%d{yyyy-MM-dd-HH}.%i.log + + 30MB + + + 7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/framework.md b/framework.md new file mode 100644 index 0000000..2cc6438 --- /dev/null +++ b/framework.md @@ -0,0 +1,14 @@ +## lingniu-framework 介绍 +- 是一个基于 SpringBoot/cloud、Apollo、Nacos 等开发的内部框架。 +## lingniu-framework 特点 +* 基于 Spring 完整的 MVC + ORM 支持。 +* 支持多数据源、分库分表和分布式事务。 +* 统一注册:支持Nacos作为注册中心,实现多配置、分群组、分命名空间、多业务模块的注册和发现功能; +* 支持基于 Apollo 分布式配置中心 +* 完整的分布式缓存、分布式session、分布式锁支持 +* 多租户功能:集成Mybatis Plus +* 组件(Mysql/Oracle/Redis/JetCache/XXLJob等)多租户友好支持 +* 消息中心:集成消息中间件RocketMQ,对业务进行异步处理; +* 等等 +## 组件介绍 +![img.png](img.png) \ No newline at end of file diff --git a/img.png b/img.png new file mode 100644 index 0000000..963a98c Binary files /dev/null and b/img.png differ diff --git a/lingniu-framework-dependencies/pom.xml b/lingniu-framework-dependencies/pom.xml new file mode 100644 index 0000000..216277a --- /dev/null +++ b/lingniu-framework-dependencies/pom.xml @@ -0,0 +1,768 @@ + + + + cn.lingniu.framework + lingniu-framework-parent + 1.0.0-SNAPSHOT + + 4.0.0 + pom + lingniu-framework-dependencies + + + 1.0.0-SNAPSHOT + 1.7.0 + 1.9.2 + 3.7.5.v20211028-RELEASE + 3.9.0 + 1.7.14 + 1.70 + 2021.0.6.1 + 3.1.0 + 2.18.0 + 2.18.0 + 5.8.38 + 4.9.3 + 2.7.18 + 2021.0.6 + 11.10 + UTF-8 + 3.17.0 + 3.2.2 + 4.13 + 1.8 + 0.6.4 + 1.0.2 + 1.1.0.Final + 27.0-jre + 1.18.38 + 1.2.25 + -Xmx1G + 1.1.1 + 4.5.1 + + 1.10 + 1.5.0 + 2.2.3 + 1.8 + 2.6 + 2.6.1 + 3.1 + 2.10 + 2.8.2 + 2.10 + 1.4 + 2.18.1 + 2.5.2 + + 1.10 + 2.2 + 2.6 + + 2.10.4 + 2.7 + 2.4.3 + 3.5.1 + 2.4 + 2.18.1 + 2.6 + 2.2 + 1.2.9 + 1.7.32 + 2.7.1 + 2.13.0 + 2.2.1 + 1.9.6 + 2.3.0 + 1.2.83 + + 1.4.2 + 2.16.0 + 1.1.2 + 6.4.2.RELEASE + 3.7.1 + 2.18.0 + 4.0.3 + 3.1.5 + 2.1.1 + 5.8.38 + 2.5.1 + 4.5 + 3.5.3.2 + 3.5.3.2 + 4.2.0 + 2.1.2 + 3.5.14 + 2.3.2 + 1.4.2 + 5.3.3 + 1.1.5 + 2.13.0 + + + + + + commons-io + commons-io + ${commons.io.version} + + + org.projectlombok + lombok + compile + + + org.springframework.boot + spring-boot-configuration-processor + true + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + + cn.lingniu.framework + lingniu-framework-plugin-util + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-core + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-apollo + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-xxljob + ${framework.version} + + + + cn.lingniu.framework + lingniu-framework-plugin-mybatis + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-jetcache + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-redisson + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-rocketmq + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-skywalking + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-prometheus + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-web + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-common + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-nacos + ${framework.version} + + + cn.lingniu.framework + lingniu-framework-plugin-easyexcel + ${framework.version} + + + com.ctrip.framework.apollo + apollo-client + ${apollo.version} + + + + + com.oracle + ojdbc14 + 10.2.0.4.0 + + + com.alibaba + easyexcel + ${easyexcel.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-to-slf4j + ${log4j.version} + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper-starter.version} + + + + com.github.pagehelper + pagehelper + ${pagehelper.version} + + + + com.github.jsqlparser + jsqlparser + ${jsqlparser.version} + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + ${mybatis-spring-boot.version} + + + + org.mybatis + mybatis-spring + ${mybatis-spring.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + + + io.micrometer + micrometer-registry-prometheus + 1.9.17 + + + + io.lettuce + lettuce-core + ${lettuce.version} + + + ojdbc + ojdbc6 + 11.2.0.4.0 + + + org.bouncycastle + bcprov-jdk15on + ${bcprov.version} + + + one.util + streamex + 0.7.0 + + + org.jooq + jool + 0.9.13 + + + org.pf4j + pf4j + ${pf4j-version} + + + redis.clients + jedis + ${jedis.version} + + + + + + com.squareup.okhttp3 + okhttp + ${okhttp3.version} + + + cn.hutool + hutool-json + ${hutool-version} + + + cn.hutool + hutool-core + ${hutool-version} + + + cn.hutool + hutool-http + ${hutool-version} + + + cn.hutool + hutool-all + ${hutoll.version} + + + + + com.jfinal + jfinal + 4.8 + + + + com.jfinal + jfinal-weixin + 2.5 + + + + net.dreamlu + mica-auto + 1.1.0 + + + + org.unidal.framework + foundation-service + 4.1.1 + + + org.redisson + redisson + 3.17.5 + + + + com.caucho + hessian + ${hessian.version} + + + + javax.persistence + persistence-api + ${javax.persistence.version} + + + + com.google.code.gson + gson + 2.9.1 + + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.db4j + reflectasm + 1.11.4-2 + + + org.mybatis + mybatis-typehandlers-jsr310 + 1.0.1 + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus-boot-starter.version} + + + com.baomidou + mybatis-plus + ${mybatis-plus-boot-starter.version} + + + com.baomidou + dynamic-datasource-spring-boot-starter + ${dynamic-datasource-starter.version} + + + + joda-time + joda-time + ${joda-time.version} + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + + org.apache.rocketmq + rocketmq-tools + ${rocketmq.version} + + + + org.apache.rocketmq + rocketmq-client + ${rocketmq.version} + + + org.slf4j + slf4j-api + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + slf4j-log4j12 + org.slf4j + ${slf4j.version} + + + + + com.google.guava + guava + ${guava.version} + + + + org.apache.commons + commons-lang3 + ${apache.commons.version} + + + + commons-collections + commons-collections + ${apache.common.collections} + + + + org.hibernate.validator + hibernate-validator + 6.2.5.Final + compile + + + + + com.alicp.jetcache + jetcache-anno + ${jetcache.version} + + + + commons-io + commons-io + ${commons.io.version} + + + + com.xuxueli + xxl-job-core + ${xxl.job.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-databind.version} + compile + + + jackson-core + com.fasterxml.jackson.core + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson.version} + compile + + + jackson-databind + com.fasterxml.jackson.core + + + jackson-core + com.fasterxml.jackson.core + + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + compile + + + jackson-databind + com.fasterxml.jackson.core + + + jackson-core + com.fasterxml.jackson.core + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.module + jackson-module-parameter-names + ${jackson.version} + compile + + + jackson-databind + com.fasterxml.jackson.core + + + jackson-core + com.fasterxml.jackson.core + + + + + + mysql + mysql-connector-java + 8.0.12 + + + + cglib + cglib + 3.1 + + + + org.jsoup + jsoup + ${jsoup.version} + + + commons-net + commons-net + ${commons-net.version} + + + org.javassist + javassist + 3.24.1-GA + + + com.alibaba + fastjson + ${fastjson.version} + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + io.github.openfeign + feign-core + ${feign-core.version} + + + org.t-io + tio-core + ${tio.version} + + + org.t-io + tio-websocket-server + ${tio.version} + + + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + com.alibaba.nacos + nacos-client + ${nacos-client.version} + + + + com.baomidou + mybatis-plus-core + ${mybatis-plus.version} + + + org.mybatis + mybatis + ${mybatis.version} + + + + org.codehaus.plexus + plexus-container-default + ${plexus.container.default} + + + com.google.collections + google-collections + + + + + + com.belerweb + pinyin4j + ${pinyin.version} + + + + org.dromara.dynamictp + dynamic-tp-spring-boot-starter-apollo + ${dynamictp.version} + + + + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + false + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + org.apache.maven.plugins + maven-install-plugin + ${maven-install-plugin.version} + + + org.apache.maven.plugins + maven-help-plugin + ${maven-help-plugin.version} + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + + + + \ No newline at end of file diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/pom.xml b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/pom.xml new file mode 100644 index 0000000..5e70b81 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-jetcache + lingniu-framework-plugin-jetcache + http://maven.apache.org + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + org.redisson + redisson + + + + org.springframework.boot + spring-boot-starter-aop + + + + com.alicp.jetcache + jetcache-anno + + + com.alibaba + fastjson + + + + + + + com.alibaba + fastjson + + + com.google.guava + guava + + + + diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AbstractCacheAutoInit.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AbstractCacheAutoInit.java new file mode 100644 index 0000000..72f1fdc --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AbstractCacheAutoInit.java @@ -0,0 +1,95 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.AbstractCacheBuilder; +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.anno.KeyConvertor; +import com.alicp.jetcache.anno.support.ParserFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.Assert; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Created on 2016/11/29. + * + * @author huangli + */ +public abstract class AbstractCacheAutoInit implements InitializingBean { + + private static Logger logger = LoggerFactory.getLogger(AbstractCacheAutoInit.class); + + @Autowired + protected ConfigurableEnvironment environment; + + @Autowired + protected AutoConfigureBeans autoConfigureBeans; + + protected String[] typeNames; + + private volatile boolean inited = false; + + public AbstractCacheAutoInit(String... cacheTypes) { + Objects.requireNonNull(cacheTypes, "cacheTypes can't be null"); + Assert.isTrue(cacheTypes.length > 0, "cacheTypes length is 0"); + this.typeNames = cacheTypes; + } + + @Override + public void afterPropertiesSet() { + if (!inited) { + synchronized (this) { + if (!inited) { + process("framework.lingniu.jetcache.local.", autoConfigureBeans.getLocalCacheBuilders(), true); + process("framework.lingniu.jetcache.remote.", autoConfigureBeans.getRemoteCacheBuilders(), false); + inited = true; + } + } + } + } + + private void process(String prefix, Map cacheBuilders, boolean local) { + ConfigTree resolver = new ConfigTree(environment, prefix); + Map m = resolver.getProperties(); + Set cacheAreaNames = resolver.directChildrenKeys(); + for (String cacheArea : cacheAreaNames) { + final Object configType = m.get(cacheArea + ".type"); + boolean match = Arrays.stream(typeNames).anyMatch((tn) -> tn.equals(configType)); + if (!match) { + continue; + } + ConfigTree ct = resolver.subTree(cacheArea + "."); + logger.info("init cache area {} , type= {}", cacheArea, typeNames[0]); + CacheBuilder c = initCache(ct, local ? "local." + cacheArea : "remote." + cacheArea); + cacheBuilders.put(cacheArea, c); + } + } + + protected void parseGeneralConfig(CacheBuilder builder, ConfigTree ct) { + AbstractCacheBuilder acb = (AbstractCacheBuilder) builder; + acb.keyConvertor(new ParserFunction(ct.getProperty("keyConvertor", KeyConvertor.FASTJSON2))); + + String expireAfterWriteInMillis = ct.getProperty("expireAfterWriteInMillis"); + if (expireAfterWriteInMillis == null) { + // compatible with 2.1 + expireAfterWriteInMillis = ct.getProperty("defaultExpireInMillis"); + } + if (expireAfterWriteInMillis != null) { + acb.setExpireAfterWriteInMillis(Long.parseLong(expireAfterWriteInMillis)); + } + + String expireAfterAccessInMillis = ct.getProperty("expireAfterAccessInMillis"); + if (expireAfterAccessInMillis != null) { + acb.setExpireAfterAccessInMillis(Long.parseLong(expireAfterAccessInMillis)); + } + + } + + protected abstract CacheBuilder initCache(ConfigTree ct, String cacheAreaWithPrefix); +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AutoConfigureBeans.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AutoConfigureBeans.java new file mode 100644 index 0000000..81e6cf5 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/AutoConfigureBeans.java @@ -0,0 +1,45 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Created on 2016/12/28. + * + * @author huangli + */ +public class AutoConfigureBeans { + + private Map localCacheBuilders = new HashMap<>(); + + private Map remoteCacheBuilders = new HashMap<>(); + + private Map customContainer = Collections.synchronizedMap(new HashMap<>()); + + public Map getLocalCacheBuilders() { + return localCacheBuilders; + } + + public void setLocalCacheBuilders(Map localCacheBuilders) { + this.localCacheBuilders = localCacheBuilders; + } + + public Map getRemoteCacheBuilders() { + return remoteCacheBuilders; + } + + public void setRemoteCacheBuilders(Map remoteCacheBuilders) { + this.remoteCacheBuilders = remoteCacheBuilders; + } + + public Map getCustomContainer() { + return customContainer; + } + + public void setCustomContainer(Map customContainer) { + this.customContainer = customContainer; + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/BeanDependencyManager.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/BeanDependencyManager.java new file mode 100644 index 0000000..d063613 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/BeanDependencyManager.java @@ -0,0 +1,33 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; + +import java.util.Arrays; + +/** + * Created on 2017/5/5. + * + * @author huangli + */ +public class BeanDependencyManager implements BeanFactoryPostProcessor { + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + String[] autoInitBeanNames = beanFactory.getBeanNamesForType(AbstractCacheAutoInit.class, false, false); + if (autoInitBeanNames != null) { + BeanDefinition bd = beanFactory.getBeanDefinition(JetCacheAutoConfiguration.GLOBAL_CACHE_CONFIG_NAME); + String[] dependsOn = bd.getDependsOn(); + if (dependsOn == null) { + dependsOn = new String[0]; + } + int oldLen = dependsOn.length; + dependsOn = Arrays.copyOf(dependsOn, dependsOn.length + autoInitBeanNames.length); + System.arraycopy(autoInitBeanNames, 0, dependsOn, oldLen, autoInitBeanNames.length); + bd.setDependsOn(dependsOn); + } + } + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/CaffeineAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/CaffeineAutoConfiguration.java new file mode 100644 index 0000000..84497e1 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/CaffeineAutoConfiguration.java @@ -0,0 +1,32 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.embedded.CaffeineCacheBuilder; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Component; + +/** + * Created on 2016/12/2. + * + * @author huangli + */ +@Component +@Conditional(CaffeineAutoConfiguration.CaffeineCondition.class) +public class CaffeineAutoConfiguration extends EmbeddedCacheAutoInit { + public CaffeineAutoConfiguration() { + super("caffeine"); + } + + @Override + protected CacheBuilder initCache(ConfigTree ct, String cacheAreaWithPrefix) { + CaffeineCacheBuilder builder = CaffeineCacheBuilder.createCaffeineCacheBuilder(); + parseGeneralConfig(builder, ct); + return builder; + } + + public static class CaffeineCondition extends JetCacheCondition { + public CaffeineCondition() { + super("caffeine"); + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ConfigTree.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ConfigTree.java new file mode 100644 index 0000000..e0d6671 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ConfigTree.java @@ -0,0 +1,106 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.util.Assert; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Created on 2017/11/20. + * + * @author huangli + */ +public class ConfigTree { + private ConfigurableEnvironment environment; + private String prefix; + + public ConfigTree(ConfigurableEnvironment environment, String prefix) { + Assert.notNull(environment, "environment is required"); + Assert.notNull(prefix, "prefix is required"); + this.environment = environment; + this.prefix = prefix; + } + + public ConfigTree subTree(String prefix) { + return new ConfigTree(environment, fullPrefixOrKey(prefix)); + } + + private String fullPrefixOrKey(String prefixOrKey) { + return this.prefix + prefixOrKey; + } + + public Map getProperties() { + Map m = new HashMap<>(); + for (PropertySource source : environment.getPropertySources()) { + if (source instanceof EnumerablePropertySource) { + for (String name : ((EnumerablePropertySource) source) + .getPropertyNames()) { + if (name != null && name.startsWith(prefix)) { + String subKey = name.substring(prefix.length()); + m.put(subKey, environment.getProperty(name)); + } + } + } + } + return m; + } + + public boolean containsProperty(String key) { + key = fullPrefixOrKey(key); + return environment.containsProperty(key); + } + + public String getProperty(String key) { + key = fullPrefixOrKey(key); + return environment.getProperty(key); + } + + public String getProperty(String key, String defaultValue) { + if (containsProperty(key)) { + return getProperty(key); + } else { + return defaultValue; + } + } + + public boolean getProperty(String key, boolean defaultValue) { + if (containsProperty(key)) { + return Boolean.parseBoolean(getProperty(key)); + } else { + return defaultValue; + } + } + + public int getProperty(String key, int defaultValue) { + if (containsProperty(key)) { + return Integer.parseInt(getProperty(key)); + } else { + return defaultValue; + } + } + + public long getProperty(String key, long defaultValue) { + if (containsProperty(key)) { + return Long.parseLong(getProperty(key)); + } else { + return defaultValue; + } + } + + public String getPrefix() { + return prefix; + } + + public Set directChildrenKeys() { + Map m = getProperties(); + return m.keySet().stream().map( + s -> s.indexOf('.') >= 0 ? s.substring(0, s.indexOf('.')) : null) + .filter(s -> s != null) + .collect(Collectors.toSet()); + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/EmbeddedCacheAutoInit.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/EmbeddedCacheAutoInit.java new file mode 100644 index 0000000..cd62f57 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/EmbeddedCacheAutoInit.java @@ -0,0 +1,25 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.anno.CacheConsts; +import com.alicp.jetcache.embedded.EmbeddedCacheBuilder; + +/** + * Created on 2016/12/2. + * + * @author huangli + */ +public abstract class EmbeddedCacheAutoInit extends AbstractCacheAutoInit { + + public EmbeddedCacheAutoInit(String... cacheTypes) { + super(cacheTypes); + } + + @Override + protected void parseGeneralConfig(CacheBuilder builder, ConfigTree ct) { + super.parseGeneralConfig(builder, ct); + EmbeddedCacheBuilder ecb = (EmbeddedCacheBuilder) builder; + + ecb.limit(Integer.parseInt(ct.getProperty("limit", String.valueOf(CacheConsts.DEFAULT_LOCAL_LIMIT)))); + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ExternalCacheAutoInit.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ExternalCacheAutoInit.java new file mode 100644 index 0000000..302937c --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/ExternalCacheAutoInit.java @@ -0,0 +1,36 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.anno.CacheConsts; +import com.alicp.jetcache.anno.support.ParserFunction; +import com.alicp.jetcache.external.ExternalCacheBuilder; + +/** + * Created on 2016/11/29. + * + * @author huangli + */ +public abstract class ExternalCacheAutoInit extends AbstractCacheAutoInit { + public ExternalCacheAutoInit(String... cacheTypes) { + super(cacheTypes); + } + + @Override + protected void parseGeneralConfig(CacheBuilder builder, ConfigTree ct) { + super.parseGeneralConfig(builder, ct); + ExternalCacheBuilder ecb = (ExternalCacheBuilder) builder; + ecb.setKeyPrefix(ct.getProperty("keyPrefix")); + ecb.setBroadcastChannel(parseBroadcastChannel(ct)); + ecb.setValueEncoder(new ParserFunction(ct.getProperty("valueEncoder", CacheConsts.DEFAULT_SERIAL_POLICY))); + ecb.setValueDecoder(new ParserFunction(ct.getProperty("valueDecoder", CacheConsts.DEFAULT_SERIAL_POLICY))); + } + + protected String parseBroadcastChannel(ConfigTree ct) { + String broadcastChannel = ct.getProperty("broadcastChannel"); + if (broadcastChannel != null && !"".equals(broadcastChannel.trim())) { + return broadcastChannel.trim(); + } else { + return null; + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheAutoConfiguration.java new file mode 100644 index 0000000..88120a9 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheAutoConfiguration.java @@ -0,0 +1,83 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheManager; +import com.alicp.jetcache.SimpleCacheManager; +import com.alicp.jetcache.anno.support.EncoderParser; +import com.alicp.jetcache.anno.support.GlobalCacheConfig; +import com.alicp.jetcache.anno.support.JetCacheBaseBeans; +import com.alicp.jetcache.anno.support.KeyConvertorParser; +import com.alicp.jetcache.anno.support.SpringConfigProvider; +import com.alicp.jetcache.support.StatInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import java.util.function.Consumer; + +/** + * Created on 2016/11/17. + * + * @author huangli + */ +@Configuration +@ConditionalOnClass(GlobalCacheConfig.class) +@ConditionalOnMissingBean(GlobalCacheConfig.class) +@EnableConfigurationProperties(JetCacheProperties.class) +@Import({CaffeineAutoConfiguration.class, + MockRemoteCacheAutoConfiguration.class, + LinkedHashMapAutoConfiguration.class, + RedissonAutoConfiguration.class}) +public class JetCacheAutoConfiguration { + + public static final String GLOBAL_CACHE_CONFIG_NAME = "globalCacheConfig"; + + @Bean + @ConditionalOnMissingBean + public SpringConfigProvider springConfigProvider( + @Autowired ApplicationContext applicationContext, + @Autowired GlobalCacheConfig globalCacheConfig, + @Autowired(required = false) EncoderParser encoderParser, + @Autowired(required = false) KeyConvertorParser keyConvertorParser, + @Autowired(required = false) Consumer metricsCallback) { + return new JetCacheBaseBeans().springConfigProvider(applicationContext, globalCacheConfig, + encoderParser, keyConvertorParser, metricsCallback); + } + + @Bean(name = "jcCacheManager") + @ConditionalOnMissingBean + public CacheManager cacheManager(@Autowired SpringConfigProvider springConfigProvider) { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCacheBuilderTemplate(springConfigProvider.getCacheBuilderTemplate()); + return cacheManager; + } + + @Bean + public AutoConfigureBeans autoConfigureBeans() { + return new AutoConfigureBeans(); + } + + @Bean + public static BeanDependencyManager beanDependencyManager() { + return new BeanDependencyManager(); + } + + @Bean(name = GLOBAL_CACHE_CONFIG_NAME) + public GlobalCacheConfig globalCacheConfig(AutoConfigureBeans autoConfigureBeans, JetCacheProperties props) { + GlobalCacheConfig _globalCacheConfig = new GlobalCacheConfig(); + _globalCacheConfig = new GlobalCacheConfig(); + _globalCacheConfig.setHiddenPackages(props.getHiddenPackages()); + _globalCacheConfig.setStatIntervalMinutes(props.getStatIntervalMinutes()); + _globalCacheConfig.setAreaInCacheName(props.isAreaInCacheName()); + _globalCacheConfig.setPenetrationProtect(props.isPenetrationProtect()); + _globalCacheConfig.setEnableMethodCache(props.isEnableMethodCache()); + _globalCacheConfig.setLocalCacheBuilders(autoConfigureBeans.getLocalCacheBuilders()); + _globalCacheConfig.setRemoteCacheBuilders(autoConfigureBeans.getRemoteCacheBuilders()); + return _globalCacheConfig; + } + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheCondition.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheCondition.java new file mode 100644 index 0000000..c154356 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheCondition.java @@ -0,0 +1,48 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.Assert; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Created on 2016/11/28. + * + * @author huangli + */ +public abstract class JetCacheCondition extends SpringBootCondition { + + private String[] cacheTypes; + + protected JetCacheCondition(String... cacheTypes) { + Objects.requireNonNull(cacheTypes, "cacheTypes can't be null"); + Assert.isTrue(cacheTypes.length > 0, "cacheTypes length is 0"); + this.cacheTypes = cacheTypes; + } + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { + ConfigTree ct = new ConfigTree((ConfigurableEnvironment) conditionContext.getEnvironment(), "framework.lingniu.jetcache."); + if (match(ct, "local.") || match(ct, "remote.")) { + return ConditionOutcome.match(); + } else { + return ConditionOutcome.noMatch("no match for " + cacheTypes[0]); + } + } + + private boolean match(ConfigTree ct, String prefix) { + Map m = ct.subTree(prefix).getProperties(); + Set cacheAreaNames = m.keySet().stream().map((s) -> s.substring(0, s.indexOf('.'))).collect(Collectors.toSet()); + final List cacheTypesList = Arrays.asList(cacheTypes); + return cacheAreaNames.stream().anyMatch((s) -> cacheTypesList.contains(m.get(s + ".type"))); + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheProperties.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheProperties.java new file mode 100644 index 0000000..0a70c20 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/JetCacheProperties.java @@ -0,0 +1,68 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Created on 2016/11/23. + * + * @author huangli + */ +@ConfigurationProperties(prefix = "jetcache") +public class JetCacheProperties { + + private String[] hiddenPackages; + private int statIntervalMinutes; + private boolean areaInCacheName = true; + private boolean penetrationProtect = false; + private boolean enableMethodCache = true; + + public JetCacheProperties() { + } + + public String[] getHiddenPackages() { + // keep same with GlobalCacheConfig + return hiddenPackages; + } + + public void setHiddenPackages(String[] hiddenPackages) { + // keep same with GlobalCacheConfig + this.hiddenPackages = hiddenPackages; + } + + public void setHidePackages(String[] hidePackages) { + // keep same with GlobalCacheConfig + this.hiddenPackages = hidePackages; + } + + public int getStatIntervalMinutes() { + return statIntervalMinutes; + } + + public void setStatIntervalMinutes(int statIntervalMinutes) { + this.statIntervalMinutes = statIntervalMinutes; + } + + public boolean isAreaInCacheName() { + return areaInCacheName; + } + + public void setAreaInCacheName(boolean areaInCacheName) { + this.areaInCacheName = areaInCacheName; + } + + public boolean isPenetrationProtect() { + return penetrationProtect; + } + + public void setPenetrationProtect(boolean penetrationProtect) { + this.penetrationProtect = penetrationProtect; + } + + public boolean isEnableMethodCache() { + return enableMethodCache; + } + + public void setEnableMethodCache(boolean enableMethodCache) { + this.enableMethodCache = enableMethodCache; + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/LinkedHashMapAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/LinkedHashMapAutoConfiguration.java new file mode 100644 index 0000000..173eb26 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/LinkedHashMapAutoConfiguration.java @@ -0,0 +1,32 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.embedded.LinkedHashMapCacheBuilder; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Component; + +/** + * Created on 2016/12/2. + * + * @author huangli + */ +@Component +@Conditional(LinkedHashMapAutoConfiguration.LinkedHashMapCondition.class) +public class LinkedHashMapAutoConfiguration extends EmbeddedCacheAutoInit { + public LinkedHashMapAutoConfiguration() { + super("linkedhashmap"); + } + + @Override + protected CacheBuilder initCache(ConfigTree ct, String cacheAreaWithPrefix) { + LinkedHashMapCacheBuilder builder = LinkedHashMapCacheBuilder.createLinkedHashMapCacheBuilder(); + parseGeneralConfig(builder, ct); + return builder; + } + + public static class LinkedHashMapCondition extends JetCacheCondition { + public LinkedHashMapCondition() { + super("linkedhashmap"); + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/MockRemoteCacheAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/MockRemoteCacheAutoConfiguration.java new file mode 100644 index 0000000..431ccaa --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/MockRemoteCacheAutoConfiguration.java @@ -0,0 +1,40 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.anno.CacheConsts; +import com.alicp.jetcache.external.MockRemoteCacheBuilder; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Component; + +/** + * Created on 2016/12/2. + * + * @author huangli + */ +@Component +@Conditional(MockRemoteCacheAutoConfiguration.MockRemoteCacheCondition.class) +public class MockRemoteCacheAutoConfiguration extends ExternalCacheAutoInit { + public MockRemoteCacheAutoConfiguration() { + super("mock"); + } + + @Override + protected CacheBuilder initCache(ConfigTree ct, String cacheAreaWithPrefix) { + MockRemoteCacheBuilder builder = MockRemoteCacheBuilder.createMockRemoteCacheBuilder(); + parseGeneralConfig(builder, ct); + return builder; + } + + @Override + protected void parseGeneralConfig(CacheBuilder builder, ConfigTree ct) { + super.parseGeneralConfig(builder, ct); + MockRemoteCacheBuilder b = (MockRemoteCacheBuilder) builder; + b.limit(Integer.parseInt(ct.getProperty("limit", String.valueOf(CacheConsts.DEFAULT_LOCAL_LIMIT)))); + } + + public static class MockRemoteCacheCondition extends JetCacheCondition { + public MockRemoteCacheCondition() { + super("mock"); + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/RedissonAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/RedissonAutoConfiguration.java new file mode 100644 index 0000000..6bf4eab --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/autoconfigure/RedissonAutoConfiguration.java @@ -0,0 +1,73 @@ +package cn.lingniu.framework.plugin.jetcache.autoconfigure; + +import cn.lingniu.framework.plugin.jetcache.redisson.RedissonCacheBuilder; +import com.alicp.jetcache.CacheBuilder; +import com.alicp.jetcache.CacheConfigException; +import com.alicp.jetcache.external.ExternalCacheBuilder; +import org.redisson.api.RedissonClient; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; +import java.util.Objects; + +/** + * Created on 2022/7/12. + * + * @author yangyong + */ +@Configuration +@Conditional(RedissonAutoConfiguration.RedissonCondition.class) +public class RedissonAutoConfiguration { + private static final String CACHE_TYPE = "redisson"; + + public static class RedissonCondition extends JetCacheCondition { + public RedissonCondition() { + super(CACHE_TYPE); + } + } + + @Bean + public RedissonAutoInit redissonAutoInit() { + return new RedissonAutoInit(); + } + + public static class RedissonAutoInit extends ExternalCacheAutoInit implements ApplicationContextAware { + private ApplicationContext context; + + public RedissonAutoInit() { + super(CACHE_TYPE); + } + + @Override + protected CacheBuilder initCache(final ConfigTree ct, final String cacheAreaWithPrefix) { + final Map beans = this.context.getBeansOfType(RedissonClient.class); + if (beans.isEmpty()) { + throw new CacheConfigException("no RedissonClient in spring context"); + } + RedissonClient client = beans.values().iterator().next(); + if (beans.size() > 1) { + final String redissonClientName = ct.getProperty("redissonClient"); + if (Objects.isNull(redissonClientName) || redissonClientName.isEmpty()) { + throw new CacheConfigException("redissonClient is required, because there is multiple RedissonClient in Spring context"); + } + if (!beans.containsKey(redissonClientName)) { + throw new CacheConfigException("there is no RedissonClient named " + redissonClientName + " in Spring context"); + } + client = beans.get(redissonClientName); + } + final ExternalCacheBuilder builder = RedissonCacheBuilder.createBuilder().redissonClient(client); + parseGeneralConfig(builder, ct); + return builder; + } + + @Override + public void setApplicationContext(final ApplicationContext context) throws BeansException { + this.context = context; + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonBroadcastManager.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonBroadcastManager.java new file mode 100644 index 0000000..75c3456 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonBroadcastManager.java @@ -0,0 +1,70 @@ +package cn.lingniu.framework.plugin.jetcache.redisson; + +import com.alicp.jetcache.CacheManager; +import com.alicp.jetcache.CacheResult; +import com.alicp.jetcache.support.BroadcastManager; +import com.alicp.jetcache.support.CacheMessage; +import com.alicp.jetcache.support.SquashedLogger; +import org.redisson.api.RedissonClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +/** + * Created on 2022/7/12. + * + * @author yangyong + */ +public class RedissonBroadcastManager extends BroadcastManager { + private static final Logger logger = LoggerFactory.getLogger(RedissonBroadcastManager.class); + private final RedissonCacheConfig config; + private final String channel; + private final RedissonClient client; + private volatile int subscribeId; + + public RedissonBroadcastManager(final CacheManager cacheManager, final RedissonCacheConfig config) { + super(cacheManager); + checkConfig(config); + this.config = config; + this.channel = config.getBroadcastChannel(); + this.client = config.getRedissonClient(); + } + + @Override + public synchronized void startSubscribe() { + if (this.subscribeId == 0 && Objects.nonNull(this.channel) && !this.channel.isEmpty()) { + this.subscribeId = this.client.getTopic(this.channel) + .addListener(byte[].class, (channel, msg) -> processNotification(msg, this.config.getValueDecoder())); + } + } + + + @Override + public synchronized void close() { + final int id; + if ((id = this.subscribeId) > 0 && Objects.nonNull(this.channel)) { + this.subscribeId = 0; + try { + this.client.getTopic(this.channel).removeListener(id); + } catch (Throwable e) { + logger.warn("unsubscribe {} fail", this.channel, e); + } + } + } + + @Override + public CacheResult publish(final CacheMessage cacheMessage) { + try { + if (Objects.nonNull(this.channel) && Objects.nonNull(cacheMessage)) { + final byte[] msg = this.config.getValueEncoder().apply(cacheMessage); + this.client.getTopic(this.channel).publish(msg); + return CacheResult.SUCCESS_WITHOUT_MSG; + } + return CacheResult.FAIL_WITHOUT_MSG; + } catch (Throwable e) { + SquashedLogger.getLogger(logger).error("jetcache publish error", e); + return new CacheResult(e); + } + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCache.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCache.java new file mode 100644 index 0000000..d1426cb --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCache.java @@ -0,0 +1,180 @@ +package cn.lingniu.framework.plugin.jetcache.redisson; + +import com.alicp.jetcache.CacheConfig; +import com.alicp.jetcache.CacheGetResult; +import com.alicp.jetcache.CacheResult; +import com.alicp.jetcache.CacheResultCode; +import com.alicp.jetcache.CacheValueHolder; +import com.alicp.jetcache.MultiGetResult; +import com.alicp.jetcache.external.AbstractExternalCache; +import org.redisson.api.RBatch; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Created on 2022/7/12. + * + * @author yangyong + */ +public class RedissonCache extends AbstractExternalCache { + private final RedissonClient client; + private final RedissonCacheConfig config; + + public RedissonCache(final RedissonCacheConfig config) { + super(config); + this.config = config; + this.client = config.getRedissonClient(); + } + + protected String getCacheKey(final K key) { + final byte[] newKey = buildKey(key); + return new String(newKey, StandardCharsets.UTF_8); + } + + @Override + public CacheConfig config() { + return this.config; + } + + @Override + public T unwrap(final Class clazz) { + throw new UnsupportedOperationException("RedissonCache does not support unwrap"); + } + + @Override + @SuppressWarnings({"unchecked"}) + protected CacheGetResult do_GET(final K key) { + try { + final RBucket> rb = this.client.getBucket(getCacheKey(key)); + final CacheValueHolder holder = rb.get(); + if (Objects.nonNull(holder)) { + final long now = System.currentTimeMillis(), expire = holder.getExpireTime(); + if (expire > 0 && now >= expire) { + return CacheGetResult.EXPIRED_WITHOUT_MSG; + } + return new CacheGetResult<>(CacheResultCode.SUCCESS, null, holder); + } + return CacheGetResult.NOT_EXISTS_WITHOUT_MSG; + } catch (Throwable e) { + logError("GET", key, e); + return new CacheGetResult<>(e); + } + } + + @Override + @SuppressWarnings({"unchecked"}) + protected MultiGetResult do_GET_ALL(final Set keys) { + try { + final Map> retMap = new HashMap<>(1 << 4); + if (Objects.nonNull(keys) && !keys.isEmpty()) { + final Map keyMap = new HashMap<>(keys.size()); + for (K k : keys) { + if (Objects.nonNull(k)) { + final String key = getCacheKey(k); + if (Objects.nonNull(key)) { + keyMap.put(k, key); + } + } + } + if (!keyMap.isEmpty()) { + final Map kvMap = this.client.getBuckets().get(keyMap.values().toArray(new String[0])); + final long now = System.currentTimeMillis(); + for (K k : keys) { + final String key = keyMap.get(k); + if (Objects.nonNull(key) && Objects.nonNull(kvMap)) { + final CacheValueHolder holder = (CacheValueHolder) kvMap.get(key); + if (Objects.nonNull(holder)) { + final long expire = holder.getExpireTime(); + final CacheGetResult ret = (expire > 0 && now >= expire) ? CacheGetResult.EXPIRED_WITHOUT_MSG : + new CacheGetResult<>(CacheResultCode.SUCCESS, null, holder); + retMap.put(k, ret); + continue; + } + } + retMap.put(k, CacheGetResult.NOT_EXISTS_WITHOUT_MSG); + } + } + } + return new MultiGetResult<>(CacheResultCode.SUCCESS, null, retMap); + } catch (Throwable e) { + logError("GET_ALL", "keys(" + (Objects.nonNull(keys) ? keys.size() : 0) + ")", e); + return new MultiGetResult<>(e); + } + } + + @Override + protected CacheResult do_PUT(final K key, final V value, final long expireAfterWrite, final TimeUnit timeUnit) { + try { + final CacheValueHolder holder = new CacheValueHolder<>(value, timeUnit.toMillis(expireAfterWrite)); + this.client.getBucket(getCacheKey(key)).set(holder, expireAfterWrite, timeUnit); + return CacheGetResult.SUCCESS_WITHOUT_MSG; + } catch (Throwable e) { + logError("PUT", key, e); + return new CacheResult(e); + } + } + + @Override + protected CacheResult do_PUT_ALL(final Map map, final long expireAfterWrite, final TimeUnit timeUnit) { + try { + if (Objects.nonNull(map) && !map.isEmpty()) { + final long expire = timeUnit.toMillis(expireAfterWrite); + final RBatch batch = this.client.createBatch(); + map.forEach((k, v) -> { + final CacheValueHolder holder = new CacheValueHolder<>(v, expire); + batch.getBucket(getCacheKey(k)).setAsync(holder, expireAfterWrite, timeUnit); + }); + batch.execute(); + } + return CacheResult.SUCCESS_WITHOUT_MSG; + } catch (Throwable e) { + logError("PUT_ALL", "map(" + map.size() + ")", e); + return new CacheResult(e); + } + } + + @Override + protected CacheResult do_REMOVE(final K key) { + try { + final boolean ret = this.client.getBucket(getCacheKey(key)).delete(); + return ret ? CacheResult.SUCCESS_WITHOUT_MSG : CacheResult.FAIL_WITHOUT_MSG; + } catch (Throwable e) { + logError("REMOVE", key, e); + return new CacheResult(e); + } + } + + @Override + protected CacheResult do_REMOVE_ALL(final Set keys) { + try { + if (Objects.nonNull(keys) && !keys.isEmpty()) { + final RBatch batch = this.client.createBatch(); + keys.forEach(key -> batch.getBucket(getCacheKey(key)).deleteAsync()); + batch.execute(); + } + return CacheResult.SUCCESS_WITHOUT_MSG; + } catch (Throwable e) { + logError("REMOVE_ALL", "keys(" + keys.size() + ")", e); + return new CacheResult(e); + } + } + + @Override + protected CacheResult do_PUT_IF_ABSENT(final K key, final V value, final long expireAfterWrite, final TimeUnit timeUnit) { + try { + final CacheValueHolder holder = new CacheValueHolder<>(value, timeUnit.toMillis(expireAfterWrite)); + final boolean success = this.client.getBucket(getCacheKey(key)).trySet(holder, expireAfterWrite, timeUnit); + return success ? CacheResult.SUCCESS_WITHOUT_MSG : CacheResult.EXISTS_WITHOUT_MSG; + } catch (Throwable e) { + logError("PUT_IF_ABSENT", key, e); + return new CacheResult(e); + } + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheBuilder.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheBuilder.java new file mode 100644 index 0000000..338408f --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheBuilder.java @@ -0,0 +1,52 @@ +package cn.lingniu.framework.plugin.jetcache.redisson; + +import com.alicp.jetcache.CacheManager; +import com.alicp.jetcache.external.ExternalCacheBuilder; +import com.alicp.jetcache.support.BroadcastManager; +import org.redisson.api.RedissonClient; + +/** + * Created on 2022/7/12. + * + * @author yangyong + */ +public class RedissonCacheBuilder> extends ExternalCacheBuilder { + + public static class RedissonDataCacheBuilderImpl extends RedissonCacheBuilder { + + } + + public static RedissonDataCacheBuilderImpl createBuilder() { + return new RedissonDataCacheBuilderImpl(); + } + + @SuppressWarnings({"all"}) + protected RedissonCacheBuilder() { + buildFunc(config -> new RedissonCache((RedissonCacheConfig) config)); + } + + @Override + @SuppressWarnings({"all"}) + public RedissonCacheConfig getConfig() { + if (this.config == null) { + this.config = new RedissonCacheConfig(); + } + return (RedissonCacheConfig) this.config; + } + + public T redissonClient(final RedissonClient client) { + this.getConfig().setRedissonClient(client); + return self(); + } + + @Override + public boolean supportBroadcast() { + return true; + } + + @Override + public BroadcastManager createBroadcastManager(final CacheManager cacheManager) { + final RedissonCacheConfig c = (RedissonCacheConfig) this.getConfig().clone(); + return new RedissonBroadcastManager(cacheManager, c); + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheConfig.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheConfig.java new file mode 100644 index 0000000..a6e4809 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/java/cn/lingniu/framework/plugin/jetcache/redisson/RedissonCacheConfig.java @@ -0,0 +1,21 @@ +package cn.lingniu.framework.plugin.jetcache.redisson; + +import com.alicp.jetcache.external.ExternalCacheConfig; +import org.redisson.api.RedissonClient; + +/** + * Created on 2022/7/12. + * + * @author yangyong + */ +public class RedissonCacheConfig extends ExternalCacheConfig { + private RedissonClient redissonClient; + + public RedissonClient getRedissonClient() { + return redissonClient; + } + + public void setRedissonClient(final RedissonClient redissonClient) { + this.redissonClient = redissonClient; + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..6188ab4 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.jetcache.autoconfigure.JetCacheAutoConfiguration \ No newline at end of file diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/jetcache-config.md b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/jetcache-config.md new file mode 100644 index 0000000..3513f6a --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache/src/main/resources/jetcache-config.md @@ -0,0 +1,122 @@ +# 【重要】jetcache详细资料---参考官网 + +## 概述 (Overview) + +1. 基于 JetCache 封装的企业级二级缓存解决方案,提供本地 JVM 本地缓存和远程 Redis 缓存的组合缓存能力 +2. 核心能力 + * 二级缓存机制: + + - 一级缓存:本地 JVM 缓存(基于 Caffeine等) + - 二级缓存:远程 Redis 缓存(redisson 模式) + +* 多样化缓存配置: + - 本地缓存:支持 Caffeine 类型,可配置缓存数量限制和过期时间 + - 远程缓存:支持 redisson 类型 + - 多实例支持:可配置多个缓存分组(如 default、employee 等) +* 注解式缓存操作: + - @Cached:标记方法结果需要缓存,支持缓存名称、过期时间等配置 + - @CacheRefresh:支持缓存自动刷新机制 + - @EnableMethodCache:启用 JetCache Bean 扫描 +* 灵活的缓存策略: + - 支持不同的 Key 生成策略 + - 可配置缓存过期时间和时间单位 + - 支持不同的缓存类型(本地、远程、双向) + +3. 适用场景:该组件特别适用于需要高性能缓存访问的企业级应用,通过本地+远程的二级缓存架构,在保证缓存访问速度的同时,确保缓存数据的共享和一致性。通过注解方式简化了缓存的使用。 + * 高并发读场景:需要减少数据库访问压力,提升系统响应速度的业务场景 + * 高可用访问:需要自动感知 Redis 集群变化以及故障自动切换能力的应用场景 + * 复杂查询结果缓存:对于计算复杂或关联查询较多的数据结果进行缓存 + * 数据一致性要求较高:需要通过二级缓存机制保证数据访问性能和一致性的场景 + * 需要缓存自动刷新:对于有一定时效性要求但不需要实时更新的数据 + +## 如何配置--更多参数参考:更多配置请参考官网jetcache + +* 此处为了统一中间件配置前缀framework.lingniu.jetcache 和更好的个性化扩展, 其他jetcache所有配置方式未改变 +* 请先依赖:lingniu-framework-plugin-redisson + redissonClient 就是 RedissonConfig配置的key名称 + +```yaml +framework: + lingniu: + jetcache: + # 需要隐藏的包路径(数组),默认为空 + hiddenPackages: [ ] + # 统计信息输出间隔(分钟),默认为 0(不输出) + statIntervalMinutes: 10 + # 是否在缓存名称中包含区域(area)信息,默认为 true + areaInCacheName: true + # 是否开启缓存穿透保护,默认为 false + penetrationProtect: false + # 本地缓存配置 + local: + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + ca2: + # 本地缓存实现类类型,支持:linkedhashmap/caffeine + type: linkedhashmap + # 缓存key数量限制, 默认100 + limit: 100 + default: + # 本地缓存实现类类型,支持:linkedhashmap/caffeine + type: caffeine + # 缓存key数量限制, 默认100 + limit: 100 + # 过期时间,0为不失效 + expireAfterAccessInMillis: 0 + # key 转换器,用于序列化,目前支持:NONE、FASTJSON、JACKSON、FASTJSON2 + keyConvertor: FASTJSON2 + remote: + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + ca2: + # 远程缓存实现类类型,目前支持:redisson + type: redisson + # 远程缓存客户端名称,默认r1 -- todo 需要在application-redisson.yml中配置 + redissonClient: r1 + # key 前缀 + keyPrefix: "cacheKeyPrefix:" + # 连接超时时间ms + timeout: 2000 + # 重试次数 + maxAttempt: 5 + # 连接池配置 + poolConfig: + maxTotal: 8 + maxIdle: 8 + minIdle: 0 + # key 转换器,用于序列化,目前支持:NONE、FASTJSON、JACKSON、FASTJSON2 + keyConvertor: FASTJSON2 + # JAVA, KRYO,KRYO5,自定义spring beanName + valueEncoder: JAVA + # JAVA, KRYO,KRYO5,自定义spring beanName + valueDecoder: JAVA + # cache area 配置, 使用CacheManager或@Cache注解时 默认area为default + default: + # 远程缓存实现类类型,目前支持:redisson + type: redisson + # 远程缓存客户端名称,默认r1 -- todo 需要在application-redisson.yml中配置 + redissonClient: r1 + +``` + +## 如何使用- + +```java +//启动 扫描类添加注解 +@EnableMethodCache(basePackages = "cn.lingniu.*") +@SpringBootApplication +public class CacheApplication { + public static void main(String[] args) { + SpringApplication.run(CacheApplication.class, args); + } +} +// 多种使用方式参考 +// 1. 注解方式 +@CachePenetrationProtect +@Cached(name = "orderCache:", key = "#orderId", expire = 3600, cacheType = CacheType.BOTH) +@CacheRefresh(refresh = 10, timeUnit = TimeUnit.MINUTES) +public DataResult getOrderById(String orderId) { + ... +} +// 2. SimpleCacheManager方式 +Cache localCache = simpleCacheManager.getOrCreateCache(QuickConfig.newBuilder(localArea, "user:").cacheType(CacheType.LOCAL).build()); +``` + diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/pom.xml b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/pom.xml new file mode 100644 index 0000000..ec1f026 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-redisson + lingniu-framework-plugin-redisson + http://maven.apache.org + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + org.redisson + redisson + + + org.springframework.boot + spring-boot-starter-aop + + + com.alibaba + fastjson + + + diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClientFactory.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClientFactory.java new file mode 100644 index 0000000..2bf65d4 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClientFactory.java @@ -0,0 +1,28 @@ +package cn.lingniu.framework.plugin.redisson; + +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import org.redisson.api.RedissonClient; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class RedissonClientFactory { + + private static volatile Map redisMap = new ConcurrentHashMap<>(); + + public static void putRedissonClient(String beanName, RedissonClient redissonClient) { + redisMap.put(beanName, redissonClient); + } + + public static RedissonClient getRedissonClient(String beanName) { + if (ObjectEmptyUtils.isEmpty(beanName) || !redisMap.containsKey(beanName)) { + throw new IllegalStateException("(redisson)bean-对应的RedissonClient未注册:" + beanName); + } + return redisMap.get(beanName); + } + + public static Map getRedisMap() { + return redisMap; + } + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClusterLockerService.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClusterLockerService.java new file mode 100644 index 0000000..27ea85b --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonClusterLockerService.java @@ -0,0 +1,30 @@ +package cn.lingniu.framework.plugin.redisson; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * 锁 + **/ +@SuppressWarnings("all") +public interface RedissonClusterLockerService { + + default public R wrapLock(T source, Function fun, + Function keyGetter, Function wrapFun) { + return wrapLock(source, fun, keyGetter, wrapFun, 15L, TimeUnit.SECONDS); + } + + R wrapLock(T source, Function fun, + Function keyGetter, Function wrapFun, + Long leaseTime, TimeUnit leaseTimeUnit); + + default public R wrapLock(T source, Function keyGetter, Function wrapFun) { + return wrapLock(source, (t) -> { + String key = keyGetter.apply(source); + return delivery(key); + }, keyGetter, wrapFun); + } + + String delivery(String key); + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockAction.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockAction.java new file mode 100644 index 0000000..7f96f9c --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockAction.java @@ -0,0 +1,51 @@ +package cn.lingniu.framework.plugin.redisson; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * 支持注解获取锁 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface RedissonLockAction { + + /** + * RedissonConfig 中 remote 的 key 名字 + */ + String redissonName(); + /** + * 锁的资源,key。支持spring El表达式 + */ + @AliasFor("key") + String value() default "'default'"; + @AliasFor("value") + String key() default "'default'"; + /** + * 锁类型 + */ + RedissonLockType lockType() default RedissonLockType.REENTRANT_LOCK; + /** + * 获取锁等待时间,默认3秒 + */ + long waitTime() default 3000L; + /** + * 锁自动释放时间,默认10秒 + */ + long leaseTime() default 10000L; + /** + * 时间单位 + */ + TimeUnit unit() default TimeUnit.MILLISECONDS; + /** + * 锁失败是否Throw异常 + */ + boolean throwException() default false; +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockType.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockType.java new file mode 100644 index 0000000..5e72cbd --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/RedissonLockType.java @@ -0,0 +1,23 @@ +package cn.lingniu.framework.plugin.redisson; + +/** + * 锁类型 + */ +public enum RedissonLockType { + /** + * 读锁 + */ + READ_LOCK, + /** + * 写锁 + */ + WRITE_LOCK, + /** + * 可重入锁 + */ + REENTRANT_LOCK, + /** + * 公平锁 + */ + FAIR_LOCK; +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonAbstractClientBuilder.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonAbstractClientBuilder.java new file mode 100644 index 0000000..367f95e --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonAbstractClientBuilder.java @@ -0,0 +1,58 @@ +package cn.lingniu.framework.plugin.redisson.builder; + +import cn.lingniu.framework.plugin.redisson.config.RedisHostAndPort; +import cn.lingniu.framework.plugin.redisson.config.RedissonProperties; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.redisson.config.Config; + +import java.util.HashSet; +import java.util.Set; + + +@Slf4j +public abstract class RedissonAbstractClientBuilder { + + @Getter(AccessLevel.PUBLIC) + private RedissonProperties redissonProperties; + + protected RedissonAbstractClientBuilder(RedissonProperties redissonProperties) { + this.redissonProperties = redissonProperties; + } + + protected abstract void config(Config redssionConfig, Set redisHostAndPorts); + + public Config build() { + Config redissionConfig = new Config(); + Set redisHostAndPorts = parseRedisHostAndPort(redissonProperties.getRedisAddresses()); + config(redissionConfig, redisHostAndPorts); + return redissionConfig; + } + + public Set parseRedisHostAndPort(String redisAddress) { + Set redisHostAndPorts = new HashSet<>(); + // 分离地址部分和密码部分 + String[] parts = redisAddress.split(";"); + String addressPart = parts[0]; + String password = parts.length > 1 ? parts[1] : null; + // 解析host:port格式的地址 + String[] hosts = addressPart.split(","); + for (String hostPort : hosts) { + hostPort = hostPort.trim(); + if (hostPort.contains(":")) { + int colonIndex = hostPort.indexOf(":"); + String host = hostPort.substring(0, colonIndex); + int port = Integer.parseInt(hostPort.substring(colonIndex + 1)); + RedisHostAndPort redisHostAndPort = new RedisHostAndPort(); + redisHostAndPort.setHost(host); + redisHostAndPort.setPort(port); + if (password != null) { + redisHostAndPort.setPkey(password); + } + redisHostAndPorts.add(redisHostAndPort); + } + } + return redisHostAndPorts; + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonClusterClientBuilder.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonClusterClientBuilder.java new file mode 100644 index 0000000..4f2b169 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonClusterClientBuilder.java @@ -0,0 +1,47 @@ +package cn.lingniu.framework.plugin.redisson.builder; + + +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.redisson.config.RedisHostAndPort; +import cn.lingniu.framework.plugin.redisson.config.RedissonProperties; +import cn.lingniu.framework.plugin.util.ip.IpUtil; +import org.apache.commons.lang3.StringUtils; +import org.redisson.config.ClusterServersConfig; +import org.redisson.config.Config; +import org.redisson.config.ReadMode; + +import java.util.Set; + +/** + * @description: 集群类型 + **/ +public class RedissonClusterClientBuilder extends RedissonAbstractClientBuilder { + + public RedissonClusterClientBuilder(RedissonProperties properties) { + super(properties); + } + + @Override + protected void config(Config redssionConfig, Set redisHostAndPorts) { + ClusterServersConfig clusterServersConfig = redssionConfig.useClusterServers(); + if (StringUtils.isEmpty(getRedissonProperties().getClientName())) { + clusterServersConfig.setClientName(ApplicationNameContext.getApplicationName() + ":" + IpUtil.getIp()); + } + clusterServersConfig.setMasterConnectionMinimumIdleSize(getRedissonProperties().getConnectionMinimumIdleSize()); + clusterServersConfig.setSlaveConnectionMinimumIdleSize(getRedissonProperties().getConnectionMinimumIdleSize()); + clusterServersConfig.setMasterConnectionPoolSize(getRedissonProperties().getConnectionPoolSize()); + clusterServersConfig.setSlaveConnectionPoolSize(getRedissonProperties().getConnectionPoolSize()); + clusterServersConfig.setIdleConnectionTimeout(getRedissonProperties().getIdleConnectionTimeout()); + clusterServersConfig.setConnectTimeout(getRedissonProperties().getConnectTimeout()); + clusterServersConfig.setTimeout(getRedissonProperties().getTimeout()); + clusterServersConfig.setRetryAttempts(getRedissonProperties().getRetryAttempts()); + RedisHostAndPort redisHostAndPort = redisHostAndPorts.stream().findAny().get(); + clusterServersConfig.setPassword(redisHostAndPort.getPkey()); + clusterServersConfig.setCheckSlotsCoverage(getRedissonProperties().getCheckSlotsCoverage()); + clusterServersConfig.setReadMode(ReadMode.valueOf(getRedissonProperties().getReadMode())); + redisHostAndPorts.stream().forEach(h -> { + clusterServersConfig.addNodeAddress(String.format("redis://%s/", h.getHost() + ":" + h.getPort())); + }); + + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonStandaloneClientBuilder.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonStandaloneClientBuilder.java new file mode 100644 index 0000000..15d122f --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/builder/RedissonStandaloneClientBuilder.java @@ -0,0 +1,41 @@ +package cn.lingniu.framework.plugin.redisson.builder; + +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.redisson.config.RedisHostAndPort; +import cn.lingniu.framework.plugin.redisson.config.RedissonProperties; +import cn.lingniu.framework.plugin.util.ip.IpUtil; +import org.apache.commons.lang3.StringUtils; +import org.redisson.config.Config; +import org.redisson.config.SingleServerConfig; + +import java.util.Set; + + +/** + * @description: 单节点类型 + **/ +public class RedissonStandaloneClientBuilder extends RedissonAbstractClientBuilder { + + + public RedissonStandaloneClientBuilder(RedissonProperties properties) { + super(properties); + } + + @Override + protected void config(Config redssionConfig, Set redisHostAndPorts) { + SingleServerConfig singleServerConfig = redssionConfig.useSingleServer(); + if (StringUtils.isEmpty(getRedissonProperties().getClientName())) { + singleServerConfig.setClientName(ApplicationNameContext.getApplicationName() + ":" + IpUtil.getIp()); + } + singleServerConfig.setDatabase(getRedissonProperties().getDatabase()); + singleServerConfig.setConnectionMinimumIdleSize(getRedissonProperties().getConnectionMinimumIdleSize()); + singleServerConfig.setConnectionPoolSize(getRedissonProperties().getConnectionPoolSize()); + singleServerConfig.setIdleConnectionTimeout(getRedissonProperties().getIdleConnectionTimeout()); + singleServerConfig.setConnectTimeout(getRedissonProperties().getConnectTimeout()); + singleServerConfig.setTimeout(getRedissonProperties().getTimeout()); + singleServerConfig.setRetryAttempts(getRedissonProperties().getRetryAttempts()); + RedisHostAndPort redisHostAndPort = redisHostAndPorts.stream().findAny().get(); + singleServerConfig.setPassword(redisHostAndPort.getPkey()); + singleServerConfig.setAddress(String.format("redis://%s/", redisHostAndPort.getHost() + ":" + redisHostAndPort.getPort())); + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisHostAndPort.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisHostAndPort.java new file mode 100644 index 0000000..e1ca3d4 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisHostAndPort.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.redisson.config; + +import lombok.Data; + +@Data +public class RedisHostAndPort { + + private String host; + + private int port; + + private String pkey; +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisType.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisType.java new file mode 100644 index 0000000..2eb3fd6 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedisType.java @@ -0,0 +1,23 @@ +package cn.lingniu.framework.plugin.redisson.config; + +import lombok.Getter; + +/** + * 集群类型 + **/ +public enum RedisType { + STANDALONE("standalone", "单节点类型"), + CLUSTER("cluster", "集群类型"), + SENTINEL("sentinel", "哨兵类型"); + + @Getter + private String type; + + @Getter + private String memo; + + RedisType(String type, String memo) { + this.type = type; + this.memo = memo; + } +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonConfig.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonConfig.java new file mode 100644 index 0000000..2cbea8f --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonConfig.java @@ -0,0 +1,21 @@ +package cn.lingniu.framework.plugin.redisson.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.HashMap; +import java.util.Map; + +/** + * redisson 配置 + */ +@ConfigurationProperties(prefix = RedissonConfig.PREFIX) +@Data +public class RedissonConfig { + + public static final String PREFIX = "framework.lingniu.redisson"; + + + private Map remote = new HashMap<>(); + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonProperties.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonProperties.java new file mode 100644 index 0000000..d13ddd8 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/config/RedissonProperties.java @@ -0,0 +1,46 @@ +package cn.lingniu.framework.plugin.redisson.config; + +import lombok.Data; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +/** + * 详细配置 + */ +@Data +public class RedissonProperties { + + /** + * redis连接串信息 host:port,host:port, 多个用,分割,最后一个是;是密码,可以不配置 + */ + private String redisAddresses; + + @Pattern(regexp = "^(SLAVE|MASTER|MASTER_SLAVE)$", message = "readMode must be one of SLAVE, MASTER, MASTER_SLAVE") + private String readMode = "MASTER"; + /** + * 类型 + */ + private RedisType storageType = RedisType.CLUSTER; + private String clientName = ""; + + @Min(value = 0, message = "database must be greater than or equal to 0") + @Max(value = 15, message = "database must be less than or equal to 15") + private int database = 0; + + private int connectionMinimumIdleSize = 12; + + private int connectionPoolSize = 32; + + private int idleConnectionTimeout = 10000; + + private int connectTimeout = 2000; + + private int timeout = 2000; + + private int retryAttempts = 4; + //cluster: Slots检查 + private Boolean checkSlotsCoverage = true; +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonAutoConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonAutoConfiguration.java new file mode 100644 index 0000000..1160fbf --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonAutoConfiguration.java @@ -0,0 +1,53 @@ +package cn.lingniu.framework.plugin.redisson.init; + +import cn.lingniu.framework.plugin.redisson.RedissonClusterLockerService; +import cn.lingniu.framework.plugin.redisson.RedissonClientFactory; +import cn.lingniu.framework.plugin.redisson.config.RedissonConfig; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import java.util.ArrayList; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +/** + * 初始化-RedissonClusterLockerService + */ +@Slf4j +@EnableConfigurationProperties({RedissonConfig.class}) +public class RedissonAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public RedissonClusterLockerService redissonClusterLockerService() { + return new RedissonClusterLockerService() { + @Override + public R wrapLock(T source, Function fun, Function keyGetter, Function wrapFun, Long leaseTime, TimeUnit leaseTimeUnit) { + RedissonClient client = Optional.ofNullable(RedissonClientFactory.getRedissonClient(fun.apply(source))).orElseThrow(() -> new IllegalStateException("Redisson client not found for: " + fun.apply(source))); + RLock lock = client.getLock(keyGetter.apply(source)); + try { + lock.lock(leaseTime, leaseTimeUnit); + return wrapFun.apply(source); + } finally { + lock.unlock(); + } + } + + @Override + public String delivery(String key) { + return new ArrayList<>(RedissonClientFactory.getRedisMap().keySet()).toArray(new String[]{})[Math.abs(key.hashCode() % RedissonClientFactory.getRedisMap().size())]; + } + }; + } + + @Bean + RedissonBeanRegisterPostProcessor redissonBeanRegister() { + return new RedissonBeanRegisterPostProcessor(); + } + +} diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonBeanRegisterPostProcessor.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonBeanRegisterPostProcessor.java new file mode 100644 index 0000000..46da7c1 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonBeanRegisterPostProcessor.java @@ -0,0 +1,75 @@ +package cn.lingniu.framework.plugin.redisson.init; + +import cn.lingniu.framework.plugin.redisson.RedissonClientFactory; +import cn.lingniu.framework.plugin.redisson.builder.RedissonClusterClientBuilder; +import cn.lingniu.framework.plugin.redisson.builder.RedissonStandaloneClientBuilder; +import cn.lingniu.framework.plugin.redisson.config.RedisType; +import cn.lingniu.framework.plugin.redisson.config.RedissonConfig; +import cn.lingniu.framework.plugin.redisson.config.RedissonProperties; +import lombok.extern.slf4j.Slf4j; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; + +import java.util.Map; +import java.util.Optional; + +/** + * 初始化 + */ +@Slf4j +public class RedissonBeanRegisterPostProcessor implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware { + + private ConfigurableApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = (ConfigurableApplicationContext) applicationContext; + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + RedissonConfig config = Binder.get(applicationContext.getEnvironment()).bind(RedissonConfig.PREFIX, RedissonConfig.class).orElse(new RedissonConfig()); + Map remote = config.getRemote(); + Optional.ofNullable(remote).ifPresent(b -> b.forEach((k, v) -> { + if (applicationContext.containsBean(k)) { + throw new IllegalStateException("redisson的实例对象已注册,请查看配置:" + k); + } + registerContainer(registry, k, v); + })); + } + + private void registerContainer(BeanDefinitionRegistry registry, String beanName, RedissonProperties redissonProperties) { + Config redissonConfig = null; + if (RedisType.STANDALONE.equals(redissonProperties.getStorageType())) { + redissonConfig = new RedissonStandaloneClientBuilder(redissonProperties).build(); + } else if (RedisType.CLUSTER.equals(redissonProperties.getStorageType())) { + redissonConfig = new RedissonClusterClientBuilder(redissonProperties).build(); + } else { + throw new IllegalArgumentException("不支持的Redisson存储类型:" + redissonProperties.getStorageType()); + } + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Redisson.class); + builder.addConstructorArgValue(redissonConfig); + registry.registerBeanDefinition(beanName, builder.getBeanDefinition()); + // 从ApplicationContext中获取已注册的Bean实例 + RedissonClient jedisCluster = applicationContext.getBean(beanName, RedissonClient.class); + RedissonClientFactory.putRedissonClient(beanName, jedisCluster); + if (!registry.containsBeanDefinition("redissonDistributedLockAspectConfiguration")) { + registry.registerBeanDefinition("redissonDistributedLockAspectConfiguration", + BeanDefinitionBuilder.rootBeanDefinition(RedissonDistributedLockAspectConfiguration.class).getBeanDefinition()); + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonDistributedLockAspectConfiguration.java b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonDistributedLockAspectConfiguration.java new file mode 100644 index 0000000..245931d --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/java/cn/lingniu/framework/plugin/redisson/init/RedissonDistributedLockAspectConfiguration.java @@ -0,0 +1,92 @@ +package cn.lingniu.framework.plugin.redisson.init; + +import cn.lingniu.framework.plugin.redisson.RedissonLockAction; +import cn.lingniu.framework.plugin.redisson.RedissonClientFactory; +import lombok.extern.slf4j.Slf4j; +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.aspectj.lang.reflect.MethodSignature; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +/** + * Aspect 实现 + */ +@Slf4j +@Aspect +public class RedissonDistributedLockAspectConfiguration { + + private ExpressionParser parser = new SpelExpressionParser(); + + private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); + + @Pointcut("@annotation(cn.lingniu.framework.plugin.redisson.RedissonLockAction)") + public void lockPoint() { + } + + @Around("lockPoint()") + public Object around(ProceedingJoinPoint pjp) throws Throwable { + Method method = ((MethodSignature) pjp.getSignature()).getMethod(); + RedissonLockAction redissonLockAction = method.getAnnotation(RedissonLockAction.class); + Object[] args = pjp.getArgs(); + String key = redissonLockAction.value(); + key = parse(key, method, args); + + RLock lock = getLock(key, redissonLockAction); + if (!lock.tryLock(redissonLockAction.waitTime(), redissonLockAction.leaseTime(), redissonLockAction.unit())) { + log.warn("RedissonLockAction-获取锁失败: [{}]", key); + if (redissonLockAction.throwException()) + throw new RuntimeException(String.format("RedissonLockAction-获取锁失败:[%s]", key)); + return null; + } + log.info("RedissonLockAction-获取锁成功: [{}]", key); + try { + return pjp.proceed(); + } catch (Exception e) { + log.error(String.format("RedissonLockAction-方法锁执行异常:[%s]", key), e); + throw e; + } finally { + lock.unlock(); + log.info("RedissonLockAction-释放锁成功: [{}]", key); + } + } + + /** + * 解析spring EL表达式 + */ + private String parse(String key, Method method, Object[] args) { + String[] params = discoverer.getParameterNames(method); + EvaluationContext context = new StandardEvaluationContext(); + for (int i = 0; i < params.length; i++) { + context.setVariable(params[i], args[i]); + } + return parser.parseExpression(key).getValue(context, String.class); + } + + private RLock getLock(String key, RedissonLockAction redissonLockAction) { + RedissonClient redissonClient = RedissonClientFactory.getRedissonClient(redissonLockAction.redissonName()); + switch (redissonLockAction.lockType()) { + case REENTRANT_LOCK: + return redissonClient.getLock(key); + case FAIR_LOCK: + return redissonClient.getFairLock(key); + case READ_LOCK: + return redissonClient.getReadWriteLock(key).readLock(); + case WRITE_LOCK: + return redissonClient.getReadWriteLock(key).writeLock(); + default: + throw new RuntimeException("RedissonLockAction-不支持的锁类型:" + redissonLockAction.lockType().name()); + } + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..fe4d817 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.redisson.init.RedissonAutoConfiguration \ No newline at end of file diff --git a/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/redisson-config.md b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/redisson-config.md new file mode 100644 index 0000000..22c4ff0 --- /dev/null +++ b/lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson/src/main/resources/redisson-config.md @@ -0,0 +1,121 @@ +# 【重要】redisson详细资料-redis搭建---参考官网 + +## 概述 (Overview) + +1. 基于 Redisson 封装的 Redis 集群操作和分布式锁组件,专门用于处理 Redis 集群环境下的分布式锁场景 +2. 核心能力 + * 高性能键值存储:基于RocksDB的高效读写能力,支持快速的数据存取操作。 + * 集群自适应感知:客户端能够感知集群变化并自动刷新配置 + * Redisson 操作:基于 Redisson 提供 Redis 集群操作能力 + * 分布式锁支持:支持多种类型的分布式锁: + ■ 可重入锁(REENTRANT_LOCK) + ■ 公平锁(FAIR_LOCK) + ■ 读锁(READ_LOCK) + ■ 写锁(WRITE_LOCK) + * 注解式锁操作:通过 @LockAction 注解简化分布式锁的使用 + * 灵活配置:支持锁等待时间、自动释放时间、时间单位等参数配置 + * 异常处理:支持锁获取失败时的异常抛出或返回 null 两种处理方式 + * 支持多集群操作redis,也可扩展队列消费 +3. 适用场景:该组件特别适用于需要在 Redis 集群环境下使用分布式锁的企业级应用,通过注解方式简化了分布式锁的使用,提供了多种锁类型以满足不同业务场景的需求 + * 分布式系统:需要在分布式环境下保证数据一致性的场景 + * 高并发业务:需要防止并发操作导致数据不一致的业务场景 + * 集群环境:基于 Redis 集群部署的应用系统 + * 资源竞争控制:需要对共享资源进行并发访问控制的场景 + * 定时任务:分布式环境下需要确保任务单实例执行的定时任务 + * 库存扣减:电商系统中需要防止超卖的库存扣减场景 + * 幂等性保证:需要保证接口幂等性的业务操作。 + +## 如何配置--更多参数参考:RedissonConfig/RedissonProperties + +```yaml +framework: + lingniu: + redisson: + remote: + r1: + # 应用id需在cachecloud申请 + redisAddresses: xxxx:6387 + # 客户端名称,默认为应用名 + IP + clientName: "" + # 单节点 Redis 默认数据库,默认为 0 + database: 0 + # 连接池最小空闲连接数,默认为 12 + connectionMinimumIdleSize: 12 + # 连接池最大连接数,默认为 32 + connectionPoolSize: 32 + # 空闲连接超时时间(毫秒),默认为 10000 + idleConnectionTimeout: 10000 + # 连接超时时间(毫秒),默认为 2000 + connectTimeout: 2000 + # 操作超时时间(毫秒),默认为 2000 + timeout: 2000 + # 操作重试次数,默认为 4 + retryAttempts: 4 + # 集群模式下是否检查 Slots 覆盖,默认为 true + checkSlotsCoverage: true + # 读取模式,默认为 "SLAVE" + # 可选值: + # - SLAVE: 从节点读取 + # - MASTER: 主节点读取 + # - MASTER_SLAVE: 主从节点读取 + readMode: "SLAVE" + # 存储类型,默认为 "CLUSTER" + # 可选值: + # - CLUSTER: 集群模式 + # - SINGLE: 单节点模式 + # - SENTINEL: 哨兵模式 + # - REPLICATED: 复制模式 + storageType: "CLUSTER" + # 实例名称 + r2: + # 应用id需在cachecloud申请 + redisAddresses: xxx:6387 + # 单节点 Redis 默认数据库,默认为 0 + database: 0 + +``` + +## 如何使用--正常操作redis + +```java +@Autowired +private RedissonClient test; +// 或者 +RedissonFactory.getRedissonClient("test").api +``` + +## 如何使用--分布式锁 + +```java +/** + * 使用 #orderId 参数作为锁的key + */ +@PostMapping("/process/{orderId}") +@LockAction( + redissonName = "r1", + key = "#orderId", + lockType = LockType.REENTRANT_LOCK, + waitTime = 3000L, leaseTime = 30000L, unit = TimeUnit.MILLISECONDS, + throwEx = true +) +public ResponseEntity processOrder(@PathVariable String orderId) { + // 业务逻辑 + return ResponseEntity.ok("Order processed: " + orderId); +} + +/** + * 使用请求参数 + */ +@GetMapping("/query") +@LockAction( + redissonName = "defaultRedisson", + key = "'query:' + #userId + ':' + #status", + throwEx = true +) +public ResponseEntity> queryOrders( + @RequestParam String userId, + @RequestParam String status) { + // 业务逻辑 + return ResponseEntity.ok(new ArrayList<>()); +} +``` \ No newline at end of file diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/pom.xml b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/pom.xml new file mode 100644 index 0000000..d7f04fa --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-apollo + ${project.artifactId} + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + com.ctrip.framework.apollo + apollo-client + + + org.javassist + javassist + + + org.springframework.cloud + spring-cloud-context + + + org.springframework.boot + spring-boot-autoconfigure + provided + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + provided + true + + + + + diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/config/ApolloConfig.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/config/ApolloConfig.java new file mode 100644 index 0000000..a1339f3 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/config/ApolloConfig.java @@ -0,0 +1,32 @@ +package cn.lingniu.framework.plugin.apollo.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import javax.validation.constraints.NotBlank; + +@Data +@ConfigurationProperties(prefix = ApolloConfig.PRE_FIX) +public class ApolloConfig { + + public final static String PRE_FIX = "framework.lingniu.apollo"; + + /** + * 是否开始Apollo配置 + */ + private Boolean enabled = true; + /** + * Meta地址 + */ + @NotBlank + private String meta; + /** + * 配置文件列表 以,分割 + */ + private String namespaces = ""; + /** + * 无需配置 app.id 等于 spring.application.name + */ + private String appId; + +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloAnnotationProcessor.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloAnnotationProcessor.java new file mode 100644 index 0000000..60ea82c --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloAnnotationProcessor.java @@ -0,0 +1,79 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigChangeListener; +import com.ctrip.framework.apollo.ConfigService; +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import com.ctrip.framework.apollo.spring.annotation.ApolloConfig; +import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener; +import com.ctrip.framework.apollo.spring.annotation.ApolloProcessor; +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Set; + +/** + * Apollo Annotation Processor for Spring Application + * + * @author Jason Song(song_s@ctrip.com) + */ +public class ApolloAnnotationProcessor extends ApolloProcessor { + + @Override + protected void processField(Object bean, String beanName, Field field) { + ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class); + if (annotation == null) { + return; + } + + Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()), + "Invalid type: %s for field: %s, should be Config", field.getType(), field); + + String namespace = System.getProperty("apollo.bootstrap.namespaces", annotation.value()); + Config config = ConfigService.getConfig(namespace); + + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, bean, config); + } + + @Override + protected void processMethod(final Object bean, String beanName, final Method method) { + ApolloConfigChangeListener annotation = AnnotationUtils + .findAnnotation(method, ApolloConfigChangeListener.class); + if (annotation == null) { + return; + } + Class[] parameterTypes = method.getParameterTypes(); + Preconditions.checkArgument(parameterTypes.length == 1, + "Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length, + method); + Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]), + "Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0], + method); + + ReflectionUtils.makeAccessible(method); + + String namespaceProperties = System.getProperty("apollo.bootstrap.namespaces", String.join(",", annotation.value())); + String[] namespaces = namespaceProperties.split(","); + String[] annotatedInterestedKeys = annotation.interestedKeys(); + String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes(); + ConfigChangeListener configChangeListener = changeEvent -> ReflectionUtils.invokeMethod(method, bean, changeEvent); + + Set interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null; + Set interestedKeyPrefixes = annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes) : null; + + for (String namespace : namespaces) { + Config config = ConfigService.getConfig(namespace); + + if (interestedKeys == null && interestedKeyPrefixes == null) { + config.addChangeListener(configChangeListener); + } else { + config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes); + } + } + } +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloConfigChangeLogListener.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloConfigChangeLogListener.java new file mode 100644 index 0000000..29c5653 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ApolloConfigChangeLogListener.java @@ -0,0 +1,58 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import com.ctrip.framework.apollo.model.ConfigChange; +import com.ctrip.framework.apollo.model.ConfigChangeEvent; +import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.context.scope.refresh.RefreshScope; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +@Component +@Slf4j +public class ApolloConfigChangeLogListener implements ApplicationContextAware { + + @Resource + RefreshScope refreshScope; + + private ApplicationContext applicationContext; + + @ApolloConfigChangeListener + public void onChange(ConfigChangeEvent changeEvent) { + if (log.isInfoEnabled()) { + for (String changedKey : changeEvent.changedKeys()) { + ConfigChange changeInfo = changeEvent.getChange(changedKey); + if (ObjectEmptyUtils.isEmpty(changeInfo)) { + continue; + } + log.info("【apollo 配置变更】 - namespace: {}, property: {}, oldValue: {}, newValue: {}, changeType: {}", + changeInfo.getNamespace(), changeInfo.getPropertyName(), + changeInfo.getOldValue(), changeInfo.getNewValue(), changeInfo.getChangeType()); + } + } + applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); + refreshProperties(changeEvent); + } + + public void refreshProperties(ConfigChangeEvent changeEvent) { + try { + this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); + if (ObjectEmptyUtils.isNotEmpty(refreshScope)) { + refreshScope.refreshAll(); + } + } catch (Exception e) { + log.error("Failed to refresh properties after Apollo config change", e); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ClassPoolUtils.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ClassPoolUtils.java new file mode 100644 index 0000000..b175302 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/ClassPoolUtils.java @@ -0,0 +1,26 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import javassist.ClassPool; +import javassist.LoaderClassPath; + + +public class ClassPoolUtils { + + private static volatile ClassPool instance; + + private ClassPoolUtils() { + } + + public static ClassPool getInstance() { + if (instance == null) { + synchronized (ClassPoolUtils.class) { + if (instance == null) { + instance = ClassPool.getDefault(); + instance.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader())); + } + } + } + return instance; + } + +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloConfigRegistrarHelper.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloConfigRegistrarHelper.java new file mode 100644 index 0000000..b4303a6 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/extend/FrameworkApolloConfigRegistrarHelper.java @@ -0,0 +1,52 @@ +package cn.lingniu.framework.plugin.apollo.extend; + +import com.ctrip.framework.apollo.core.spi.Ordered; +import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValueProcessor; +import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; +import com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor; +import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor; +import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor; +import com.ctrip.framework.apollo.spring.util.BeanRegistrationUtil; +import com.google.common.collect.Lists; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; + +import java.util.HashMap; +import java.util.Map; + +public class FrameworkApolloConfigRegistrarHelper implements com.ctrip.framework.apollo.spring.spi.ApolloConfigRegistrarHelper, Ordered { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + AnnotationAttributes attributes = AnnotationAttributes + .fromMap(importingClassMetadata.getAnnotationAttributes(EnableApolloConfig.class.getName())); + String[] namespaces = System.getProperty("apollo.bootstrap.namespaces", "application").split(","); + //attributes.getStringArray("value"); + int order = attributes.getNumber("order"); + PropertySourcesProcessor.addNamespaces(Lists.newArrayList(namespaces), order); + + Map propertySourcesPlaceholderPropertyValues = new HashMap<>(); + // to make sure the default PropertySourcesPlaceholderConfigurer's priority is higher than PropertyPlaceholderConfigurer + propertySourcesPlaceholderPropertyValues.put("order", 0); + + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class.getName(), + PropertySourcesPlaceholderConfigurer.class, propertySourcesPlaceholderPropertyValues); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesProcessor.class.getName(), + PropertySourcesProcessor.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(), + ApolloAnnotationProcessor.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(), + SpringValueProcessor.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueDefinitionProcessor.class.getName(), + SpringValueDefinitionProcessor.class); + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloJsonValueProcessor.class.getName(), + ApolloJsonValueProcessor.class); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloAutoConfiguration.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloAutoConfiguration.java new file mode 100644 index 0000000..db278ea --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloAutoConfiguration.java @@ -0,0 +1,26 @@ +package cn.lingniu.framework.plugin.apollo.init; + +import cn.lingniu.framework.plugin.apollo.extend.ApolloConfigChangeLogListener; +import cn.lingniu.framework.plugin.apollo.config.ApolloConfig; +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + + +@Slf4j +@Configuration +@EnableApolloConfig +@ConditionalOnProperty(prefix = ApolloConfig.PRE_FIX, name = "enabled", havingValue = "true") +@Import({ApolloConfigChangeLogListener.class}) +public class ApolloAutoConfiguration implements InitializingBean { + + @Override + public void afterPropertiesSet() { + log.info("Apollo configuration enabled(启动), meta URL: {}", PropertyUtils.getProperty("apollo.meta")); + } + +} diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloInit.java b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloInit.java new file mode 100644 index 0000000..81d2f5f --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/java/cn/lingniu/framework/plugin/apollo/init/ApolloInit.java @@ -0,0 +1,114 @@ +package cn.lingniu.framework.plugin.apollo.init; + + +import cn.lingniu.framework.plugin.apollo.config.ApolloConfig; +import cn.lingniu.framework.plugin.apollo.extend.ClassPoolUtils; +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.io.File; + + +/** + * apollo 初始化 + */ +@Slf4j +@Order(Integer.MIN_VALUE + 100) +public class ApolloInit implements ApplicationContextInitializer { + + private static boolean isload = false; + + private String applicationName; + + public final static String PROJECT = "Apollo"; + private final static String AppId = "app.id"; + public final static String ApolloMeta = "apollo.meta"; + private final static String ApolloBootstrapEnabled = "apollo.bootstrap.enabled"; + private final static String ApolloBootstrapNamespaces = "apollo.bootstrap.namespaces"; + private final static String ApolloBootstrapEagerLoadEnabled = "apollo.bootstrap.eagerLoad.enabled"; + private final static String UserDir = "user.dir"; + private final static String ApolloCacheDir = "apollo.cacheDir"; + public final static String Env = "env"; + + public final static String NameSpaces = "framework.lingniu.apollo.namespaces"; + public final static String FrameworkMeta = "framework.lingniu.apollo.meta"; + public final static String FrameworkEnabled = "framework.lingniu.apollo.enabled"; + + + private final static String APOLLO_NAME = ApolloConfig.PRE_FIX + ".name"; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + applicationName = environment.getProperty(APOLLO_NAME, environment.getProperty(CommonConstant.SPRING_APP_NAME_KEY)); + String profile = environment.getProperty(CommonConstant.ACTIVE_PROFILES_PROPERTY); + this.initApolloConfig(environment, applicationName, profile); + } + + + void initApolloConfig(ConfigurableEnvironment environment, String appName, String profile) { + //优先原始配置 + if (!ObjectEmptyUtils.isEmpty(environment.getProperty(ApolloBootstrapEnabled)) + && !ObjectEmptyUtils.isEmpty(environment.getProperty(ApolloMeta)) + && !ObjectEmptyUtils.isEmpty(environment.getProperty(AppId))) { + this.replaceCatInit(environment); + return; + } + if (ObjectEmptyUtils.isEmpty(environment.getProperty(FrameworkEnabled)) || environment.getProperty(FrameworkEnabled).equalsIgnoreCase("false")) { + setDefaultProperty(ApolloBootstrapEnabled, "false"); + setDefaultProperty(ApolloBootstrapEagerLoadEnabled, "false"); + return; + } + //默认设置app.id + PropertyUtils.setDefaultInitProperty(AppId, appName); + setDefaultProperty(ApolloMeta, environment.getProperty(FrameworkMeta)); + setDefaultProperty(ApolloBootstrapEnabled, "true"); + setDefaultProperty(ApolloBootstrapNamespaces, environment.getProperty(NameSpaces, "application")); + setDefaultProperty(ApolloBootstrapEagerLoadEnabled, "true"); + setDefaultProperty("spring.boot.enableautoconfiguration", "true"); + setDefaultProperty(Env, profile.toUpperCase()); + setDefaultProperty(ApolloCacheDir, System.getProperty(UserDir) + File.separator + "apolloConfig" + File.separator); + this.replaceCatInit(environment); + } + + private void replaceCatInit(ConfigurableEnvironment environment) { + try { + ClassPool classPool = ClassPoolUtils.getInstance(); + CtClass ctClass = classPool.get("com.ctrip.framework.apollo.tracer.internals.DefaultMessageProducerManager"); + if (!isload) { + isload = true; + CtConstructor[] constructors = ctClass.getConstructors(); + if (constructors != null && constructors.length > 0) { + CtConstructor constructor = constructors[0]; + constructor.setBody(newMethodCode()); + } + if (ctClass.isFrozen()) { + ctClass.defrost(); + } + ctClass.toClass(); + log.info("{} {} {}", ApolloInit.class, PROJECT, "重写 apollo init ok"); + } + } catch (Exception exp) { + log.error("{} {} {} {}", ApolloInit.class, PROJECT, "重写 apollo init 异常", exp); + } + } + + private String newMethodCode() { + String code = "{" + " producer = new com.ctrip.framework.apollo.tracer.internals.NullMessageProducerManager().getProducer();" + "}"; + return code; + } + + void setDefaultProperty(String key, String defaultPropertyValue) { + PropertyUtils.setDefaultInitProperty(key, defaultPropertyValue); + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ApolloConfigRegistrarHelper b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ApolloConfigRegistrarHelper new file mode 100644 index 0000000..6e69ca4 --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/services/com.ctrip.framework.apollo.spring.spi.ApolloConfigRegistrarHelper @@ -0,0 +1 @@ +cn.lingniu.framework.plugin.apollo.extend.FrameworkApolloConfigRegistrarHelper diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..455cf8f --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.apollo.init.ApolloAutoConfiguration +org.springframework.context.ApplicationContextInitializer=\ +cn.lingniu.framework.plugin.apollo.init.ApolloInit \ No newline at end of file diff --git a/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/apollo-config.md b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/apollo-config.md new file mode 100644 index 0000000..5e7dedc --- /dev/null +++ b/lingniu-framework-plugin/config/lingniu-framework-plugin-apollo/src/main/resources/apollo-config.md @@ -0,0 +1,37 @@ +# 【重要】apllo服务搭建、和详细demo使用教程---参考官网 + +## 概述 (Overview) + +1. 定位:基于 Apollo 封装的分布式配置管理中心组件 +2. 核心能力 + * 集中化配置管理:统一管理不同环境、不同集群的配置 + * 实时推送更新:配置变更后实时推送到应用端,无需重启服务 + * 多环境支持:支持开发、测试、生产等多套环境配置隔离 +3. 适用场景 + * 微服务架构下的配置管理 + * 多环境、多集群的应用配置管理 + * 需要动态调整配置参数的业务场 + * 对配置变更实时性要求较高的系统 + +## 如何配置--参考:ApolloConfig类 + +```yaml +framework: + lingniu: + apollo: + #namespaces必须配置,配置文件列表,以逗号分割 + namespaces: application,applicationTest + # 是否开启 Apollo 配置,默认true + enabled: true + # appId 等于 spring.application.name,无需配置 + appId: lingniu-framework-demo + # meta 地址,框架自动获取,无需配置 + meta: http://xxx:8180 +``` + +## 核心参数描述 + +- app.id:此参数未配置,默认采用:spring.application.name +- framework.lingniu.apollo.meta: 为apollo的eurka注册地址 +- framework.lingniu.apollo.enabled:是否启用 +- framework.lingniu.apollo.namespaces 可配置多个配置文件 \ No newline at end of file diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/pom.xml b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/pom.xml new file mode 100644 index 0000000..cee1683 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-mybatis + lingniu-framework-plugin-mybatis + http://maven.apache.org + + UTF-8 + 4.2.0 + + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-autoconfigure + + + org.bouncycastle + bcprov-jdk15on + + + org.springframework.boot + spring-boot-starter-jdbc + provided + + + org.springframework.boot + spring-boot-starter-aop + provided + + + + javax.persistence + persistence-api + + + org.mybatis + mybatis-typehandlers-jsr310 + + + mysql + mysql-connector-java + + + com.baomidou + dynamic-datasource-spring-boot-starter + ${dynamic-datasource-starter.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus-boot-starter.version} + + + ojdbc + ojdbc6 + true + + + + javax.servlet + javax.servlet-api + provided + true + + + com.alibaba + druid-spring-boot-starter + + + com.github.pagehelper + pagehelper-spring-boot-starter + + + tk.mybatis + mapper-spring-boot-starter + ${tk.mapper-spring-boot.version} + + + mybatis-spring + org.mybatis + + + + + com.google.guava + guava + + + + diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DBSqlLogInterceptor.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DBSqlLogInterceptor.java new file mode 100644 index 0000000..8cd7973 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DBSqlLogInterceptor.java @@ -0,0 +1,181 @@ +package cn.lingniu.framework.plugin.mybatis.base; + +import cn.lingniu.framework.plugin.mybatis.config.DataSourceConfig; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.core.toolkit.SystemClock; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Plugin; +import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.SystemMetaObject; +import org.apache.ibatis.session.ResultHandler; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +/** + * 输出SQL 语句及其执行时间 + */ +@Slf4j +@Intercepts({ + @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), + @Signature(type = StatementHandler.class, method = "update", args = Statement.class), + @Signature(type = StatementHandler.class, method = "batch", args = Statement.class) +}) +@RequiredArgsConstructor +public class DBSqlLogInterceptor implements Interceptor { + private static final String DRUID_POOLED_PREPARED_STATEMENT = "com.alibaba.druid.pool.DruidPooledPreparedStatement"; + private static final String T4C_PREPARED_STATEMENT = "oracle.jdbc.driver.T4CPreparedStatement"; + private static final String ORACLE_PREPARED_STATEMENT_WRAPPER = "oracle.jdbc.driver.OraclePreparedStatementWrapper"; + private Method oracleGetOriginalSqlMethod; + private Method druidGetSqlMethod; + private final DataSourceConfig dataSourceConfig; + + @Override + public Object intercept(Invocation invocation) throws Throwable { + if (!dataSourceConfig.isSqlLog()) { + return invocation.proceed(); + } + Statement statement; + Object firstArg = invocation.getArgs()[0]; + if (Proxy.isProxyClass(firstArg.getClass())) { + statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement"); + } else { + statement = (Statement) firstArg; + } + MetaObject stmtMetaObj = SystemMetaObject.forObject(statement); + try { + statement = (Statement) stmtMetaObj.getValue("stmt.statement"); + } catch (Exception e) { + // do nothing + } + if (stmtMetaObj.hasGetter("delegate")) {//Hikari + try { + statement = (Statement) stmtMetaObj.getValue("delegate"); + } catch (Exception ignored) { + // do nothing + } + } + String originalSql = null; + String stmtClassName = statement.getClass().getName(); + if (DRUID_POOLED_PREPARED_STATEMENT.equals(stmtClassName)) { + try { + if (druidGetSqlMethod == null) { + Class clazz = Class.forName(DRUID_POOLED_PREPARED_STATEMENT); + druidGetSqlMethod = clazz.getMethod("getSql"); + } + Object stmtSql = druidGetSqlMethod.invoke(statement); + if (stmtSql instanceof String) { + originalSql = (String) stmtSql; + } + } catch (Exception e) { + e.printStackTrace(); + } + } else if (T4C_PREPARED_STATEMENT.equals(stmtClassName) + || ORACLE_PREPARED_STATEMENT_WRAPPER.equals(stmtClassName)) { + try { + if (oracleGetOriginalSqlMethod != null) { + Object stmtSql = oracleGetOriginalSqlMethod.invoke(statement); + if (stmtSql instanceof String) { + originalSql = (String) stmtSql; + } + } else { + Class clazz = Class.forName(stmtClassName); + oracleGetOriginalSqlMethod = getMethodRegular(clazz, "getOriginalSql"); + if (oracleGetOriginalSqlMethod != null) { + //OraclePreparedStatementWrapper is not a public class, need set this. + oracleGetOriginalSqlMethod.setAccessible(true); + if (null != oracleGetOriginalSqlMethod) { + Object stmtSql = oracleGetOriginalSqlMethod.invoke(statement); + if (stmtSql instanceof String) { + originalSql = (String) stmtSql; + } + } + } + } + } catch (Exception e) { + //ignore + } + } + if (originalSql == null) { + originalSql = statement.toString(); + } + originalSql = originalSql.replaceAll("[\\s]+", StringPool.SPACE); + int index = indexOfSqlStart(originalSql); + if (index > 0) { + originalSql = originalSql.substring(index); + } + long start = SystemClock.now(); + Object result = invocation.proceed(); + long timing = SystemClock.now() - start; + Object target = PluginUtils.realTarget(invocation.getTarget()); + MetaObject metaObject = SystemMetaObject.forObject(target); + MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); + // 打印 sql + String sqlLogger = "\nsql日志执行日志:开始==============" + + "\nExecute ID :{}" + + "\nExecute SQL :{}" + + "\nExecute Time:{} ms" + + "\nsql日志执行日志:结束 ==============\n"; + if (log.isInfoEnabled()) { + log.info(sqlLogger, ms.getId(), originalSql, timing); + } + return result; + } + + @Override + public Object plugin(Object target) { + if (target instanceof StatementHandler) { + return Plugin.wrap(target, this); + } + return target; + } + + @Override + public void setProperties(Properties properties) { + } + + private Method getMethodRegular(Class clazz, String methodName) { + if (Object.class.equals(clazz)) { + return null; + } + for (Method method : clazz.getDeclaredMethods()) { + if (method.getName().equals(methodName)) { + return method; + } + } + return getMethodRegular(clazz.getSuperclass(), methodName); + } + + private int indexOfSqlStart(String sql) { + String upperCaseSql = sql.toUpperCase(); + Set set = new HashSet<>(); + set.add(upperCaseSql.indexOf("SELECT ")); + set.add(upperCaseSql.indexOf("UPDATE ")); + set.add(upperCaseSql.indexOf("INSERT ")); + set.add(upperCaseSql.indexOf("DELETE ")); + set.remove(-1); + if (CollectionUtils.isEmpty(set)) { + return -1; + } + List list = new ArrayList<>(set); + list.sort(Comparator.naturalOrder()); + return list.get(0); + } + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DataSourceEnum.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DataSourceEnum.java new file mode 100644 index 0000000..bf42e2d --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DataSourceEnum.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.mybatis.base; + +/** + * 对应于多数据源中不同数据源配置 + *

+ * 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。 + * 注意,默认是 {@link #MASTER} 数据源 + *

+ * 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html + */ +public interface DataSourceEnum { + + /** + * 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解 + */ + String MASTER = "master"; + /** + * 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解 + */ + String SLAVE = "slave"; + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DruidAdRemoveFilter.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DruidAdRemoveFilter.java new file mode 100644 index 0000000..cc989d4 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/base/DruidAdRemoveFilter.java @@ -0,0 +1,36 @@ +package cn.lingniu.framework.plugin.mybatis.base; + +import com.alibaba.druid.util.Utils; +import org.springframework.web.filter.OncePerRequestFilter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Druid 底部广告过滤器 + * + */ +public class DruidAdRemoveFilter extends OncePerRequestFilter { + + /** + * common.js 的路径 + */ + private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取 common.js + String text = Utils.readFromResource(COMMON_JS_ILE_PATH); + // 正则替换 banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/config/DataSourceConfig.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/config/DataSourceConfig.java new file mode 100644 index 0000000..cbe4f2b --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/config/DataSourceConfig.java @@ -0,0 +1,29 @@ +package cn.lingniu.framework.plugin.mybatis.config; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 数据源配置--mybatis, mybatis-plus, dynamic-datasource, tk-mapper 配置参数整合 + **/ +@Data +@ConfigurationProperties(prefix = DataSourceConfig.PRE_FIX) +public class DataSourceConfig { + + public static final String PRE_FIX = "framework.lingniu.datasource"; + + /** + * sql分析日志打印, 默认关闭 + */ + private boolean sqlLog = false; + /** + * druid 账号, 默认 SpringApplicationConfig#name + */ + private String druidUsername; + /** + * druid 密码, 默认 SpringApplicationConfig#name + 123 + */ + private String druidPassword; + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceAutoConfiguration.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceAutoConfiguration.java new file mode 100644 index 0000000..25abd07 --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceAutoConfiguration.java @@ -0,0 +1,67 @@ +package cn.lingniu.framework.plugin.mybatis.init; + +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.mybatis.base.DBSqlLogInterceptor; +import cn.lingniu.framework.plugin.mybatis.base.DruidAdRemoveFilter; +import cn.lingniu.framework.plugin.mybatis.config.DataSourceConfig; +import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; +import com.alibaba.druid.support.http.ResourceServlet; +import com.alibaba.druid.support.http.StatViewServlet; +import com.alibaba.druid.support.http.WebStatFilter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Optional; + +@Configuration +@EnableConfigurationProperties({DataSourceConfig.class}) +public class DataSourceAutoConfiguration { + + @Bean + public DBSqlLogInterceptor sqlLogInterceptor(DataSourceConfig dataSourceConfig) { + return new DBSqlLogInterceptor(dataSourceConfig); + } + + @Bean(name = "druidStatView") + public ServletRegistrationBean druidStatView(DataSourceConfig dataSourceConfig) { + ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); + registrationBean.addInitParameter(ResourceServlet.PARAM_NAME_ALLOW, ""); + registrationBean.addInitParameter(StatViewServlet.PARAM_NAME_DENY, ""); + registrationBean.addInitParameter(StatViewServlet.PARAM_NAME_USERNAME, Optional.ofNullable(dataSourceConfig.getDruidUsername()).orElseGet(()-> ApplicationNameContext.getApplicationName())); + registrationBean.addInitParameter(StatViewServlet.PARAM_NAME_PASSWORD, Optional.ofNullable(dataSourceConfig.getDruidPassword()).orElseGet(() -> ApplicationNameContext.getApplicationName() + "123")); + registrationBean.addInitParameter(StatViewServlet.PARAM_NAME_RESET_ENABLE, "false"); + return registrationBean; + } + + @Bean(name = "druidWebStatFilter") + public FilterRegistrationBean druidWebStatFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new WebStatFilter()); + registrationBean.addUrlPatterns("/*"); + registrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"); + return registrationBean; + } + + + /** + * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告 + */ + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true") + public FilterRegistrationBean druidAdRemoveFilterFilter(DruidStatProperties properties) { + // 获取 druid web 监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取 common.js 的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + // 创建 DruidAdRemoveFilter Bean + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new DruidAdRemoveFilter()); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceInit.java b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceInit.java new file mode 100644 index 0000000..4ca6c8b --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/java/cn/lingniu/framework/plugin/mybatis/init/DataSourceInit.java @@ -0,0 +1,19 @@ +package cn.lingniu.framework.plugin.mybatis.init; + +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.Order; + +/** + * 数据源属性初始化--处理监控开关 + */ +@Order(Integer.MIN_VALUE + 100) +public class DataSourceInit implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + PropertyUtils.setDefaultInitProperty("management.health.db.enabled", "false"); + } + +} diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..aa46a1c --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.mybatis.init.DataSourceAutoConfiguration +org.springframework.context.ApplicationContextInitializer=\ +cn.lingniu.framework.plugin.mybatis.init.DataSourceInit \ No newline at end of file diff --git a/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/mybatis-config.md b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/mybatis-config.md new file mode 100644 index 0000000..4692f8c --- /dev/null +++ b/lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis/src/main/resources/mybatis-config.md @@ -0,0 +1,65 @@ +# 【重要】基于 Mybatis-Plus + dynamic-datasource + tk 详细使用参考官网---参考官网 + +## 概述 (Overview) + +1. 基于 Mybatis-Plus + dynamic-datasource + tk 封装的数据源交互工具 +2. 核心能力 + * 数据源CRUD操作,支持mysql, oracle, clickhouse 等 + * 多数据源支持 + * 分页查询 + * Sql语句执行分析 + * 默认集成druid +3. 适用场景: + * 依赖外部数据源场景 + * 关系型数据库场景 + +## 如何配置--更多参数参考:RedissonConfig/RedissonProperties + +```yaml +spring: + application: + name: lingniu-framework-demo + profiles: + active: dev,redisson,jetcache,xxljob,db + #todo db配置 + datasource: + dynamic: + primary: master + datasource: + #如需连接mysql,需修改下面的配置 + master: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb + username: sa + password: sa + slaver: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb + username: sa + password: sa + # http://localhost:8080/h2-console 访问 h2 数据库 账号: sa/sa + h2: + console: + enabled: true + path: /h2-console +framework: + lingniu: + # 框架数据源配置 + datasource: + # 开启框架 sql执行分析 + sqlLog: true + # druid 后台地址:http://localhost:8080/druid/login.html + # druid 账号,默认值为 SpringApplicationProperties#name + druidUsername: "your_druid_username" + # druid 密码,默认值为 SpringApplicationProperties#name + "123" + druidPassword: "your_druid_password" +``` + +## druid 配置请参考:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter + +## 如何使用--Mybatis基本功能 + +1. 基本功能使用可参考Mybatis-Plus: https://www.baomidou.com +2. 默认配置下 在 src/main/resources/mapper/ 文件夹下定义 xml 文件 +3. 定义 java 接口 继承 com.baomidou.mybatisplus.core.mapper.BaseMapper +4. 使用@DS 注解实现数据源切换:@DS("master") diff --git a/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/pom.xml b/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/pom.xml new file mode 100644 index 0000000..0e97038 --- /dev/null +++ b/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-easyexcel + ${project.artifactId} + + + + com.alibaba + easyexcel + + + + diff --git a/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/src/main/resources/file.md b/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/src/main/resources/file.md new file mode 100644 index 0000000..f49b36b --- /dev/null +++ b/lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel/src/main/resources/file.md @@ -0,0 +1,3 @@ +# easyexcel * easyexcel 性能好,api简单,上手更简单 + +# https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write diff --git a/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/pom.xml b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/pom.xml new file mode 100644 index 0000000..0c14354 --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-xxljob + ${project.artifactId} + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + org.springframework.boot + spring-boot-autoconfigure + + + com.xuxueli + xxl-job-core + + + com.alibaba + fastjson + + + + + diff --git a/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/config/XxlJobConfig.java b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/config/XxlJobConfig.java new file mode 100644 index 0000000..cdff222 --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/config/XxlJobConfig.java @@ -0,0 +1,64 @@ +package cn.lingniu.framework.plugin.xxljob.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Data +@ConfigurationProperties(XxlJobConfig.PRE_FIX) +public class XxlJobConfig { + + public final static String PRE_FIX = "framework.lingniu.xxljob"; + + /** + * xxl job admin properties. + */ + private AdminProperties admin = new AdminProperties(); + /** + * xxl job executor properties. + */ + private ExecutorProperties executor = new ExecutorProperties(); + + + /** + * xxl-job admin properties. + */ + @Data + static public class AdminProperties { + /** + * xxl job admin address. + */ + private String adminAddresses = "http://xxx:8080/xxl-job-admin"; + /** + * xxl job admin registry access token. + */ + private String accessToken; + } + + /** + * xxl-job executor properties. + */ + @Data + static public class ExecutorProperties { + /** + * xxl job registry name. [等于 spring.application.name] ApplicationNameContext.getApplicationName() + */ + private String appName = "xxl-job-executor"; + /** + * xxl job registry ip. + */ + private String ip; + /** + * xxl job registry port. + */ + private Integer port = 9999; + /** + * xxl job log files path. todo 注意权限问题 + */ + private String logPath = "logs/applogs/xxl-job/jobhandler"; + /** + * xxl job log files retention days. + */ + private Integer logRetentionDays = 7; + } + +} diff --git a/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/init/XxlJobAutoConfiguration.java b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/init/XxlJobAutoConfiguration.java new file mode 100644 index 0000000..783daca --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/java/cn/lingniu/framework/plugin/xxljob/init/XxlJobAutoConfiguration.java @@ -0,0 +1,41 @@ +package cn.lingniu.framework.plugin.xxljob.init; + +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.util.json.JsonUtil; +import cn.lingniu.framework.plugin.xxljob.config.XxlJobConfig; +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Slf4j +@EnableConfigurationProperties({XxlJobConfig.class}) +public class XxlJobAutoConfiguration { + + @Bean(initMethod = "start", destroyMethod = "destroy") + public XxlJobSpringExecutor xxlJobSpringExecutor(XxlJobConfig prop) { + validateXxlJobConfig(prop); + + XxlJobSpringExecutor executor = new XxlJobSpringExecutor(); + executor.setAdminAddresses(prop.getAdmin().getAdminAddresses()); + executor.setAccessToken(prop.getAdmin().getAccessToken()); + + executor.setPort(prop.getExecutor().getPort()); + executor.setLogRetentionDays(prop.getExecutor().getLogRetentionDays()); + executor.setLogPath(prop.getExecutor().getLogPath()); + executor.setIp(prop.getExecutor().getIp()); + executor.setAppname(ApplicationNameContext.getApplicationName()); + + log.info("XxlJob configuration initialized: {}", JsonUtil.bean2Json(prop)); + return executor; + } + + private void validateXxlJobConfig(XxlJobConfig prop) { + if (prop.getAdmin() == null || prop.getExecutor() == null) { + throw new IllegalArgumentException("XxlJob configuration cannot be null"); + } + } + +} diff --git a/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..b545551 --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.xxljob.init.XxlJobAutoConfiguration \ No newline at end of file diff --git a/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/xxl-job.md b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/xxl-job.md new file mode 100644 index 0000000..5ec0eed --- /dev/null +++ b/lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob/src/main/resources/xxl-job.md @@ -0,0 +1,43 @@ +# 【重要】xxl-job服务搭建、和详细demo使用教程---参考官网 + +## 概述 (Overview) + +1. 定位: 基于 xxljob 封装的分布式定时任务开发工具 +2. 核心能力: + * 分布式任务调度:基于 XXL-JOB 实现任务的分布式执行和分片处理。 + * 动态调度:支持任务的动态添加、修改和删除,无需重启服务。 + * 故障转移:自动检测任务执行失败并重新分配,确保任务高可用。 + * 监控与管理:提供任务执行日志、状态监控和告警功能。 + * 插件化集成:通过插件机制与主框架无缝对接,支持动态加载和卸载。 +3. 适用场景: + * 定时任务:如报表生成、数据清理等周期性任务。 + * 分布式计算:需要分片处理大量数据的场景。 + * 高可用需求:确保任务在节点故障时自动恢复。 + * 动态调整:需要灵活调整任务执行策略的场景。 + +## 如何配置--参考:XxlJobConfig + +```yaml + +framework: + lingniu: + # XXL-JOB 配置 + xxljob: + # 调度中心配置 + admin: + # 调度中心部署根地址(多个地址用逗号分隔,为空则关闭自动注册) + adminAddresses: "http://xxx:8099/xxl-job-admin" + # 调度中心通讯TOKEN(非空时启用) + accessToken: "default_token" + + # 执行器配置 + executor: + # 执行器IP(默认为空表示自动获取IP) + ip: "" + # 执行器端口号(小于等于0则自动获取,默认9999) + port: 9999 + # 执行器日志文件保存天数(大于等于3时生效,-1表示关闭自动清理,默认7天) + logRetentionDays: 7 + # 执行器运行日志文件存储磁盘路径(需对该路径拥有读写权限,为空则使用默认路径) + logPath: logs/applogs/xxl-job/jobhandler +``` \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/pom.xml b/lingniu-framework-plugin/lingniu-framework-plugin-core/pom.xml new file mode 100644 index 0000000..e6deef2 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-core + ${project.artifactId} + + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + joda-time + joda-time + + + org.db4j + reflectasm + + + org.springframework.boot + spring-boot + compile + + + com.fasterxml.jackson.core + jackson-databind + compile + + + org.slf4j + slf4j-api + + + + org.apache.commons + commons-lang3 + + + commons-collections + commons-collections + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + compile + true + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + compile + + + javax.servlet + javax.servlet-api + compile + + + one.util + streamex + + + org.jooq + jool + + + org.springframework.boot + spring-boot-autoconfigure + compile + true + + + + + diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/CommonResult.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/CommonResult.java new file mode 100644 index 0000000..314e6a7 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/CommonResult.java @@ -0,0 +1,121 @@ +package cn.lingniu.framework.plugin.core.base; + +import cn.hutool.core.lang.Assert; +import cn.lingniu.framework.plugin.core.exception.ErrorCode; +import cn.lingniu.framework.plugin.core.exception.ServiceException; +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import cn.lingniu.framework.plugin.core.exception.util.ServiceExceptionUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + * + * @see ErrorCode#getCode() + */ + private Integer code; + /** + * 错误提示,用户可阅读 + * + * @see ErrorCode#getMsg() () + */ + private String msg; + /** + * 返回数据 + */ + private T data; + + /** + * 将传入的 result 对象,转换成另外一个泛型结果的对象 + * + * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 + * + * @param result 传入的 result 对象 + * @param 返回的泛型 + * @return 新的 CommonResult 对象 + */ + public static CommonResult error(CommonResult result) { + return error(result.getCode(), result.getMsg()); + } + + public static CommonResult error(Integer code, String message) { + Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + return result; + } + + public static CommonResult error(ErrorCode errorCode, Object... params) { + Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), errorCode.getCode(), "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = errorCode.getCode(); + result.msg = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), params); + return result; + } + + public static CommonResult error(ErrorCode errorCode) { + return error(errorCode.getCode(), errorCode.getMsg()); + } + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); + result.data = data; + result.msg = ""; + return result; + } + + public static boolean isSuccess(Integer code) { + return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isSuccess() { + return isSuccess(code); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isError() { + return !isSuccess(); + } + + // ========= 和 Exception 异常体系集成 ========= + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + */ + public void checkError() throws ServiceException { + if (isSuccess()) { + return; + } + // 业务异常 + throw new ServiceException(code, msg); + } + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + * 如果没有,则返回 {@link #data} 数据 + */ + @JsonIgnore // 避免 jackson 序列化 + public T getCheckedData() { + checkError(); + return data; + } + + public static CommonResult error(ServiceException serviceException) { + return error(serviceException.getCode(), serviceException.getMessage()); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/TerminalEnum.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/TerminalEnum.java new file mode 100644 index 0000000..85c3519 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/TerminalEnum.java @@ -0,0 +1,38 @@ +package cn.lingniu.framework.plugin.core.base; + +import cn.lingniu.framework.plugin.util.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 终端的枚举 + */ +@RequiredArgsConstructor +@Getter +public enum TerminalEnum implements ArrayValuable { + + UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它 + WECHAT_MINI_PROGRAM(10, "微信小程序"), + WECHAT_WAP(11, "微信公众号"), + H5(20, "H5 网页"), + APP(31, "手机 App"), + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(TerminalEnum::getTerminal).toArray(Integer[]::new); + + /** + * 终端 + */ + private final Integer terminal; + /** + * 终端名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/WebFilterOrderEnum.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/WebFilterOrderEnum.java new file mode 100644 index 0000000..d6f0949 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/base/WebFilterOrderEnum.java @@ -0,0 +1,35 @@ +package cn.lingniu.framework.plugin.core.base; + +/** + * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 + * + * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enums 包下 + * + */ +public interface WebFilterOrderEnum { + + int CORS_FILTER = Integer.MIN_VALUE; + + int TRACE_FILTER = CORS_FILTER + 1; + + int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; + + int API_ENCRYPT_FILTER = REQUEST_BODY_CACHE_FILTER + 1; + + // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 + + int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 + + int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面 + + int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面 + + // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类 + + int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面 + + int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面 + + int DEMO_FILTER = Integer.MAX_VALUE; + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/config/CommonConstant.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/config/CommonConstant.java new file mode 100644 index 0000000..c3400dd --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/config/CommonConstant.java @@ -0,0 +1,16 @@ +package cn.lingniu.framework.plugin.core.config; + +import lombok.experimental.UtilityClass; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +@UtilityClass +public class CommonConstant { + //Spring 应用名 + public static final String SPRING_APP_NAME_KEY = "spring.application.name"; + //active profiles + public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active"; + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/ApplicationNameContext.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/ApplicationNameContext.java new file mode 100644 index 0000000..6e8dfec --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/ApplicationNameContext.java @@ -0,0 +1,15 @@ +package cn.lingniu.framework.plugin.core.context; + +public abstract class ApplicationNameContext { + + private static String applicationName; + + public static String getApplicationName() { + return applicationName; + } + + public static void setApplicationName(String applicationName) { + ApplicationNameContext.applicationName = applicationName; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/SpringBeanApplicationContext.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/SpringBeanApplicationContext.java new file mode 100644 index 0000000..f11cda7 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/context/SpringBeanApplicationContext.java @@ -0,0 +1,60 @@ +package cn.lingniu.framework.plugin.core.context; + + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider;import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.SpringBootVersion; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import java.util.Map; + +/** + * spring bean 的所有bean存储 + */ +@Slf4j +public class SpringBeanApplicationContext implements ApplicationContextAware { + + private static ApplicationContext applicationContext = null; + + private static final String SPRING_BOOT_VERSION = SpringBootVersion.getVersion(); + + + private static String applicationName; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + SpringBeanApplicationContext.applicationContext = applicationContext; + } + + public static Object getBean(String beanName) throws BeansException { + return applicationContext.getBean(beanName); + } + + public static T getBean(Class clazz) throws BeansException { + return applicationContext.getBean(clazz); + } + + public static String getSpringBootVersion() { + return SPRING_BOOT_VERSION; + } + + public static ObjectProvider getBeanProvider(Class clazz) throws BeansException { + return applicationContext.getBeanProvider(clazz); + } + + public static T getBean(String beanName, Class clazz) throws BeansException { + return applicationContext.getBean(beanName, clazz); + } + + public static Map getBeans(Class clazz) throws BeansException { + return applicationContext.getBeansOfType(clazz); + } + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ErrorCode.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ErrorCode.java new file mode 100644 index 0000000..566b512 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ErrorCode.java @@ -0,0 +1,30 @@ +package cn.lingniu.framework.plugin.core.exception; + +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import cn.lingniu.framework.plugin.core.exception.enums.ServiceErrorCodeRange; +import lombok.Data; + +/** + * 错误码对象 + * + * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} + * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} + */ +@Data +public class ErrorCode { + + /** + * 错误码 + */ + private final Integer code; + /** + * 错误提示 + */ + private final String msg; + + public ErrorCode(Integer code, String message) { + this.code = code; + this.msg = message; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServerException.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServerException.java new file mode 100644 index 0000000..5fb7d1c --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServerException.java @@ -0,0 +1,60 @@ +package cn.lingniu.framework.plugin.core.exception; + +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 服务器异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServerException extends RuntimeException { + + /** + * 全局错误码 + * + * @see GlobalErrorCodeConstants + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServerException() { + } + + public ServerException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMsg(); + } + + public ServerException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServerException setCode(Integer code) { + this.code = code; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public ServerException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServiceException.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServiceException.java new file mode 100644 index 0000000..ed49df3 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/ServiceException.java @@ -0,0 +1,60 @@ +package cn.lingniu.framework.plugin.core.exception; + +import cn.lingniu.framework.plugin.core.exception.enums.ServiceErrorCodeRange; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务逻辑异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServiceException extends RuntimeException { + + /** + * 业务错误码 + * + * @see ServiceErrorCodeRange + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() { + } + + public ServiceException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMsg(); + } + + public ServiceException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServiceException setCode(Integer code) { + this.code = code; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public ServiceException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/GlobalErrorCodeConstants.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/GlobalErrorCodeConstants.java new file mode 100644 index 0000000..d0c2b82 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/GlobalErrorCodeConstants.java @@ -0,0 +1,39 @@ +package cn.lingniu.framework.plugin.core.exception.enums; + + +import cn.lingniu.framework.plugin.core.exception.ErrorCode; + +/** + * 全局错误码枚举 + * 0-999 系统异常编码保留 + * + * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status + * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 + * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 + */ +public interface GlobalErrorCodeConstants { + + ErrorCode SUCCESS = new ErrorCode(0, "成功"); + + // ========== 客户端错误段 ========== + + ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); + ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); + ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); + ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); + ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); + ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许 + ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); + ErrorCode TIME_OUT_ERROR = new ErrorCode(430, "请求超时!"); + + // ========== 服务端错误段 ========== + + ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); + ErrorCode BAD_GATE_WAY_ERROR = new ErrorCode(502, "服务端Bad Gateway异常"); + + // ========== 自定义错误段 ========== + + ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); + ErrorCode FEIGN_DECODE_ERROR = new ErrorCode(900, "Feign异常解码错误"); + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/ServiceErrorCodeRange.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/ServiceErrorCodeRange.java new file mode 100644 index 0000000..b2f7515 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/enums/ServiceErrorCodeRange.java @@ -0,0 +1,11 @@ +package cn.lingniu.framework.plugin.core.exception.enums; + +/** + * todo 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 + * 抽象类,业务可自定义继承 + */ +public abstract class ServiceErrorCodeRange { + + + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/util/ServiceExceptionUtil.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/util/ServiceExceptionUtil.java new file mode 100644 index 0000000..e268ba6 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/exception/util/ServiceExceptionUtil.java @@ -0,0 +1,76 @@ +package cn.lingniu.framework.plugin.core.exception.util; + +import cn.lingniu.framework.plugin.core.exception.ErrorCode; +import cn.lingniu.framework.plugin.core.exception.ServiceException; +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +/** + * {@link ServiceException} 工具类 + * + * 目的在于,格式化异常信息提示。 + * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 + */ +@Slf4j +public class ServiceExceptionUtil { + + // ========== 和 ServiceException 的集成 ========== + + public static ServiceException exception(ErrorCode errorCode) { + return exception0(errorCode.getCode(), errorCode.getMsg()); + } + + public static ServiceException exception(ErrorCode errorCode, Object... params) { + return exception0(errorCode.getCode(), errorCode.getMsg(), params); + } + + public static ServiceException exception0(Integer code, String messagePattern, Object... params) { + String message = doFormat(code, messagePattern, params); + return new ServiceException(code, message); + } + + public static ServiceException invalidParamException(String messagePattern, Object... params) { + return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params); + } + + // ========== 格式化方法 ========== + + /** + * 将错误编号对应的消息使用 params 进行格式化。 + * + * @param code 错误编号 + * @param messagePattern 消息模版 + * @param params 参数 + * @return 格式化后的提示 + */ + @VisibleForTesting + public static String doFormat(int code, String messagePattern, Object... params) { + StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); + int i = 0; + int j; + int l; + for (l = 0; l < params.length; l++) { + j = messagePattern.indexOf("{}", i); + if (j == -1) { + log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + if (i == 0) { + return messagePattern; + } else { + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + } else { + sbuf.append(messagePattern, i, j); + sbuf.append(params[l]); + i = j + 2; + } + } + if (messagePattern.indexOf("{}", i) != -1) { + log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + } + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/FrameworkSystemInitContextInitializer.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/FrameworkSystemInitContextInitializer.java new file mode 100644 index 0000000..ca3e892 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/FrameworkSystemInitContextInitializer.java @@ -0,0 +1,43 @@ +package cn.lingniu.framework.plugin.core.init; + +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * 优先级最高 + */ +@Configuration +@Slf4j +public class FrameworkSystemInitContextInitializer implements EnvironmentPostProcessor, Ordered { + + private static boolean initialized = false; + + private final static String APPLICATION_NAME = CommonConstant.SPRING_APP_NAME_KEY; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (initialized) { + return; + } + initialized = true; + String applicationName = environment.getProperty(APPLICATION_NAME, ""); + if (ObjectEmptyUtils.isEmpty(applicationName)) { + throw new IllegalArgumentException("[框架异常][" + CommonConstant.SPRING_APP_NAME_KEY + "] is not set-必须配置!"); + } else { + ApplicationNameContext.setApplicationName(applicationName); + } + } + + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE - 100000; + } +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextCondition.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextCondition.java new file mode 100644 index 0000000..02361b7 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextCondition.java @@ -0,0 +1,16 @@ +package cn.lingniu.framework.plugin.core.init.applicationContext; + +import cn.lingniu.framework.plugin.core.context.SpringBeanApplicationContext; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + + +public class SpringApplicationContextCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return SpringBeanApplicationContext.getApplicationContext() == null; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextConfiguration.java b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextConfiguration.java new file mode 100644 index 0000000..3534c90 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/java/cn/lingniu/framework/plugin/core/init/applicationContext/SpringApplicationContextConfiguration.java @@ -0,0 +1,25 @@ +package cn.lingniu.framework.plugin.core.init.applicationContext; + +import cn.lingniu.framework.plugin.core.context.SpringBeanApplicationContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +@Configuration +public class SpringApplicationContextConfiguration implements EnvironmentAware { + + @Bean + @Conditional(SpringApplicationContextCondition.class) + public SpringBeanApplicationContext springApplicationContext(ApplicationContext applicationContext) { + SpringBeanApplicationContext context = new SpringBeanApplicationContext(); + context.setApplicationContext(applicationContext); + return context; + } + + @Override + public void setEnvironment(Environment event) { + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..13044ad --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,6 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ +cn.lingniu.framework.plugin.core.init.FrameworkSystemInitContextInitializer + + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.core.init.applicationContext.SpringApplicationContextConfiguration \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/core.md b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/core.md new file mode 100644 index 0000000..9391c8c --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-core/src/main/resources/core.md @@ -0,0 +1,7 @@ +# 框架核心模块 + +## ApplicationNameContext 初始化 + +## 异常定义 + +## 等 \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/pom.xml b/lingniu-framework-plugin/lingniu-framework-plugin-util/pom.xml new file mode 100644 index 0000000..e46a74d --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-util + ${project.artifactId} + + + + + org.springframework.boot + spring-boot + + + org.springframework + spring-webmvc + compile + + + org.aspectj + aspectjweaver + provided + + + org.springframework + spring-core + + + org.springframework + spring-aop + + + org.springframework.boot + spring-boot-autoconfigure + compile + true + + + cn.hutool + hutool-all + + + com.google.guava + guava + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.apache.commons + commons-lang3 + + + commons-codec + commons-codec + + + cn.hutool + hutool-core + + + org.hibernate.validator + hibernate-validator + + + joda-time + joda-time + + + org.slf4j + slf4j-api + + + javax.servlet + javax.servlet-api + provided + true + + + + diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/cache/CacheUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/cache/CacheUtils.java new file mode 100644 index 0000000..2654297 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/cache/CacheUtils.java @@ -0,0 +1,59 @@ +package cn.lingniu.framework.plugin.util.cache; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import java.time.Duration; +import java.util.concurrent.Executors; + +/** + * Cache 工具类 + */ +public class CacheUtils { + + /** + * 异步刷新的 LoadingCache 最大缓存数量 + * + * @see 本地缓存 CacheUtils 工具类建议 + */ + private static final Integer CACHE_MAX_SIZE = 10000; + + /** + * 构建异步刷新的 LoadingCache 对象 + * + * 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法 + * + * 或者简单理解: + * 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法 + * 2、和“全局”、“系统”相关的,使用当前缓存方法 + * + * @param duration 过期时间 + * @param loader CacheLoader 对象 + * @return LoadingCache 对象 + */ + public static LoadingCache buildAsyncReloadingCache(Duration duration, CacheLoader loader) { + return CacheBuilder.newBuilder() + .maximumSize(CACHE_MAX_SIZE) + // 只阻塞当前数据加载线程,其他线程返回旧值 + .refreshAfterWrite(duration) + // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程 + .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); + } + + /** + * 构建同步刷新的 LoadingCache 对象 + * + * @param duration 过期时间 + * @param loader CacheLoader 对象 + * @return LoadingCache 对象 + */ + public static LoadingCache buildCache(Duration duration, CacheLoader loader) { + return CacheBuilder.newBuilder() + .maximumSize(CACHE_MAX_SIZE) + // 只阻塞当前数据加载线程,其他线程返回旧值 + .refreshAfterWrite(duration) + .build(loader); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/ArrayUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/ArrayUtils.java new file mode 100644 index 0000000..afe83b1 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/ArrayUtils.java @@ -0,0 +1,54 @@ +package cn.lingniu.framework.plugin.util.collection; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.util.ArrayUtil; + +import java.util.Collection; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Array 工具类 + */ +public class ArrayUtils { + + /** + * 将 object 和 newElements 合并成一个数组 + * + * @param object 对象 + * @param newElements 数组 + * @param 泛型 + * @return 结果数组 + */ + @SafeVarargs + public static Consumer[] append(Consumer object, Consumer... newElements) { + if (object == null) { + return newElements; + } + Consumer[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); + result[0] = object; + System.arraycopy(newElements, 0, result, 1, newElements.length); + return result; + } + + public static V[] toArray(Collection from, Function mapper) { + return toArray(CollectionUtils.convertList(from, mapper)); + } + + @SuppressWarnings("unchecked") + public static T[] toArray(Collection from) { + if (CollectionUtil.isEmpty(from)) { + return (T[]) (new Object[0]); + } + return ArrayUtil.toArray(from, (Class) IterUtil.getElementType(from.iterator())); + } + + public static T get(T[] array, int index) { + if (null == array || index >= array.length) { + return null; + } + return array[index]; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/CollectionUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/CollectionUtils.java new file mode 100644 index 0000000..ae6a29b --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/CollectionUtils.java @@ -0,0 +1,358 @@ +package cn.lingniu.framework.plugin.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static cn.hutool.core.convert.Convert.toCollection; +import static java.util.Arrays.asList; + +/** + * Collection 工具类 + */ +public class CollectionUtils { + + public static boolean containsAny(Object source, Object... targets) { + return asList(targets).contains(source); + } + + public static boolean isAnyEmpty(Collection... collections) { + return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); + } + + public static boolean anyMatch(Collection from, Predicate predicate) { + return from.stream().anyMatch(predicate); + } + + public static List filterList(Collection from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(predicate).collect(Collectors.toList()); + } + + public static List distinct(Collection from, Function keyMapper) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return distinct(from, keyMapper, (t1, t2) -> t1); + } + + public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); + } + + public static List convertList(T[] from, Function func) { + if (ArrayUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return convertList(Arrays.asList(from), func); + } + + public static List convertList(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertList(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertListByFlatMap(Collection from, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertListByFlatMap(Collection from, + Function mapper, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List mergeValuesFromMap(Map> map) { + return map.values() + .stream() + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + public static Set convertSet(Collection from) { + return convertSet(from, v -> v); + } + + public static Set convertSet(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Set convertSet(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Map convertMapByFilter(Collection from, Predicate filter, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v)); + } + + public static Set convertSetByFlatMap(Collection from, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Set convertSetByFlatMap(Collection from, + Function mapper, + Function> func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Map convertMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, Function.identity()); + } + + public static Map convertMap(Collection from, Function keyFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, Function.identity(), supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream() + .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); + } + + // 暂时没想好名字,先以 2 结尾噶 + public static Map> convertMultiMap2(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); + } + + public static Map convertImmutableMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return Collections.emptyMap(); + } + ImmutableMap.Builder builder = ImmutableMap.builder(); + from.forEach(item -> builder.put(keyFunc.apply(item), item)); + return builder.build(); + } + + /** + * 对比老、新两个列表,找出新增、修改、删除的数据 + * + * @param oldList 老列表 + * @param newList 新列表 + * @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同 + * 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据 + * @return [新增列表、修改列表、删除列表] + */ + public static List> diffList(Collection oldList, Collection newList, + BiFunction sameFunc) { + List createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除 + List updateList = new ArrayList<>(); + List deleteList = new ArrayList<>(); + + // 通过以 oldList 为主遍历,找出 updateList 和 deleteList + for (T oldObj : oldList) { + // 1. 寻找是否有匹配的 + T foundObj = null; + for (Iterator iterator = createList.iterator(); iterator.hasNext(); ) { + T newObj = iterator.next(); + // 1.1 不匹配,则直接跳过 + if (!sameFunc.apply(oldObj, newObj)) { + continue; + } + // 1.2 匹配,则移除,并结束寻找 + iterator.remove(); + foundObj = newObj; + break; + } + // 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中 + if (foundObj != null) { + updateList.add(foundObj); + } else { + deleteList.add(oldObj); + } + } + return asList(createList, updateList, deleteList); + } + + public static boolean containsAny(Collection source, Collection candidates) { + return org.springframework.util.CollectionUtils.containsAny(source, candidates); + } + + public static T getFirst(List from) { + return !CollectionUtil.isEmpty(from) ? from.get(0) : null; + } + + public static T findFirst(Collection from, Predicate predicate) { + return findFirst(from, predicate, Function.identity()); + } + + public static U findFirst(Collection from, Predicate predicate, Function func) { + if (CollUtil.isEmpty(from)) { + return null; + } + return from.stream().filter(predicate).findFirst().map(func).orElse(null); + } + + public static > V getMaxValue(Collection from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert !from.isEmpty(); // 断言,避免告警 + T t = from.stream().max(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static > V getMinValue(List from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + T t = from.stream().min(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static > T getMinObject(List from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + return from.stream().min(Comparator.comparing(valueFunc)).get(); + } + + public static > V getSumValue(Collection from, Function valueFunc, + BinaryOperator accumulator) { + return getSumValue(from, valueFunc, accumulator, null); + } + + public static > V getSumValue(Collection from, Function valueFunc, + BinaryOperator accumulator, V defaultValue) { + if (CollUtil.isEmpty(from)) { + return defaultValue; + } + assert !from.isEmpty(); // 断言,避免告警 + return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue); + } + + public static void addIfNotNull(Collection coll, T item) { + if (item == null) { + return; + } + coll.add(item); + } + + public static Collection singleton(T obj) { + return obj == null ? Collections.emptyList() : Collections.singleton(obj); + } + + public static List newArrayList(List> list) { + return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList()); + } + + /** + * 转换为 LinkedHashSet + * + * @param 元素类型 + * @param elementType 集合中元素类型 + * @param value 被转换的值 + * @return {@link LinkedHashSet} + */ + @SuppressWarnings("unchecked") + public static LinkedHashSet toLinkedHashSet(Class elementType, Object value) { + return (LinkedHashSet) toCollection(LinkedHashSet.class, elementType, value); + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/MapUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/MapUtils.java new file mode 100644 index 0000000..f196eb1 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/MapUtils.java @@ -0,0 +1,58 @@ +package cn.lingniu.framework.plugin.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjUtil; +import com.google.common.collect.Multimap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Map 工具类 + */ +public class MapUtils { + + /** + * 从哈希表表中,获得 keys 对应的所有 value 数组 + * + * @param multimap 哈希表 + * @param keys keys + * @return value 数组 + */ + public static List getList(Multimap multimap, Collection keys) { + List result = new ArrayList<>(); + keys.forEach(k -> { + Collection values = multimap.get(k); + if (CollectionUtil.isEmpty(values)) { + return; + } + result.addAll(values); + }); + return result; + } + + /** + * 从哈希表查找到 key 对应的 value,然后进一步处理 + * key 为 null 时, 不处理 + * 注意,如果查找到的 value 为 null 时,不进行处理 + * + * @param map 哈希表 + * @param key key + * @param consumer 进一步处理的逻辑 + */ + public static void findAndThen(Map map, K key, Consumer consumer) { + if (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) { + return; + } + V value = map.get(key); + if (value == null) { + return; + } + consumer.accept(value); + } + + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/SetUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/SetUtils.java new file mode 100644 index 0000000..42f3b72 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/collection/SetUtils.java @@ -0,0 +1,17 @@ +package cn.lingniu.framework.plugin.util.collection; + +import cn.hutool.core.collection.CollUtil; + +import java.util.Set; + +/** + * Set 工具类 + */ +public class SetUtils { + + @SafeVarargs + public static Set asSet(T... objs) { + return CollUtil.newHashSet(objs); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/ContextUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/ContextUtils.java new file mode 100644 index 0000000..7cecc7e --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/ContextUtils.java @@ -0,0 +1,63 @@ +package cn.lingniu.framework.plugin.util.config; + +import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * 获取上下文 todo 得设置 setApplicationContext + **/ +public class ContextUtils { + public static Class MainClass; + + private static ConfigurableApplicationContext applicationContext; + + public static void setApplicationContext(ConfigurableApplicationContext applicationContext) { + ContextUtils.applicationContext = applicationContext; + } + + public static ConfigurableApplicationContext getApplicationContext() { + return applicationContext; + } + + public static T getBean(Class type, boolean required) { + if (type != null && applicationContext != null) { + if (required) { + return applicationContext.getBean(type); + } else { + if (applicationContext.getBeansOfType(type).size() > 0) { + return applicationContext.getBean(type); + } + + } + } + return null; + } + + public static Object getBean(String type, boolean required) { + if (type != null && applicationContext != null) { + if (required) { + return applicationContext.getBean(type); + } else { + if (applicationContext.containsBean(type)) { + return applicationContext.getBean(type); + } + + } + } + return null; + } + + public static ConfigurableWebServerApplicationContext getConfigurableWebServerApplicationContext() { + ApplicationContext context = ContextUtils.getApplicationContext(); + if (context != null && context instanceof ConfigurableWebServerApplicationContext) { + return (ConfigurableWebServerApplicationContext) context; + } + return null; + } + + public static boolean isWeb() { + return getConfigurableWebServerApplicationContext() != null; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/PropertyUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/PropertyUtils.java new file mode 100644 index 0000000..900e66b --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/config/PropertyUtils.java @@ -0,0 +1,43 @@ +package cn.lingniu.framework.plugin.util.config; + + +import org.springframework.boot.convert.ApplicationConversionService; + +public class PropertyUtils { + public static T getProperty(String key, T defaultvalue) { + String value = System.getProperty(key); + if (value == null) { + value = System.getenv(key); + } + if (value == null && ContextUtils.getApplicationContext() != null) { + value = ContextUtils.getApplicationContext().getEnvironment().getProperty(key); + } + if (value == null) { + return defaultvalue; + } + return (T) convert(value, defaultvalue.getClass()); + } + + public static T convert(Object value, Class type) { + if (value == null) { + return null; + } + return (T) ApplicationConversionService.getSharedInstance().convert(value, type); + } + + public static Object getProperty(String key) { + String value = System.getProperty(key); + if (value == null) { + value = System.getenv(key); + } + if (value == null && ContextUtils.getApplicationContext() != null) { + value = ContextUtils.getApplicationContext().getEnvironment().getProperty(key); + } + return value; + } + + public static void setDefaultInitProperty(String key, String propertyValue) { + System.setProperty(key, propertyValue); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/ArrayValuable.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/ArrayValuable.java new file mode 100644 index 0000000..846a9a8 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/ArrayValuable.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.util.core; + +/** + * 可生成 T 数组的接口 + */ +public interface ArrayValuable { + + /** + * @return 数组 + */ + T[] array(); + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/KeyValue.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/KeyValue.java new file mode 100644 index 0000000..1dbd28f --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/core/KeyValue.java @@ -0,0 +1,20 @@ +package cn.lingniu.framework.plugin.util.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Key Value 的键值对 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KeyValue implements Serializable { + + private K key; + private V value; + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateIntervalEnum.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateIntervalEnum.java new file mode 100644 index 0000000..f66b3db --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateIntervalEnum.java @@ -0,0 +1,47 @@ +package cn.lingniu.framework.plugin.util.date; + +import cn.hutool.core.util.ArrayUtil; +import cn.lingniu.framework.plugin.util.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 时间间隔的枚举 + * + * @author dhb52 + */ +@Getter +@AllArgsConstructor +public enum DateIntervalEnum implements ArrayValuable { + + HOUR(0, "小时"), // 特殊:字典里,暂时不会有这个枚举!!!因为大多数情况下,用不到这个间隔 + DAY(1, "天"), + WEEK(2, "周"), + MONTH(3, "月"), + QUARTER(4, "季度"), + YEAR(5, "年") + ; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(DateIntervalEnum::getInterval).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer interval; + /** + * 名称 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static DateIntervalEnum valueOf(Integer interval) { + return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values()); + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateUtils.java new file mode 100644 index 0000000..6a1bb97 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/DateUtils.java @@ -0,0 +1,151 @@ +package cn.lingniu.framework.plugin.util.date; + +import cn.hutool.core.date.LocalDateTimeUtil; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Date; + +/** + * 时间工具类 + */ +public class DateUtils { + + /** + * 时区 - 默认 + */ + public static final String TIME_ZONE_DEFAULT = "GMT+8"; + + /** + * 秒转换成毫秒 + */ + public static final long SECOND_MILLIS = 1000; + + public static final String FORMAT_YEAR_MONTH_DAY = "yyyy-MM-dd"; + + public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; + + /** + * 将 LocalDateTime 转换成 Date + * + * @param date LocalDateTime + * @return LocalDateTime + */ + public static Date of(LocalDateTime date) { + if (date == null) { + return null; + } + // 将此日期时间与时区相结合以创建 ZonedDateTime + ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault()); + // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳 + Instant instant = zonedDateTime.toInstant(); + // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 + return Date.from(instant); + } + + /** + * 将 Date 转换成 LocalDateTime + * + * @param date Date + * @return LocalDateTime + */ + public static LocalDateTime of(Date date) { + if (date == null) { + return null; + } + // 转为时间戳 + Instant instant = date.toInstant(); + // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + + public static Date addTime(Duration duration) { + return new Date(System.currentTimeMillis() + duration.toMillis()); + } + + public static boolean isExpired(LocalDateTime time) { + LocalDateTime now = LocalDateTime.now(); + return now.isAfter(time); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param month 月 + * @param day 日 + * @return 指定时间 + */ + public static Date buildTime(int year, int month, int day) { + return buildTime(year, month, day, 0, 0, 0); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param month 月 + * @param day 日 + * @param hour 小时 + * @param minute 分钟 + * @param second 秒 + * @return 指定时间 + */ + public static Date buildTime(int year, int month, int day, + int hour, int minute, int second) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒 + return calendar.getTime(); + } + + public static Date max(Date a, Date b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.compareTo(b) > 0 ? a : b; + } + + public static LocalDateTime max(LocalDateTime a, LocalDateTime b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.isAfter(b) ? a : b; + } + + /** + * 是否今天 + * + * @param date 日期 + * @return 是否 + */ + public static boolean isToday(LocalDateTime date) { + return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now()); + } + + /** + * 是否昨天 + * + * @param date 日期 + * @return 是否 + */ + public static boolean isYesterday(LocalDateTime date) { + return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1)); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/LocalDateTimeUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/LocalDateTimeUtils.java new file mode 100644 index 0000000..e8ce131 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/date/LocalDateTimeUtils.java @@ -0,0 +1,352 @@ +package cn.lingniu.framework.plugin.util.date; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.date.TemporalAccessorUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import java.sql.Timestamp; +import java.time.DateTimeException; +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAdjusters; +import java.util.ArrayList; +import java.util.List; + +import static cn.hutool.core.date.DatePattern.UTC_MS_WITH_XXX_OFFSET_PATTERN; +import static cn.hutool.core.date.DatePattern.createFormatter; + +/** + * 时间工具类,用于 {@link LocalDateTime} + */ +public class LocalDateTimeUtils { + + /** + * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值 + */ + public static LocalDateTime EMPTY = buildTime(1970, 1, 1); + + public static DateTimeFormatter UTC_MS_WITH_XXX_OFFSET_FORMATTER = createFormatter(UTC_MS_WITH_XXX_OFFSET_PATTERN); + + /** + * 解析时间 + * + * 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功 + * + * @param time 时间 + * @return 时间字符串 + */ + public static LocalDateTime parse(String time) { + try { + return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN); + } catch (DateTimeParseException e) { + return LocalDateTimeUtil.parse(time); + } + } + + public static LocalDateTime addTime(Duration duration) { + return LocalDateTime.now().plus(duration); + } + + public static LocalDateTime minusTime(Duration duration) { + return LocalDateTime.now().minus(duration); + } + + public static boolean beforeNow(LocalDateTime date) { + return date.isBefore(LocalDateTime.now()); + } + + public static boolean afterNow(LocalDateTime date) { + return date.isAfter(LocalDateTime.now()); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param month 月 + * @param day 日 + * @return 指定时间 + */ + public static LocalDateTime buildTime(int year, int month, int day) { + return LocalDateTime.of(year, month, day, 0, 0, 0); + } + + public static LocalDateTime[] buildBetweenTime(int year1, int month1, int day1, + int year2, int month2, int day2) { + return new LocalDateTime[]{buildTime(year1, month1, day1), buildTime(year2, month2, day2)}; + } + + /** + * 判指定断时间,是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param time 指定时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, Timestamp time) { + if (startTime == null || endTime == null || time == null) { + return false; + } + return LocalDateTimeUtil.isIn(LocalDateTimeUtil.of(time), startTime, endTime); + } + + /** + * 判指定断时间,是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param time 指定时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) { + if (startTime == null || endTime == null || time == null) { + return false; + } + return LocalDateTimeUtil.isIn(parse(time), startTime, endTime); + } + + /** + * 判断当前时间是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) { + if (startTime == null || endTime == null) { + return false; + } + return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime); + } + + /** + * 判断当前时间是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 是否 + */ + public static boolean isBetween(String startTime, String endTime) { + if (startTime == null || endTime == null) { + return false; + } + LocalDate nowDate = LocalDate.now(); + return LocalDateTimeUtil.isIn(LocalDateTime.now(), + LocalDateTime.of(nowDate, LocalTime.parse(startTime)), + LocalDateTime.of(nowDate, LocalTime.parse(endTime))); + } + + /** + * 判断时间段是否重叠 + * + * @param startTime1 开始 time1 + * @param endTime1 结束 time1 + * @param startTime2 开始 time2 + * @param endTime2 结束 time2 + * @return 重叠:true 不重叠:false + */ + public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) { + LocalDate nowDate = LocalDate.now(); + return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1), + LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2)); + } + + /** + * 获取指定日期所在的月份的开始时间 + * 例如:2023-09-30 00:00:00,000 + * + * @param date 日期 + * @return 月份的开始时间 + */ + public static LocalDateTime beginOfMonth(LocalDateTime date) { + return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN); + } + + /** + * 获取指定日期所在的月份的最后时间 + * 例如:2023-09-30 23:59:59,999 + * + * @param date 日期 + * @return 月份的结束时间 + */ + public static LocalDateTime endOfMonth(LocalDateTime date) { + return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX); + } + + /** + * 获得指定日期所在季度 + * + * @param date 日期 + * @return 所在季度 + */ + public static int getQuarterOfYear(LocalDateTime date) { + return (date.getMonthValue() - 1) / 3 + 1; + } + + /** + * 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负 + * + * @param dateTime 日期 + * @return 相差天数 + */ + public static Long between(LocalDateTime dateTime) { + return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS); + } + + /** + * 获取今天的开始时间 + * + * @return 今天 + */ + public static LocalDateTime getToday() { + return LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); + } + + /** + * 获取昨天的开始时间 + * + * @return 昨天 + */ + public static LocalDateTime getYesterday() { + return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1)); + } + + /** + * 获取本月的开始时间 + * + * @return 本月 + */ + public static LocalDateTime getMonth() { + return beginOfMonth(LocalDateTime.now()); + } + + /** + * 获取本年的开始时间 + * + * @return 本年 + */ + public static LocalDateTime getYear() { + return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN); + } + + public static List getDateRangeList(LocalDateTime startTime, + LocalDateTime endTime, + Integer interval) { + // 1.1 找到枚举 + DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); + Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); + // 1.2 将时间对齐 + startTime = LocalDateTimeUtil.beginOfDay(startTime); + endTime = LocalDateTimeUtil.endOfDay(endTime); + + // 2. 循环,生成时间范围 + List timeRanges = new ArrayList<>(); + switch (intervalEnum) { + case HOUR: + while (startTime.isBefore(endTime)) { + timeRanges.add(new LocalDateTime[]{startTime, startTime.plusHours(1).minusNanos(1)}); + startTime = startTime.plusHours(1); + } + case DAY: + while (startTime.isBefore(endTime)) { + timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)}); + startTime = startTime.plusDays(1); + } + break; + case WEEK: + while (startTime.isBefore(endTime)) { + LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, endOfWeek}); + startTime = endOfWeek.plusNanos(1); + } + break; + case MONTH: + while (startTime.isBefore(endTime)) { + LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, endOfMonth}); + startTime = endOfMonth.plusNanos(1); + } + break; + case QUARTER: + while (startTime.isBefore(endTime)) { + int quarterOfYear = getQuarterOfYear(startTime); + LocalDateTime quarterEnd = quarterOfYear == 4 + ? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1) + : startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, quarterEnd}); + startTime = quarterEnd.plusNanos(1); + } + break; + case YEAR: + while (startTime.isBefore(endTime)) { + LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1); + timeRanges.add(new LocalDateTime[]{startTime, endOfYear}); + startTime = endOfYear.plusNanos(1); + } + break; + default: + throw new IllegalArgumentException("Invalid interval: " + interval); + } + // 3. 兜底,最后一个时间,需要保持在 endTime 之前 + LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges); + if (lastTimeRange != null) { + lastTimeRange[1] = endTime; + } + return timeRanges; + } + + /** + * 格式化时间范围 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param interval 时间间隔 + * @return 时间范围 + */ + public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) { + // 1. 找到枚举 + DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); + Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); + + // 2. 循环,生成时间范围 + switch (intervalEnum) { + case HOUR: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATETIME_MINUTE_PATTERN); + case DAY: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN); + case WEEK: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN) + + StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime)); + case MONTH: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN); + case QUARTER: + return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime)); + case YEAR: + return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN); + default: + throw new IllegalArgumentException("Invalid interval: " + interval); + } + } + + /** + * 将给定的 {@link LocalDateTime} 转换为自 Unix 纪元时间(1970-01-01T00:00:00Z)以来的秒数。 + * + * @param sourceDateTime 需要转换的本地日期时间,不能为空 + * @return 自 1970-01-01T00:00:00Z 起的秒数(epoch second) + * @throws NullPointerException 如果 {@code sourceDateTime} 为 {@code null} + * @throws DateTimeException 如果转换过程中发生时间超出范围或其他时间处理异常 + */ + public static Long toEpochSecond(LocalDateTime sourceDateTime) { + return TemporalAccessorUtil.toInstant(sourceDateTime).getEpochSecond(); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/FileUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/FileUtils.java new file mode 100644 index 0000000..7053a01 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/FileUtils.java @@ -0,0 +1,59 @@ +package cn.lingniu.framework.plugin.util.io; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import lombok.SneakyThrows; + +import java.io.File; + +/** + * 文件工具类 + */ +public class FileUtils { + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(String data) { + File file = createTempFile(); + // 写入内容 + FileUtil.writeUtf8String(data, file); + return file; + } + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(byte[] data) { + File file = createTempFile(); + // 写入内容 + FileUtil.writeBytes(data, file); + return file; + } + + /** + * 创建临时文件,无内容 + * 该文件会在 JVM 退出时,进行删除 + * + * @return 文件 + */ + @SneakyThrows + public static File createTempFile() { + // 创建文件,通过 UUID 保证唯一 + File file = File.createTempFile(IdUtil.simpleUUID(), null); + // 标记 JVM 退出时,自动删除 + file.deleteOnExit(); + return file; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/IoUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/IoUtils.java new file mode 100644 index 0000000..39951f8 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/io/IoUtils.java @@ -0,0 +1,26 @@ +package cn.lingniu.framework.plugin.util.io; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.InputStream; + +/** + * IO 工具类,用于 {@link IoUtil} 缺失的方法 + */ +public class IoUtils { + + /** + * 从流中读取 UTF8 编码的内容 + * + * @param in 输入流 + * @param isClose 是否关闭 + * @return 内容 + * @throws IORuntimeException IO 异常 + */ + public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException { + return StrUtil.utf8Str(IoUtil.read(in, isClose)); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/ip/IpUtil.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/ip/IpUtil.java new file mode 100644 index 0000000..a1fa975 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/ip/IpUtil.java @@ -0,0 +1,59 @@ +package cn.lingniu.framework.plugin.util.ip; + +import org.springframework.util.CollectionUtils; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * 获取本机IP + */ +public final class IpUtil { + + private static List ipList = null; + + public static final String LOCAL_HOST = "127.0.0.1"; + + /** + * 获取本机IP + */ + public static String getIp() { + if (!CollectionUtils.isEmpty(ipList)) { + return ipList.get(0); + } + List ipList = new ArrayList<>(); + Enumeration allNetInterfaces; + try { + allNetInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException e) { + e.printStackTrace(); + return "localhost"; + } + InetAddress ip; + while (allNetInterfaces.hasMoreElements()) { + NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement(); + Enumeration addresses = netInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + ip = (InetAddress) addresses.nextElement(); + if (ip != null && ip instanceof Inet4Address) { + String localIp = ip.getHostAddress(); + if (localIp.equals(LOCAL_HOST)) { + continue; + } + ipList.add(localIp); + } + } + } + if (ipList.size() > 0) { + IpUtil.ipList = ipList; + return ipList.get(0); + } else { + return LOCAL_HOST; + } + } +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtil.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtil.java new file mode 100644 index 0000000..72d8add --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtil.java @@ -0,0 +1,156 @@ +package cn.lingniu.framework.plugin.util.json; + +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; +import java.io.StringWriter; +import java.time.ZoneId; +import java.util.TimeZone; + +/** + * JSON序列化工具类 + */ +@Slf4j +public class JsonUtil implements Serializable { + private static ObjectMapper mapper = new ObjectMapper(); + private static XmlMapper xmlMapper = new XmlMapper(); + + static { + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.registerModule(new JavaTimeModule()); + mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false); + mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); + mapper.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + + xmlMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + xmlMapper.registerModule(new JavaTimeModule()); + xmlMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + xmlMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + xmlMapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false); + xmlMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); + xmlMapper.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + + xmlMapper.setDefaultUseWrapper(false); + //字段为null,自动忽略,不再序列化 + //XML标签名:使用骆驼命名的属性名, + xmlMapper.setPropertyNamingStrategy(PropertyNamingStrategy.UPPER_CAMEL_CASE); + //设置转换模式 + xmlMapper.enable(MapperFeature.USE_STD_BEAN_NAMING); + } + + /** + * 类型自动转换 + */ + public static T convertValue(I inValue, ObjectMapper objectMapper, TypeReference toValueTypeRef) { + return objectMapper.convertValue(inValue, toValueTypeRef); + } + + + /** + * 类型自动转换 + */ + public static T convertValue(I inValue, TypeReference toValueTypeRef) { + return convertValue(inValue, mapper, toValueTypeRef); + } + + public static String bean2Json(T obj) { + return bean2Json(obj, mapper, true); + } + + + public static String bean2Json(T obj, boolean forString) { + return bean2Json(obj, mapper, forString); + } + + + public static String bean2Json(T obj, ObjectMapper jsonMapper, boolean forString) { + try { + if (ObjectEmptyUtils.isNotEmpty(obj) && obj instanceof String && forString) { + return obj.toString(); + } + StringWriter sw = new StringWriter(); + JsonGenerator gen = new JsonFactory().createJsonGenerator(sw); + jsonMapper.writeValue(gen, obj); + gen.close(); + return sw.toString(); + } catch (Exception ex) { + log.error(String.format("对象象序列化异常:%s", obj.getClass().getName()), ex); + } + return ""; + } + + public static String bean2Json(T obj, ObjectMapper jsonMapper) { + return bean2Json(obj, jsonMapper, true); + } + + + public static T json2Bean(String jsonStr, Class objClass) { + return json2Bean(jsonStr, objClass, false); + } + + + @SneakyThrows + public static T json2Bean(String jsonStr, Class objClass, boolean throwEx) { + try { + if (ObjectEmptyUtils.isNotEmpty(objClass) + && objClass.equals(String.class) + && !jsonStr.startsWith("\"") + && !jsonStr.endsWith("\"")) { + return (T) jsonStr; + } + return mapper.readValue(jsonStr, objClass); + } catch (Exception ex) { + if (throwEx) { + throw ex; + } + log.error(String.format("对象象反序列化异常:%s", objClass.getName()), ex); + return null; + } + } + + public static T json2Bean(String jsonStr, TypeReference objClass) { + return json2Bean(jsonStr, objClass, mapper, false); + } + + public static T json2Bean(String jsonStr, TypeReference objClass, ObjectMapper mapperClz) { + return json2Bean(jsonStr, objClass, mapperClz, false); + } + + + @SneakyThrows + public static T json2Bean(String jsonStr, TypeReference objClass, ObjectMapper mapperClz, boolean throwEx) { + try { + if (ObjectEmptyUtils.isEmpty(jsonStr)) { + return null; + } + if (ObjectEmptyUtils.isNotEmpty(objClass) && objClass.getType() == String.class + && !jsonStr.startsWith("\"") && !jsonStr.endsWith("\"")) { + return (T) jsonStr; + } + return (ObjectEmptyUtils.isEmpty(mapperClz) ? mapper : mapperClz).readValue(jsonStr, objClass); + } catch (Exception ex) { + if (throwEx) { + throw ex; + } + log.error(String.format("对象象反序列化异常:%s", objClass.getType().getTypeName()), ex); + return null; + } + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtils.java new file mode 100644 index 0000000..746979b --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/JsonUtils.java @@ -0,0 +1,231 @@ +package cn.lingniu.framework.plugin.util.json; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import cn.lingniu.framework.plugin.util.json.databind.TimestampLocalDateTimeDeserializer; +import cn.lingniu.framework.plugin.util.json.databind.TimestampLocalDateTimeSerializer; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * JSON 工具类 + * + */ +@Slf4j +public class JsonUtils { + + @Getter + private static ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值 + // 解决 LocalDateTime 的序列化 + SimpleModule simpleModule = new JavaTimeModule() + .addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE) + .addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); + objectMapper.registerModules(simpleModule); + } + + /** + * 初始化 objectMapper 属性 + *

+ * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean + * + * @param objectMapper ObjectMapper 对象 + */ + public static void init(ObjectMapper objectMapper) { + JsonUtils.objectMapper = objectMapper; + } + + @SneakyThrows + public static String toJsonString(Object object) { + return objectMapper.writeValueAsString(object); + } + + @SneakyThrows + public static byte[] toJsonByte(Object object) { + return objectMapper.writeValueAsBytes(object); + } + + @SneakyThrows + public static String toJsonPrettyString(Object object) { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); + } + + public static T parseObject(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, String path, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + JsonNode treeNode = objectMapper.readTree(text); + JsonNode pathNode = treeNode.path(path); + return objectMapper.readValue(pathNode.toString(), clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, Type type) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(byte[] text, Type type) { + if (ArrayUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + /** + * 将字符串解析成指定类型的对象 + * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, + * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。 + * + * @param text 字符串 + * @param clazz 类型 + * @return 对象 + */ + public static T parseObject2(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + return JSONUtil.toBean(text, clazz); + } + + public static T parseObject(byte[] bytes, Class clazz) { + if (ArrayUtil.isEmpty(bytes)) { + return null; + } + try { + return objectMapper.readValue(bytes, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", bytes, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + /** + * 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null + * + * @param text 字符串 + * @param typeReference 类型引用 + * @return 指定类型的对象 + */ + public static T parseObjectQuietly(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + return null; + } + } + + public static List parseArray(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static List parseArray(String text, String path, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + JsonNode treeNode = objectMapper.readTree(text); + JsonNode pathNode = treeNode.path(path); + return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(String text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(byte[] text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static boolean isJson(String text) { + return JSONUtil.isTypeJSON(text); + } + + /** + * 判断字符串是否为 JSON 类型的字符串 + * @param str 字符串 + */ + public static boolean isJsonObject(String str) { + return JSONUtil.isTypeJSONObject(str); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/NumberSerializer.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/NumberSerializer.java new file mode 100644 index 0000000..59ac6d4 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/NumberSerializer.java @@ -0,0 +1,35 @@ +package cn.lingniu.framework.plugin.util.json.databind; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; + +import java.io.IOException; + +/** + * Long 序列化规则 + * + * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题 + */ +@JacksonStdImpl +public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer { + + private static final long MAX_SAFE_INTEGER = 9007199254740991L; + private static final long MIN_SAFE_INTEGER = -9007199254740991L; + + public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class); + + public NumberSerializer(Class rawType) { + super(rawType); + } + + @Override + public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // 超出范围 序列化位字符串 + if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) { + super.serialize(value, gen, serializers); + } else { + gen.writeString(value.toString()); + } + } +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeDeserializer.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeDeserializer.java new file mode 100644 index 0000000..d42b89f --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeDeserializer.java @@ -0,0 +1,25 @@ +package cn.lingniu.framework.plugin.util.json.databind; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * 基于时间戳的 LocalDateTime 反序列化器 + */ +public class TimestampLocalDateTimeDeserializer extends JsonDeserializer { + + public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // 将 Long 时间戳,转换为 LocalDateTime 对象 + return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeSerializer.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeSerializer.java new file mode 100644 index 0000000..818cc80 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/json/databind/TimestampLocalDateTimeSerializer.java @@ -0,0 +1,82 @@ +package cn.lingniu.framework.plugin.util.json.databind; + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 基于时间戳的 LocalDateTime 序列化器 + */ +@Slf4j +public class TimestampLocalDateTimeSerializer extends JsonSerializer { + + public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer(); + + private static final Map, Map> FIELD_CACHE = new ConcurrentHashMap<>(); + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // 情况一:有 JsonFormat 自定义注解,则使用它。 + String fieldName = gen.getOutputContext().getCurrentName(); + if (fieldName != null) { + Object currentValue = gen.getOutputContext().getCurrentValue(); + if (currentValue != null) { + Class clazz = currentValue.getClass(); + Map fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap); + Field field = fieldMap.get(fieldName); + if (field != null && field.isAnnotationPresent(JsonFormat.class)) { + JsonFormat jsonFormat = field.getAnnotation(JsonFormat.class); + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(jsonFormat.pattern()); + gen.writeString(formatter.format(value)); + return; + } catch (Exception ex) { + log.warn("[serialize][({}#{}) 使用 JsonFormat pattern 失败,尝试使用默认的 Long 时间戳]", + clazz.getName(), fieldName, ex); + } + } + } + } + + // 情况二:默认将 LocalDateTime 对象,转换为 Long 时间戳 + gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + } + + /** + * 构建字段映射(缓存) + * + * @param clazz 类 + * @return 字段映射 + */ + private Map buildFieldMap(Class clazz) { + Map fieldMap = new HashMap<>(); + for (Field field : ReflectUtil.getFields(clazz)) { + String fieldName = field.getName(); + JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class); + if (jsonProperty != null) { + String value = jsonProperty.value(); + if (StrUtil.isNotEmpty(value) && ObjUtil.notEqual("\u0000", value)) { + fieldName = value; + } + } + fieldMap.put(fieldName, field); + } + return fieldMap; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/MoneyUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/MoneyUtils.java new file mode 100644 index 0000000..21e50ec --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/MoneyUtils.java @@ -0,0 +1,129 @@ +package cn.lingniu.framework.plugin.util.number; + +import cn.hutool.core.math.Money; +import cn.hutool.core.util.NumberUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 金额工具类 + */ +public class MoneyUtils { + + /** + * 金额的小数位数 + */ + private static final int PRICE_SCALE = 2; + + /** + * 百分比对应的 BigDecimal 对象 + */ + public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100); + + /** + * 计算百分比金额,四舍五入 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @return 百分比金额 + */ + public static Integer calculateRatePrice(Integer price, Double rate) { + return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue(); + } + + /** + * 计算百分比金额,向下传入 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @return 百分比金额 + */ + public static Integer calculateRatePriceFloor(Integer price, Double rate) { + return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue(); + } + + /** + * 计算百分比金额 + * + * @param price 金额(单位分) + * @param count 数量 + * @param percent 折扣(单位分),列如 60.2%,则传入 6020 + * @return 商品总价 + */ + public static Integer calculator(Integer price, Integer count, Integer percent) { + price = price * count; + if (percent == null) { + return price; + } + return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100)); + } + + /** + * 计算百分比金额 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @param scale 保留小数位数 + * @param roundingMode 舍入模式 + */ + public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) { + return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以 + .divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100 + } + + /** + * 分转元 + * + * @param fen 分 + * @return 元 + */ + public static BigDecimal fenToYuan(int fen) { + return new Money(0, fen).getAmount(); + } + + /** + * 分转元(字符串) + * + * 例如说 fen 为 1 时,则结果为 0.01 + * + * @param fen 分 + * @return 元 + */ + public static String fenToYuanStr(int fen) { + return new Money(0, fen).toString(); + } + + /** + * 金额相乘,默认进行四舍五入 + * + * 位数:{@link #PRICE_SCALE} + * + * @param price 金额 + * @param count 数量 + * @return 金额相乘结果 + */ + public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) { + if (price == null || count == null) { + return null; + } + return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP); + } + + /** + * 金额相乘(百分比),默认进行四舍五入 + * + * 位数:{@link #PRICE_SCALE} + * + * @param price 金额 + * @param percent 百分比 + * @return 金额相乘结果 + */ + public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) { + if (price == null || percent == null) { + return null; + } + return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/NumberUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/NumberUtils.java new file mode 100644 index 0000000..04ed751 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/number/NumberUtils.java @@ -0,0 +1,76 @@ +package cn.lingniu.framework.plugin.util.number; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 数字的工具类,补全 {@link NumberUtil} 的功能 + */ +public class NumberUtils { + + public static Long parseLong(String str) { + return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null; + } + + public static Integer parseInt(String str) { + return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null; + } + + public static boolean isAllNumber(List values) { + if (CollUtil.isEmpty(values)) { + return false; + } + for (String value : values) { + if (!NumberUtil.isNumber(value)) { + return false; + } + } + return true; + } + + /** + * 通过经纬度获取地球上两点之间的距离 + * + * 参考 <DistanceUtil> 实现,目前它已经被 hutool 删除 + * + * @param lat1 经度1 + * @param lng1 纬度1 + * @param lat2 经度2 + * @param lng2 纬度2 + * @return 距离,单位:千米 + */ + public static double getDistance(double lat1, double lng1, double lat2, double lng2) { + double radLat1 = lat1 * Math.PI / 180.0; + double radLat2 = lat2 * Math.PI / 180.0; + double a = radLat1 - radLat2; + double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; + double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + + Math.cos(radLat1) * Math.cos(radLat2) + * Math.pow(Math.sin(b / 2), 2))); + distance = distance * 6378.137; + distance = Math.round(distance * 10000d) / 10000d; + return distance; + } + + /** + * 提供精确的乘法运算 + * + * 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null,则返回 null + * + * @param values 多个被乘值 + * @return 积 + */ + public static BigDecimal mul(BigDecimal... values) { + for (BigDecimal value : values) { + if (value == null) { + return null; + } + } + return NumberUtil.mul(values); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/BeanUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/BeanUtils.java new file mode 100644 index 0000000..3771b03 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/BeanUtils.java @@ -0,0 +1,53 @@ +package cn.lingniu.framework.plugin.util.object; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.db.PageResult; +import cn.lingniu.framework.plugin.util.collection.CollectionUtils; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Bean 工具类 + * + * 1. 默认使用 {@link BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能 + * 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现 + */ +public class BeanUtils { + + public static T toBean(Object source, Class targetClass) { + return BeanUtil.toBean(source, targetClass); + } + + public static T toBean(Object source, Class targetClass, Consumer peek) { + T target = toBean(source, targetClass); + if (target != null) { + peek.accept(target); + } + return target; + } + + public static List toBean(List source, Class targetType) { + if (source == null) { + return null; + } + return CollectionUtils.convertList(source, s -> toBean(s, targetType)); + } + + public static List toBean(List source, Class targetType, Consumer peek) { + List list = toBean(source, targetType); + if (list != null) { + list.forEach(peek); + } + return list; + } + + + public static void copyProperties(Object source, Object target) { + if (source == null || target == null) { + return; + } + BeanUtil.copyProperties(source, target, false); + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/ObjectUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/ObjectUtils.java new file mode 100644 index 0000000..49ab0be --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/object/ObjectUtils.java @@ -0,0 +1,65 @@ +package cn.lingniu.framework.plugin.util.object; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.function.Consumer; + +/** + * Object 工具类 + */ +public class ObjectUtils { + + /** + * 复制对象,并忽略 Id 编号 + * + * @param object 被复制对象 + * @param consumer 消费者,可以二次编辑被复制对象 + * @return 复制后的对象 + */ + public static T cloneIgnoreId(T object, Consumer consumer) { + T result = ObjectUtil.clone(object); + // 忽略 id 编号 + Field field = ReflectUtil.getField(object.getClass(), "id"); + if (field != null) { + ReflectUtil.setFieldValue(result, field, null); + } + // 二次编辑 + if (result != null) { + consumer.accept(result); + } + return result; + } + + public static > T max(T obj1, T obj2) { + if (obj1 == null) { + return obj2; + } + if (obj2 == null) { + return obj1; + } + return obj1.compareTo(obj2) > 0 ? obj1 : obj2; + } + + @SafeVarargs + public static T defaultIfNull(T... array) { + for (T item : array) { + if (item != null) { + return item; + } + } + return null; + } + + @SafeVarargs + public static boolean equalsAny(T obj, T... array) { + return Arrays.asList(array).contains(obj); + } + + public static boolean isNotAllEmpty(Object... objs) { + return !ObjectUtil.isAllEmpty(objs); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/servlet/ServletUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/servlet/ServletUtils.java new file mode 100644 index 0000000..b4c0a3a --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/servlet/ServletUtils.java @@ -0,0 +1,121 @@ +package cn.lingniu.framework.plugin.util.servlet; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import org.springframework.http.MediaType; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.Map; + +/** + * 客户端工具类 + */ +public class ServletUtils { + + /** + * 返回 JSON 字符串 + * + * @param response 响应 + * @param object 对象,会序列化成 JSON 字符串 + */ + @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 + public static void writeJSON(HttpServletResponse response, Object object) { + String content = JsonUtils.toJsonString(object); + ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + /** + * 返回附件 + * + * @param response 响应 + * @param filename 文件名 + * @param content 附件内容 + */ + public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { + // 设置 header 和 contentType + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + // 输出附件 + IoUtil.write(response.getOutputStream(), false, content); + } + + /** + * @param request 请求 + * @return ua + */ + public static String getUserAgent(HttpServletRequest request) { + String ua = request.getHeader("User-Agent"); + return ua != null ? ua : ""; + } + + /** + * 获得请求 + * + * @return HttpServletRequest + */ + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (!(requestAttributes instanceof ServletRequestAttributes)) { + return null; + } + return ((ServletRequestAttributes) requestAttributes).getRequest(); + } + + public static String getUserAgent() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return getUserAgent(request); + } + + public static String getClientIP() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return ServletUtil.getClientIP(request); + } + + public static boolean isJsonRequest(ServletRequest request) { + return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); + } + + public static String getBody(HttpServletRequest request) { + // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取 + if (isJsonRequest(request)) { + return ServletUtil.getBody(request); + } + return null; + } + + public static byte[] getBodyBytes(HttpServletRequest request) { + // 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取 + if (isJsonRequest(request)) { + return ServletUtil.getBodyBytes(request); + } + return null; + } + + public static String getClientIP(HttpServletRequest request) { + return ServletUtil.getClientIP(request); + } + + public static Map getParamMap(HttpServletRequest request) { + return ServletUtil.getParamMap(request); + } + + public static Map getHeaderMap(HttpServletRequest request) { + return ServletUtil.getHeaderMap(request); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringExpressionUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringExpressionUtils.java new file mode 100644 index 0000000..7d97e08 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringExpressionUtils.java @@ -0,0 +1,123 @@ +package cn.lingniu.framework.plugin.util.spring; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Spring EL 表达式的工具类 + * + * @author mashu + */ +public class SpringExpressionUtils { + + /** + * Spring EL 表达式解析器 + */ + private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + /** + * 参数名发现器 + */ + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + private SpringExpressionUtils() { + } + + /** + * 从切面中,单个解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionString EL 表达式数组 + * @return 执行界面 + */ + public static Object parseExpression(JoinPoint joinPoint, String expressionString) { + Map result = parseExpressions(joinPoint, Collections.singletonList(expressionString)); + return result.get(expressionString); + } + + /** + * 从切面中,批量解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionStrings EL 表达式数组 + * @return 结果,key 为表达式,value 为对应值 + */ + public static Map parseExpressions(JoinPoint joinPoint, List expressionStrings) { + // 如果为空,则不进行解析 + if (CollUtil.isEmpty(expressionStrings)) { + return MapUtil.newHashMap(); + } + + // 第一步,构建解析的上下文 EvaluationContext + // 通过 joinPoint 获取被注解方法 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + Method method = methodSignature.getMethod(); + // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组 + String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); + // Spring 的表达式上下文对象 + EvaluationContext context = new StandardEvaluationContext(); + // 给上下文赋值 + if (ArrayUtil.isNotEmpty(paramNames)) { + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < paramNames.length; i++) { + context.setVariable(paramNames[i], args[i]); + } + } + + // 第二步,逐个参数解析 + Map result = MapUtil.newHashMap(expressionStrings.size(), true); + expressionStrings.forEach(key -> { + Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); + result.put(key, value); + }); + return result; + } + + /** + * 从 Bean 工厂,解析 EL 表达式的结果 + * + * @param expressionString EL 表达式 + * @return 执行界面 + */ + public static Object parseExpression(String expressionString) { + return parseExpression(expressionString, null); + } + + /** + * 从 Bean 工厂,解析 EL 表达式的结果 + * + * @param expressionString EL 表达式 + * @param variables 变量 + * @return 执行界面 + */ + public static Object parseExpression(String expressionString, Map variables) { + if (StrUtil.isBlank(expressionString)) { + return null; + } + Expression expression = EXPRESSION_PARSER.parseExpression(expressionString); + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext())); + if (MapUtil.isNotEmpty(variables)) { + context.setVariables(variables); + } + return expression.getValue(context); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringUtils.java new file mode 100644 index 0000000..f4234c9 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/spring/SpringUtils.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.util.spring; + +import cn.hutool.extra.spring.SpringUtil; + +import java.util.Objects; + +/** + * Spring 工具类 + */ +public class SpringUtils extends SpringUtil { + + /** + * 是否为生产环境 + * + * @return 是否生产环境 + */ + public static boolean isProd() { + String activeProfile = getActiveProfile(); + return Objects.equals("prod", activeProfile); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StrUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StrUtils.java new file mode 100644 index 0000000..35a7a49 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StrUtils.java @@ -0,0 +1,78 @@ +package cn.lingniu.framework.plugin.util.string; + +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 字符串工具类 + */ +public class StrUtils { + + public static String maxLength(CharSequence str, int maxLength) { + return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好 + } + + /** + * 给定字符串是否以任何一个字符串开始 + * 给定字符串和数组为空都返回 false + * + * @param str 给定字符串 + * @param prefixes 需要检测的开始字符串 + * @since 3.0.6 + */ + public static boolean startWithAny(String str, Collection prefixes) { + if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) { + return false; + } + + for (CharSequence suffix : prefixes) { + if (StrUtil.startWith(str, suffix, false)) { + return true; + } + } + return false; + } + + public static List splitToLong(String value, CharSequence separator) { + long[] longs = StrUtil.splitToLong(value, separator); + return Arrays.stream(longs).boxed().collect(Collectors.toList()); + } + + public static Set splitToLongSet(String value) { + return splitToLongSet(value, StrPool.COMMA); + } + + public static Set splitToLongSet(String value, CharSequence separator) { + long[] longs = StrUtil.splitToLong(value, separator); + return Arrays.stream(longs).boxed().collect(Collectors.toSet()); + } + + public static List splitToInteger(String value, CharSequence separator) { + int[] integers = StrUtil.splitToInt(value, separator); + return Arrays.stream(integers).boxed().collect(Collectors.toList()); + } + + /** + * 移除字符串中,包含指定字符串的行 + * + * @param content 字符串 + * @param sequence 包含的字符串 + * @return 移除后的字符串 + */ + public static String removeLineContains(String content, String sequence) { + if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) { + return content; + } + return Arrays.stream(content.split("\n")) + .filter(line -> !line.contains(sequence)) + .collect(Collectors.joining("\n")); + } + + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StringUtil.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StringUtil.java new file mode 100644 index 0000000..1fa8f24 --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/string/StringUtil.java @@ -0,0 +1,72 @@ +package cn.lingniu.framework.plugin.util.string; + +import org.apache.commons.lang3.StringUtils; +import java.util.Vector; + +public class StringUtil extends StringUtils { + + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * @param input the string to split + * @param delimiter the delimiter to use to split the string + * @return split, and return extra data defined past last delimiter, start at begining of string + */ + public static String[] split(String input, String delimiter) { + return split(input, delimiter, true, 0); + } + + + /** + * Split the given string around the delimiter given. + * + * @param str the string to split + * @param delimiter the delimiter to use to split the string + * @param withEnd return data that is found past the last delimiter? + * @param initIndex the starting index to begin splitting around + * @return array of data found around the delimiters + */ + public static String[] split(String str, String delimiter, boolean withEnd, int initIndex) { + String input = str; + //quit if: data is null or is an empty string + if (input == null || input.equals(new String()) || input == "") return new String[0]; + + //create a buffer array for the worst case scenario of data size + Vector dataBuffer = new Vector(); + int index = 0; + + //cut the string begining at the given index if needed + if (initIndex > 0) + input = input.substring(initIndex); + + while (true) { + if (input == null) break; + int cutIndex = input.indexOf(delimiter); + if (cutIndex == -1) { + if (withEnd) + dataBuffer.addElement(input); + break; + } + String sub1 = input.substring(0, cutIndex); + String sub2 = input.substring(cutIndex + 1); + + dataBuffer.addElement(sub1); + input = sub2; + } + //convert the buffer to an array and return it + String[] d = new String[dataBuffer.size()]; + + int l = dataBuffer.size(); + for (int i = 0; i < l; i++) { + d[i] = (String) dataBuffer.elementAt(i); + } + return d; + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/validation/ObjectEmptyUtils.java b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/validation/ObjectEmptyUtils.java new file mode 100644 index 0000000..eedd9ae --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/java/cn/lingniu/framework/plugin/util/validation/ObjectEmptyUtils.java @@ -0,0 +1,79 @@ +package cn.lingniu.framework.plugin.util.validation; + +import org.springframework.util.ObjectUtils; + +import java.util.Collection; +import java.util.Map; + +/** + * 判空工具类 + */ +public final class ObjectEmptyUtils { + + /** + * 判断集合是否为空 + */ + public static boolean isEmpty(Collection coll) { + return (coll == null || coll.isEmpty()); + } + + /** + * 判断集合是否不为空 + */ + public static boolean isNotEmpty(Collection coll) { + return !isEmpty(coll); + } + + /** + * 判断map是否为空 + */ + public static boolean isEmpty(Map map) { + return (map == null || map.isEmpty()); + } + + /** + * 判断map是否不为空 + */ + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * 判断一个对象是否为空 + */ + public static boolean isEmpty(T t) { + if (t == null) { + return true; + } + // 根据对象类型选择最优判断方式 + if (t instanceof String) { + return ((String) t).length() == 0; // 比isEmpty()更直接 + } else if (t instanceof Collection) { + return ((Collection) t).size() == 0; + } else if (t instanceof Map) { + return ((Map) t).size() == 0; + } else if (t.getClass().isArray()) { + return ObjectUtils.isEmpty(t); + } else { + // 对于其他类型,提供安全的字符串判断 + String str = t.toString(); + return str == null || str.isEmpty(); + } + + } + + /** + * 判断数组是否不为空 + */ + public static boolean isEmpty(T[] datas) { + return ObjectUtils.isEmpty(datas); + } + + /** + * 判断一个对象是否不为空 + */ + public static boolean isNotEmpty(T t) { + return !isEmpty(t); + } + +} diff --git a/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/resources/util.md b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/resources/util.md new file mode 100644 index 0000000..e47a6fe --- /dev/null +++ b/lingniu-framework-plugin/lingniu-framework-plugin-util/src/main/resources/util.md @@ -0,0 +1 @@ +# 框架核心公共util模块 diff --git a/lingniu-framework-plugin/log/logback-spring.xml b/lingniu-framework-plugin/log/logback-spring.xml new file mode 100644 index 0000000..5ff0f40 --- /dev/null +++ b/lingniu-framework-plugin/log/logback-spring.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n + UTF-8 + + + + + + + INFO + ACCEPT + DENY + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n + UTF-8 + + true + + ${LOG_PATH}/${APP_NAME}-info.%d{yyyy-MM-dd}.%i.log + ${MAX_FILE_SIZE} + ${MAX_HISTORY} + ${TOTAL_SIZE_CAP} + + + + + + + ERROR + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n + UTF-8 + + true + + ${LOG_PATH}/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log + ${MAX_FILE_SIZE} + ${MAX_HISTORY} + ${TOTAL_SIZE_CAP} + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%logger{50}] - %msg%n + UTF-8 + + true + + ${LOG_PATH}/${APP_NAME}-business.%d{yyyy-MM-dd}.%i.log + ${MAX_FILE_SIZE} + ${MAX_HISTORY} + ${TOTAL_SIZE_CAP} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/pom.xml b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/pom.xml new file mode 100644 index 0000000..3b5a1bb --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-microservice-common + lingniu-framework-plugin-microservice-common + http://maven.apache.org + + UTF-8 + + + + io.github.openfeign + feign-hystrix + true + + + + + cn.lingniu.framework + lingniu-framework-plugin-web + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + io.github.openfeign + feign-okhttp + + + io.github.openfeign + feign-httpclient + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + commons-logging + commons-logging + + + + + com.alibaba + fastjson + provided + true + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + 3.1.6 + + + org.springframework.retry + spring-retry + 1.3.4 + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.cloud + spring-cloud-context + + + \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/DefaultHttpProperties.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/DefaultHttpProperties.java new file mode 100644 index 0000000..bfb1e44 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/DefaultHttpProperties.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.microservice.common.config; + +import lombok.Data; + + +@Data +public class DefaultHttpProperties { + + /** + * 每个route默认的最大连接数 + */ + private Integer defaultMaxPerRoute = 25; + /** + * 从连接池中获取连接的超时时间,超时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool + */ + private Integer connectionRequestTimeout = 500; + /** + * 空闲永久连接检查间隔 + */ + private Integer validateAfterInactivity = 2000; + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/FeignProperties.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/FeignProperties.java new file mode 100644 index 0000000..5029083 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/FeignProperties.java @@ -0,0 +1,18 @@ +package cn.lingniu.framework.plugin.microservice.common.config; + +import feign.Logger; +import lombok.Data; + + +@Data +public class FeignProperties { + + private Logger.Level loggerLevel = Logger.Level.BASIC; + + private Long period = 500L; + + private Integer maxPeriod = 1000; + + private Integer maxAttempts = 1; + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/MircroServiceConfig.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/MircroServiceConfig.java new file mode 100644 index 0000000..de69b55 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/MircroServiceConfig.java @@ -0,0 +1,47 @@ +package cn.lingniu.framework.plugin.microservice.common.config; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@Data +@ConfigurationProperties(MircroServiceConfig.PRE_FIX) +public class MircroServiceConfig { + + public static final String PRE_FIX = "framework.lingniu.spring.cloud"; + + /** + * 是否启用OkHttpClient + */ + private Boolean isOkHttp = true; + /** + * common + * 读取超时 + */ + private Integer readTimeout = 1000; + /** + * common + * 连接超时 + */ + private Integer connectTimeout = 1000; + /** + * common + * 整个连接池的最大连接数 + */ + private Integer maxTotal = 150; + /** + * fegin配置 + */ + private FeignProperties feign = new FeignProperties(); + /** + * defaultHttp. 默认http配置 + */ + private DefaultHttpProperties defaultHttp = new DefaultHttpProperties(); + /** + * okHttp相关配置 + */ + private OkHttpProperties okHttp = new OkHttpProperties(); + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/OkHttpProperties.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/OkHttpProperties.java new file mode 100644 index 0000000..f1d4ec0 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/config/OkHttpProperties.java @@ -0,0 +1,30 @@ +package cn.lingniu.framework.plugin.microservice.common.config; + +import lombok.Data; + +import java.util.concurrent.TimeUnit; + +@Data +public class OkHttpProperties { + + /** + * OkHttp TimeToLive + */ + private Long timeToLive = 2000L; + /** + * 时间单位 + */ + private TimeUnit timeToLiveUnit = TimeUnit.MILLISECONDS; + /** + * 是否开启重定身 + */ + private Boolean isFollowRedirects = true; + /** + * 是否开启SSL验证 + */ + private Boolean disableSslValidation = false; + /** + * 写起时 + */ + private Integer writeTimeout = 1000; +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/ErrorCommonResultHandler.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/ErrorCommonResultHandler.java new file mode 100644 index 0000000..fa70270 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/ErrorCommonResultHandler.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.microservice.common.core; + + +import cn.lingniu.framework.plugin.core.base.CommonResult; + +/** + * @description: 异常解码器 + **/ +public interface ErrorCommonResultHandler { + + CommonResult handler(String body); + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/StringHttpMessageConverter.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/StringHttpMessageConverter.java new file mode 100644 index 0000000..cc896a4 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/StringHttpMessageConverter.java @@ -0,0 +1,122 @@ +package cn.lingniu.framework.plugin.microservice.common.core; + + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of {@link HttpMessageConverter} that can read and write strings. + * + *

By default, this converter supports all media types ({@code */*}), + * and writes with a {@code Content-Type} of {@code text/plain}. This can be overridden + * by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property. + * + * @author Arjen Poutsma + * @author Juergen Hoeller + * @since 3.0 + */ +public class StringHttpMessageConverter extends AbstractHttpMessageConverter { + + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + + @Nullable + private volatile List availableCharsets; + + private boolean writeAcceptCharset = true; + + + /** + * A default constructor that uses {@code "ISO-8859-1"} as the default charset. + * + * @see #StringHttpMessageConverter(Charset) + */ + public StringHttpMessageConverter() { + this(DEFAULT_CHARSET); + } + + /** + * A constructor accepting a default charset to use if the requested content + * type does not specify one. + */ + public StringHttpMessageConverter(Charset defaultCharset) { + super(defaultCharset, MediaType.TEXT_PLAIN, MediaType.ALL); + } + + + /** + * Indicates whether the {@code Accept-Charset} should be written to any outgoing request. + *

Default is {@code true}. + */ + public void setWriteAcceptCharset(boolean writeAcceptCharset) { + this.writeAcceptCharset = writeAcceptCharset; + } + + + @Override + + public boolean supports(Class clazz) { + return String.class == clazz; + } + + @Override + protected String readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { + Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType()); + return StreamUtils.copyToString(inputMessage.getBody(), charset); + } + + @Override + protected Long getContentLength(String str, @Nullable MediaType contentType) { + Charset charset = getContentTypeCharset(contentType); + return (long) str.getBytes(charset).length; + } + + @Override + protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException { + if (this.writeAcceptCharset) { + outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets()); + } + Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType()); + StreamUtils.copy(str, charset, outputMessage.getBody()); + } + + + /** + * Return the list of supported {@link Charset}s. + *

By default, returns {@link Charset#availableCharsets()}. + * Can be overridden in subclasses. + * + * @return the list of accepted charsets + */ + protected List getAcceptedCharsets() { + List charsets = this.availableCharsets; + if (charsets == null) { + charsets = new ArrayList<>(Charset.availableCharsets().values()); + this.availableCharsets = charsets; + } + return charsets; + } + + private Charset getContentTypeCharset(@Nullable MediaType contentType) { + if (contentType != null && contentType.getCharset() != null) { + return contentType.getCharset(); + } else { + Charset charset = getDefaultCharset(); + Assert.state(charset != null, "No default charset"); + return charset; + } + } + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuErrorDecoder.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuErrorDecoder.java new file mode 100644 index 0000000..7b0b5a5 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuErrorDecoder.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2015-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.lingniu.framework.plugin.microservice.common.core.decoder; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.exception.ServerException; +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import cn.lingniu.framework.plugin.microservice.common.core.ErrorCommonResultHandler; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import feign.Response; +import feign.Util; +import feign.codec.ErrorDecoder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; + +/** + * 异常解析 + */ +@Slf4j +public class LingniuErrorDecoder extends ErrorDecoder.Default implements ErrorDecoder { + + private final ErrorCommonResultHandler handler; + + public LingniuErrorDecoder(ErrorCommonResultHandler errorCommonResultHandler) { + this.handler = errorCommonResultHandler; + } + + @Override + public Exception decode(final String methodKey, final Response response) { + try { + if (HttpStatus.NOT_FOUND.value() == response.status() || ObjectEmptyUtils.isEmpty(response.body())) { + return new ServerException(GlobalErrorCodeConstants.NOT_FOUND); + } else if (HttpStatus.UNAUTHORIZED.value() == response.status()) { + return new ServerException(GlobalErrorCodeConstants.UNAUTHORIZED); + } else if (HttpStatus.METHOD_NOT_ALLOWED.value() == response.status()) { + return new ServerException(GlobalErrorCodeConstants.FORBIDDEN); + } else if (HttpStatus.TOO_MANY_REQUESTS.value() == response.status()) { + return new ServerException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS); + } else if (HttpStatus.REQUEST_TIMEOUT.value() == response.status() || HttpStatus.GATEWAY_TIMEOUT.value() == response.status()) { + return new ServerException(GlobalErrorCodeConstants.TIME_OUT_ERROR); + } else if (HttpStatus.BAD_GATEWAY.value() == response.status()) { + return new ServerException(GlobalErrorCodeConstants.BAD_GATE_WAY_ERROR); + } + String body = Util.toString(response.body().asReader()); + CommonResult result = handler.handler(body); + if (ObjectEmptyUtils.isNotEmpty(result) && result.getCode() != 0) { + if (log.isWarnEnabled()) { + log.warn("微服务调用: {} 响应异常信息:{} {} ", methodKey, result.getCode(), result.getMsg()); + } + return new ServerException(result.getCode(), result.getMsg()); + } else if (HttpStatus.valueOf(response.status()).is5xxServerError()) { + return new ServerException(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR); + } else { + return new ServerException(GlobalErrorCodeConstants.UNKNOWN); + } + } catch (Exception ex) { + log.error("Feign异常解码出现问题", ex); + return new ServerException(GlobalErrorCodeConstants.FEIGN_DECODE_ERROR); + } + } +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuFeignDecoder.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuFeignDecoder.java new file mode 100644 index 0000000..6f1dc33 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/decoder/LingniuFeignDecoder.java @@ -0,0 +1,85 @@ +/* + * Copyright © 2015-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.lingniu.framework.plugin.microservice.common.core.decoder; + +import feign.FeignException; +import feign.Response; +import feign.codec.Decoder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.LinkedList; + +/** + * LingniuFeignDecoder + */ +@Slf4j +public class LingniuFeignDecoder implements Decoder { + + private Decoder decoder; + + public LingniuFeignDecoder(final Decoder decoder) { + this.decoder = decoder; + } + + @Override + public Object decode(final Response response, final Type t) throws IOException, FeignException { + Type type = t; + if (isParameterizeHttpEntity(type)) { + type = ((ParameterizedType) type).getActualTypeArguments()[0]; + Object decodedObject = decoder.decode(response, type); + return createResponse(decodedObject, response); + } else if (isHttpEntity(type)) { + return createResponse(null, response); + } else { + Object decodeResponse = decoder.decode(response, type); + return decodeResponse; + } + } + + private boolean isParameterizeHttpEntity(final Type type) { + if (type instanceof ParameterizedType) { + return isHttpEntity(((ParameterizedType) type).getRawType()); + } + return false; + } + + @SuppressWarnings("rawtypes") + private boolean isHttpEntity(final Type type) { + if (type instanceof Class) { + Class c = (Class) type; + return HttpEntity.class.isAssignableFrom(c); + } + return false; + } + + @SuppressWarnings("unchecked") + private ResponseEntity createResponse(final Object instance, final Response response) { + MultiValueMap headers = new LinkedMultiValueMap<>(); + for (String key : response.headers().keySet()) { + headers.put(key, new LinkedList<>(response.headers().get(key))); + } + return new ResponseEntity<>((T) instance, headers, HttpStatus.valueOf(response.status())); + } +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/CharlesRequestInterceptor.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/CharlesRequestInterceptor.java new file mode 100644 index 0000000..c97f6b5 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/CharlesRequestInterceptor.java @@ -0,0 +1,83 @@ +package cn.lingniu.framework.plugin.microservice.common.core.interceptor; + +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +@Slf4j +public class CharlesRequestInterceptor implements RequestInterceptor { + + private static ObjectMapper objectMapper; + + static { + objectMapper = new ObjectMapper(); + } + + @Override + public void apply(RequestTemplate template) { + // feign 不支持 GET 方法传 POJO, json body转query + if (template.method().equals("GET") && ObjectEmptyUtils.isNotEmpty(template.body())) { + try { + JsonNode jsonNode = objectMapper.readTree(template.body()); + template.body(null, Charset.defaultCharset()); + Map> queries = new HashMap<>(); + buildQuery(jsonNode, "", queries); + template.queries(queries); + } catch (IOException e) { + log.error("CharlesRequestInterceptor异常", e); + } + } + } + + private String encoderParameters(String parameterValue) { + try { + return URLEncoder.encode(parameterValue, "utf-8"); + } catch (Exception ex) { + log.error("参数转换异常", ex); + } + return parameterValue; + } + + private void buildQuery(JsonNode jsonNode, String path, Map> queries) { + if (!jsonNode.isContainerNode()) { + if (jsonNode.isNull()) { + return; + } + Collection values = queries.get(path); + if (null == values) { + values = new ArrayList<>(); + queries.put(path, values); + } + values.add(encoderParameters(jsonNode.asText())); + return; + } + if (jsonNode.isArray()) { + Iterator it = jsonNode.elements(); + while (it.hasNext()) { + buildQuery(it.next(), path, queries); + } + } else { + Iterator> it = jsonNode.fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (ObjectEmptyUtils.isNotEmpty(path)) { + buildQuery(entry.getValue(), path + "." + entry.getKey(), queries); + } else { + buildQuery(entry.getValue(), entry.getKey(), queries); + } + } + } + } +} \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/FeignLoggerInterceptor.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/FeignLoggerInterceptor.java new file mode 100644 index 0000000..ef839a6 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/core/interceptor/FeignLoggerInterceptor.java @@ -0,0 +1,19 @@ +package cn.lingniu.framework.plugin.microservice.common.core.interceptor; + +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import feign.RequestInterceptor; +import feign.RequestTemplate; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FeignLoggerInterceptor implements RequestInterceptor { + + public FeignLoggerInterceptor() { + } + + @Override + public void apply(RequestTemplate requestTemplate) { + String bodyContent = ObjectEmptyUtils.isNotEmpty(requestTemplate.body()) ? new String(requestTemplate.body()) : ""; + log.info("Feign调用日志:URL:{} Mehod:{} Body:{} ", requestTemplate.url(), requestTemplate.method(), bodyContent); + } +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/DefaultFeignLoadBalancedConfiguration.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/DefaultFeignLoadBalancedConfiguration.java new file mode 100644 index 0000000..969d29a --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/DefaultFeignLoadBalancedConfiguration.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package cn.lingniu.framework.plugin.microservice.common.init; + +import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig; +import feign.Client; +import feign.httpclient.ApacheHttpClient; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * DefaultFeignLoadBalancedConfiguration + */ +@Configuration +@ConditionalOnProperty(value = "framework.lingniu.spring.cloud.isOkHttp", havingValue = "false") +class DefaultFeignLoadBalancedConfiguration { + + + @Bean + public Client feignClient(MircroServiceConfig mircroServiceConfig, + LoadBalancerClient loadBalancerClient, + LoadBalancedRetryFactory loadBalancedRetryFactory, + LoadBalancerClientFactory loadBalancerClientFactory) { + ApacheHttpClient delegate = new ApacheHttpClient(httpClient(mircroServiceConfig)); + return new RetryableFeignBlockingLoadBalancerClient(delegate, + loadBalancerClient, loadBalancedRetryFactory, loadBalancerClientFactory); + } + + + public HttpClient httpClient(MircroServiceConfig mircroServiceConfig) { + Registry registry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", SSLConnectionSocketFactory.getSocketFactory()) + .build(); + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry); + connectionManager.setMaxTotal(mircroServiceConfig.getMaxTotal()); + connectionManager.setDefaultMaxPerRoute(mircroServiceConfig.getDefaultHttp().getDefaultMaxPerRoute()); + connectionManager.setValidateAfterInactivity(mircroServiceConfig.getDefaultHttp().getValidateAfterInactivity()); + RequestConfig requestConfig = RequestConfig.custom() + .setSocketTimeout(mircroServiceConfig.getReadTimeout()) + .setConnectTimeout(mircroServiceConfig.getConnectTimeout()) + .setConnectionRequestTimeout(mircroServiceConfig.getDefaultHttp().getConnectionRequestTimeout()) + .build(); + return HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .setConnectionManager(connectionManager) + .build(); + } + + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/HttpMessageFastJsonConverterConfiguration.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/HttpMessageFastJsonConverterConfiguration.java new file mode 100644 index 0000000..72723f0 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/HttpMessageFastJsonConverterConfiguration.java @@ -0,0 +1,96 @@ +package cn.lingniu.framework.plugin.microservice.common.init; + +import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig; +import cn.lingniu.framework.plugin.microservice.common.core.StringHttpMessageConverter; +import cn.lingniu.framework.plugin.microservice.common.core.decoder.LingniuFeignDecoder; +import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson.support.config.FastJsonConfig; +import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.form.FormEncoder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.cloud.openfeign.support.SpringDecoder; +import org.springframework.cloud.openfeign.support.SpringEncoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@ConditionalOnClass(name = {"com.alibaba.fastjson.support.config.FastJsonConfig"}) +@ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "json-converter", havingValue = "fastjson") +@Configuration +@AutoConfigureBefore({LingniuCloudAutoConfiguration.class}) +public class HttpMessageFastJsonConverterConfiguration { + + HttpMessageConverters converters = new HttpMessageConverters(createStringConverter(), createFastJsonConverter()); + + @Bean + public Decoder fastJsonFeignDecoder() { + return new LingniuFeignDecoder(new SpringDecoder(() -> converters)); + } + + @Bean + public Encoder fastJsonFeignEncoder() { + return new FormEncoder(new SpringEncoder(() -> converters)); + } + + private HttpMessageConverter createStringConverter() { + //fastConverter转换纯String有问题,该处使用的是重写后的转换器,防止被Spring优先级排到后面导致没有生效 + StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(StandardCharsets.UTF_8); + stringHttpMessageConverter.setWriteAcceptCharset(false); + return stringHttpMessageConverter; + } + + private HttpMessageConverter createFastJsonConverter() { + //创建fastJson消息转换器 + FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); + //升级最新版本需加============================================================= + List supportedMediaTypes = new ArrayList<>(); + supportedMediaTypes.add(MediaType.APPLICATION_JSON); + supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8); + supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML); + supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); + supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM); + supportedMediaTypes.add(MediaType.APPLICATION_PDF); + supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML); + supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML); + supportedMediaTypes.add(MediaType.APPLICATION_XML); + supportedMediaTypes.add(MediaType.IMAGE_GIF); + supportedMediaTypes.add(MediaType.IMAGE_JPEG); + supportedMediaTypes.add(MediaType.IMAGE_PNG); + supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM); + supportedMediaTypes.add(MediaType.TEXT_HTML); + supportedMediaTypes.add(MediaType.TEXT_MARKDOWN); + supportedMediaTypes.add(MediaType.TEXT_PLAIN); + supportedMediaTypes.add(MediaType.TEXT_XML); + fastConverter.setSupportedMediaTypes(supportedMediaTypes); + + //创建配置类 + FastJsonConfig fastJsonConfig = new FastJsonConfig(); + //修改配置返回内容的过滤 + //WriteNullListAsEmpty :List字段如果为null,输出为[],而非null + //WriteNullStringAsEmpty : 字符类型字段如果为null,输出为"",而非null + //DisableCircularReferenceDetect :消除对同一对象循环引用的问题,默认为false(如果不配置有可能会进入死循环) + //WriteNullBooleanAsFalse:Boolean字段如果为null,输出为false,而非null + //WriteMapNullValue:是否输出值为null的字段,默认为false + fastJsonConfig.setSerializerFeatures( + SerializerFeature.DisableCircularReferenceDetect, + SerializerFeature.WriteMapNullValue + ); + fastConverter.setFastJsonConfig(fastJsonConfig); + + return fastConverter; + } + + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudAutoConfiguration.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudAutoConfiguration.java new file mode 100644 index 0000000..63970f5 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudAutoConfiguration.java @@ -0,0 +1,171 @@ +/* + * Copyright © 2015-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.lingniu.framework.plugin.microservice.common.init; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig; +import cn.lingniu.framework.plugin.microservice.common.core.ErrorCommonResultHandler; +import cn.lingniu.framework.plugin.microservice.common.core.decoder.LingniuErrorDecoder; +import cn.lingniu.framework.plugin.microservice.common.core.decoder.LingniuFeignDecoder; +import cn.lingniu.framework.plugin.microservice.common.core.interceptor.CharlesRequestInterceptor; +import cn.lingniu.framework.plugin.microservice.common.core.interceptor.FeignLoggerInterceptor; +import cn.lingniu.framework.plugin.util.json.JsonUtil; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import feign.Client; +import feign.Feign; +import feign.Request; +import feign.Retryer; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import feign.form.FormEncoder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.cloud.openfeign.support.SpringDecoder; +import org.springframework.cloud.openfeign.support.SpringEncoder; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Scope; +import org.springframework.core.env.Environment; +import org.springframework.web.client.RestTemplate; + +import java.util.Optional; + +/** + * LingniuCloudAutoConfiguration配置 + */ +@Configuration +@Slf4j +@EnableFeignClients(basePackages = {"cn.lingniu.framework.plugin.microservice.common", "cn.lingniu"}) +@ConditionalOnClass(Feign.class) +@AutoConfigureBefore(FeignAutoConfiguration.class) +@EnableConfigurationProperties({MircroServiceConfig.class}) +@AutoConfigureAfter(FrameworkWebConfig.class) +@RequiredArgsConstructor +@Import({OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class, HttpMessageFastJsonConverterConfiguration.class}) +public class LingniuCloudAutoConfiguration implements EnvironmentAware { + + private static final long PERIOD = 500L; + private static final int MAX_PERIOD = 1000; + private static final int MAX_ATTEMPTS = 2; + private final ObjectFactory messageConverters; + private final MircroServiceConfig mircroServiceConfig; + private final FrameworkWebConfig frameworkWebConfig; + private Environment environment; + + @Bean + public ErrorDecoder getLingniuErrorDecoder(ErrorCommonResultHandler errorCommonResultHandler) { + return new LingniuErrorDecoder(errorCommonResultHandler); + } + + @Bean + @ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "json-converter", havingValue = "jackson", matchIfMissing = true) + public Decoder getLingniuFeignDecoder() { + return new LingniuFeignDecoder(new SpringDecoder(messageConverters)); + } + + @Bean + @ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "json-converter", havingValue = "jackson", matchIfMissing = true) + public Encoder getFormEncoder() { + return new FormEncoder(new SpringEncoder(messageConverters)); + } + + @Bean + @ConditionalOnMissingBean + public ErrorCommonResultHandler errorHandler() { + return (b) -> JsonUtil.json2Bean(b, CommonResult.class); + } + + @Bean + public CharlesRequestInterceptor charlesRequestInterceptor() { + return new CharlesRequestInterceptor(); + } + + @Bean + public FeignLoggerInterceptor headersInterceptor() { + return new FeignLoggerInterceptor(); + } + + /** + * feignBuilder创建 + */ + @Bean + @Scope("prototype") + public Feign.Builder feignBuilder(ErrorCommonResultHandler handler, Optional retryer, Client restClient, Request.Options options) { + Feign.Builder builder = Feign.builder() + .errorDecoder(new LingniuErrorDecoder(handler)) + .options(options) + .logLevel(mircroServiceConfig.getFeign().getLoggerLevel()) + .retryer(retryer.isPresent() ? retryer.get() : Retryer.NEVER_RETRY) + .client(restClient); + return builder; + } + + @Bean + public Request.Options options() { + return new Request.Options(mircroServiceConfig.getConnectTimeout(), mircroServiceConfig.getReadTimeout()); + } + + /** + * todo 后续可扩展 + * 创建 RestTemplate 实例 + * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder} + */ + @Bean + @ConditionalOnMissingBean + @LoadBalanced + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder.build(); + } + + /** + * feignRetryer实例化 + * + * @return feignRetryer实例 + */ + @Bean + @ConditionalOnProperty(prefix = MircroServiceConfig.PRE_FIX, name = "feign-retryer", havingValue = "true", matchIfMissing = false) + public Retryer feignRetryer() { + return new Retryer.Default(ObjectEmptyUtils.isEmpty(mircroServiceConfig.getFeign().getPeriod()) ? PERIOD : mircroServiceConfig.getFeign().getPeriod(), + ObjectEmptyUtils.isEmpty(mircroServiceConfig.getFeign().getMaxPeriod()) ? MAX_PERIOD : mircroServiceConfig.getFeign().getMaxPeriod(), + ObjectEmptyUtils.isEmpty(mircroServiceConfig.getFeign().getMaxAttempts()) ? MAX_ATTEMPTS : mircroServiceConfig.getFeign().getMaxAttempts()); + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudInit.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudInit.java new file mode 100644 index 0000000..29e0b50 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/LingniuCloudInit.java @@ -0,0 +1,31 @@ +package cn.lingniu.framework.plugin.microservice.common.init; + + +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; + + +@Order(Integer.MIN_VALUE + 500) +public class LingniuCloudInit implements org.springframework.context.ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + PropertyUtils.setDefaultInitProperty("spring.main.allow-bean-definition-overriding", "true"); + PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[0]", "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure"); + PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[1]", "org.springframework.boot.actuate.autoconfigure.redis.RedisHealthIndicatorAutoConfiguration"); + PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[2]", "org.springframework.boot.actuate.autoconfigure.jdbc.DataSourceHealthIndicatorAutoConfiguration"); + PropertyUtils.setDefaultInitProperty("spring.autoconfigure.exclude[3]", "org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchRestHealthIndicatorAutoConfiguration"); + if (environment.getProperty("framework.lingniu.spring.cloud.isOkHttp", "true").equalsIgnoreCase(Boolean.TRUE.toString())) { + PropertyUtils.setDefaultInitProperty("feign.httpclient.enabled", "false"); + PropertyUtils.setDefaultInitProperty("feign.okhttp.enabled", "true"); + } else { + PropertyUtils.setDefaultInitProperty("feign.httpclient.enabled", "true"); + PropertyUtils.setDefaultInitProperty("feign.okhttp.enabled", "false"); + } + } + + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/OkHttpFeignLoadBalancedConfiguration.java b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/OkHttpFeignLoadBalancedConfiguration.java new file mode 100644 index 0000000..1752f4f --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/java/cn/lingniu/framework/plugin/microservice/common/init/OkHttpFeignLoadBalancedConfiguration.java @@ -0,0 +1,90 @@ +/* + * Copyright 2013-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package cn.lingniu.framework.plugin.microservice.common.init; + +import cn.lingniu.framework.plugin.microservice.common.config.MircroServiceConfig; +import feign.Client; +import feign.okhttp.OkHttpClient; +import okhttp3.ConnectionPool; +import okhttp3.EventListener; +import okhttp3.Interceptor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; +import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; +import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; +import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PreDestroy; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * OkHttpFeignLoadBalancedConfiguration + */ +@Configuration +@ConditionalOnClass(OkHttpClient.class) +@ConditionalOnProperty(value = "framework.lingniu.spring.cloud.isOkHttp", havingValue = "true", matchIfMissing = true) +class OkHttpFeignLoadBalancedConfiguration { + + @Bean + public Client feignClient(okhttp3.OkHttpClient okHttpClient, + LoadBalancerClient loadBalancerClient, + LoadBalancedRetryFactory loadBalancedRetryFactory, + LoadBalancerClientFactory loadBalancerClientFactory) { + return new RetryableFeignBlockingLoadBalancerClient(new OkHttpClient(okHttpClient), loadBalancerClient, loadBalancedRetryFactory, loadBalancerClientFactory); + } + + @Configuration + protected static class OkHttpFeignConfiguration { + private okhttp3.OkHttpClient okHttpClient; + + @Bean + public ConnectionPool httpClientConnectionPool(MircroServiceConfig mircroServiceConfig, + OkHttpClientConnectionPoolFactory connectionPoolFactory) { + return connectionPoolFactory.create(mircroServiceConfig.getMaxTotal(), mircroServiceConfig.getOkHttp().getTimeToLive(), mircroServiceConfig.getOkHttp().getTimeToLiveUnit()); + } + + @Bean + public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, Optional> okFeignInterceptors, + ConnectionPool connectionPool, MircroServiceConfig mircroServiceConfig) { + okhttp3.OkHttpClient.Builder builder = httpClientFactory.createBuilder(mircroServiceConfig.getOkHttp().getDisableSslValidation()). + connectTimeout(mircroServiceConfig.getConnectTimeout(), TimeUnit.MILLISECONDS) + .readTimeout(mircroServiceConfig.getReadTimeout(), TimeUnit.MILLISECONDS) + .writeTimeout(mircroServiceConfig.getOkHttp().getWriteTimeout(), TimeUnit.MILLISECONDS) + .followRedirects(mircroServiceConfig.getOkHttp().getIsFollowRedirects()) + .connectionPool(connectionPool); + okFeignInterceptors.ifPresent(n -> {n.forEach(builder::addInterceptor);}); + this.okHttpClient = builder.build(); + return okHttpClient; + } + + @PreDestroy + public void destroy() { + if (okHttpClient != null) { + okHttpClient.dispatcher().executorService().shutdown(); + okHttpClient.connectionPool().evictAll(); + } + } + } +} diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..d368d9d --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.microservice.common.init.LingniuCloudAutoConfiguration +org.springframework.context.ApplicationContextInitializer=\ +cn.lingniu.framework.plugin.microservice.common.init.LingniuCloudInit \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/pom.xml b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/pom.xml new file mode 100644 index 0000000..e54cca2 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-microservice-nacos + lingniu-framework-plugin-microservice-nacos + http://maven.apache.org + + UTF-8 + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + nacos-client + com.alibaba.nacos + + + + + nacos-client + com.alibaba.nacos + + + cn.lingniu.framework + lingniu-framework-plugin-core + compile + + + org.springframework.boot + spring-boot-starter-web + compile + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-common + compile + + + io.reactivex + rxjava + 1.3.8 + compile + + + \ No newline at end of file diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..e69de29 diff --git a/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/nacos.md b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/nacos.md new file mode 100644 index 0000000..378cbb8 --- /dev/null +++ b/lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos/src/main/resources/nacos.md @@ -0,0 +1,72 @@ +# 框架核心web应用模块 + + +## 概述 (Overview) + +1. 定位: 基于 Alibaba Nacos 封装,支持服务注册、发现、多中心注册等功能的自动化组件 +2. 核心能力: + * 支持多中心注册与服务发现 +3. 适用场景: + * 公司内部微服务统一注册中心 + * 与 FeignClient 配合实现服务间调用 + + +## 如何配置 + +```yaml +spring: + application: + name: lingniu-framework-demo + profiles: + active: dev + cloud: + nacos: + enabled: true #开启微服务自定义注册,自动获取当前框架信息,启动时间等到metadata中 + discovery: + server-addr: http://nacos-uat-new-inter.xx.net.cn:8848 #注册中心地址 + username: nacos_test #注册中心用户名 + password: nacos_test #注册中心密码 + +``` + + +## 接口提供端 + +```java +@RestController +@RequestMapping +public class GithubApiController { + + @GetMapping("/repos/{owner}/{repo}/contributors") + public CommonResult> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo) { + // 模拟返回贡献者列表 + Contributor contributor1 = new Contributor("user1", 10); + Contributor contributor2 = new Contributor("user2", 5); + List response = new ArrayList<>(); + response.add(contributor1); + response.add(contributor2); + return CommonResult.success(response); + } +} +``` + +## 接口调用方 + +```java +// name = 接口提供方的应用名 +@FeignClient(name = "lingniu-framework-provider-demo" +//外部接口配置url: @FeignClient(name = "github-api", url = "https://api.github.com" +// 方式1 配置fallback +// , fallback = GithubApiClientFallBack.class +// 方式2 配置fallbackFactory +// , fallbackFactory = GithubApiClientFallbackFactory.class +) +public interface DemoFeignClient { + + @GetMapping("/repos/{owner}/{repo}/contributors") + CommonResult> contributors(@PathVariable("owner") String owner, @PathVariable("repo") String repo); + + @GetMapping("/reposNotFound") + CommonResult> reposNotFound(@RequestParam("owner") String owner, @RequestParam("repo") String repo); +} +``` \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/pom.xml b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/pom.xml new file mode 100644 index 0000000..786ebdd --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-prometheus + ${project.artifactId} + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + io.micrometer + micrometer-registry-prometheus + + + org.springframework.boot + spring-boot-starter-aop + provided + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + + + org.springframework.boot + spring-boot-actuator + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework + spring-web + compile + + + org.springframework.cloud + spring-cloud-commons + provided + true + + + javax.servlet + javax.servlet-api + compile + + + + diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusCounter.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusCounter.java new file mode 100644 index 0000000..1f3c3c2 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusCounter.java @@ -0,0 +1,34 @@ +package cn.lingniu.framework.plugin.prometheus; + +/** + * Counter 类型代表一种样本数据单调递增的指标,即只增不减,除非监控系统发生了重置 + **/ + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PrometheusCounter { + + /** + * 如果name有设置值,使用name作为Metric name + */ + String name() default ""; + /** + * 标签 + */ + String[] labels() default ""; + /** + * SPEL 参数列表 + */ + String[] parameterKeys() default ""; + /** + * 备注 + */ + String memo() default "default"; +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusMetrics.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusMetrics.java new file mode 100644 index 0000000..df21a48 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusMetrics.java @@ -0,0 +1,19 @@ +package cn.lingniu.framework.plugin.prometheus; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PrometheusMetrics { + + /** + * 默认为空,程序使用method signature作为Metric name + * 如果name有设置值,使用name作为Metric name + */ + String name() default ""; +} \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusSummary.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusSummary.java new file mode 100644 index 0000000..5bf61dc --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/PrometheusSummary.java @@ -0,0 +1,34 @@ +package cn.lingniu.framework.plugin.prometheus; + + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Summary 类型 + **/ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PrometheusSummary { + + /** + * 如果name有设置值,使用name作为Metric name + */ + String name() default ""; + /** + * 标签 + */ + String[] labels() default ""; + /** + * SPEL 参数列表 + */ + String[] parameterKeys() default ""; + /** + * 备注 + */ + String memo() default "default"; +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/BasePrometheusAspect.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/BasePrometheusAspect.java new file mode 100644 index 0000000..5093e85 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/BasePrometheusAspect.java @@ -0,0 +1,41 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +/** + * 基础类 + **/ +@Slf4j +public abstract class BasePrometheusAspect { + + /** + * SpelExpressionParser 获取参数值 + */ + protected T getValue(EvaluationContext context, String key, Class clazz) { + SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); + Expression expression = spelExpressionParser.parseExpression(key); + return expression.getValue(context, clazz); + } + + /** + * 获取参数容器 + */ + protected EvaluationContext getContext(Object[] arguments, Method signatureMethod) { + String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(signatureMethod); + if (parameterNames == null) { + throw new IllegalArgumentException("parameterNames is null"); + } + EvaluationContext evaluationContext = new StandardEvaluationContext(); + for (int i = 0; i < arguments.length; i++) { + evaluationContext.setVariable(parameterNames[i], arguments[i]); + } + return evaluationContext; + } +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusCounterAspect.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusCounterAspect.java new file mode 100644 index 0000000..914b9bd --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusCounterAspect.java @@ -0,0 +1,75 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import cn.lingniu.framework.plugin.prometheus.PrometheusCounter; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import io.micrometer.core.instrument.Tag; +import lombok.extern.slf4j.Slf4j; +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.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.expression.EvaluationContext; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * Count实现类 + **/ +@Slf4j +@Aspect +public class PrometheusCounterAspect extends BasePrometheusAspect { + + @Autowired + PrometheusService prometheusService; + + @Pointcut("@annotation(cn.lingniu.framework.plugin.prometheus.PrometheusCounter)") + public void prometheusCounterAop() { + } + + @Around("prometheusCounterAop() && @annotation(prometheusCounter)") + public Object prometheusCounterAspect(ProceedingJoinPoint point, PrometheusCounter prometheusCounter) throws Throwable { + MethodSignature signature = (MethodSignature) point.getSignature(); + Method signatureMethod = signature.getMethod(); + String countName = ObjectEmptyUtils.isEmpty(prometheusCounter.name()) ? signatureMethod.getName() : prometheusCounter.name(); + Object val = null; + try { + val = point.proceed(); + if (ObjectEmptyUtils.isNotEmpty(prometheusCounter.parameterKeys())) { + prometheusService.count(countName, () -> buildTags(point, prometheusCounter, "SUCCESS")); + } + } catch (Exception ex) { + if (ObjectEmptyUtils.isNotEmpty(prometheusCounter.parameterKeys())) { + prometheusService.count(countName, () -> buildTags(point, prometheusCounter, "FAILED")); + } + throw ex; + } + return val; + } + + private List buildTags(ProceedingJoinPoint point, PrometheusCounter prometheusCounter, String responseStatus) { + EvaluationContext context = getContext(point.getArgs(), ((MethodSignature) point.getSignature()).getMethod()); + List tags = new ArrayList<>(); + + if (ObjectEmptyUtils.isEmpty(prometheusCounter.labels()) || prometheusCounter.labels().length != prometheusCounter.parameterKeys().length) { + // 使用parameterKeys作为标签名 + for (String parameterKey : prometheusCounter.parameterKeys()) { + if (!ObjectEmptyUtils.isEmpty(parameterKey)) { + String cleanKey = parameterKey.replace("#", ""); + tags.add(Tag.of(cleanKey, getValue(context, parameterKey, String.class))); + } + } + } else { + // 使用labels作为标签名 + for (int i = 0; i < prometheusCounter.labels().length; i++) { + tags.add(Tag.of(prometheusCounter.labels()[i], getValue(context, prometheusCounter.parameterKeys()[i], String.class))); + } + } + tags.add(Tag.of("response", responseStatus)); + return tags; + } + + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusMetricsAspect.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusMetricsAspect.java new file mode 100644 index 0000000..1409f31 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusMetricsAspect.java @@ -0,0 +1,73 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import cn.lingniu.framework.plugin.prometheus.PrometheusMetrics; +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; +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.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import javax.servlet.http.HttpServletRequest; + +@Aspect +@Component +public class PrometheusMetricsAspect { + + private static final Counter requestTotal = Counter.build().name("api_request_total").labelNames("api").help + ("total request counter of api").register(); + private static final Counter requestError = Counter.build().name("api_request_error_total").labelNames("api").help + ("response error counter of api").register(); + private static final Histogram histogram = Histogram.build().name("api_response_duration_seconds").labelNames("api").help + ("response consuming of api").register(); + + @Pointcut("@annotation(cn.lingniu.framework.plugin.prometheus.PrometheusMetrics)") + public void prometheusMetricsAop() { + } + + @Around(value = "prometheusMetricsAop() && @annotation(prometheusMetrics)") + public Object prometheusMetricsAspect(ProceedingJoinPoint joinPoint, PrometheusMetrics prometheusMetrics) throws Throwable { + String name; + if (StringUtils.isNotEmpty(prometheusMetrics.name())) { + name = prometheusMetrics.name(); + } else { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (requestAttributes == null) { // 如果没有请求上下文,使用方法名作为指标名 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + name = methodSignature.getMethod().getName(); + } else {// 对URI进行安全过滤,防止路径注入 + HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); + String requestURI = request.getRequestURI(); + name = sanitizeUri(requestURI); + } + } + requestTotal.labels(name).inc(); + Histogram.Timer requestTimer = histogram.labels(name).startTimer(); + Object object; + try { + object = joinPoint.proceed(); + } catch (Throwable t) { + requestError.labels(name).inc(); + throw t; + } finally { + requestTimer.observeDuration(); + } + return object; + } + + /** + * 移除可能的危险字符,只保留字母、数字、斜杠、点、连字符和下划线 + */ + private String sanitizeUri(String uri) { + if (uri == null) { + return "unknown"; + } + return uri.replaceAll("[^a-zA-Z0-9/._-]", "_"); + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusService.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusService.java new file mode 100644 index 0000000..51283ea --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusService.java @@ -0,0 +1,56 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import io.micrometer.core.instrument.Tag; + +import java.util.List; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; + +/** + * PrometheusService 接口 + **/ +public interface PrometheusService { + + /** + * 摘要统计指标 + * @param metricName 指标名称,不能为空 + * @param labelsProvider 标签提供者,不能为空 + * @param amt 统计值,不能为NaN或无穷大 + */ + void summary(String metricName, Supplier> labelsProvider, double amt); + + /** + * 计数指标 + * @param metricName 指标名称,不能为空 + * @param labelsProvider 标签提供者,不能为空 + */ + void count(String metricName, Supplier> labelsProvider); + + /** + * 观察值指标 + * @param metricName 指标名称,不能为空 + * @param labelsProvider 标签提供者,不能为空 + * @param amt 观察值,不能为空且必须为有效数值 + * @param 数值类型,必须继承自Number + */ + void gauge(String metricName, Supplier> labelsProvider, T amt); + + /** + * 观察值指标(带转换函数) + * @param metricName 指标名称,不能为空 + * @param labelsProvider 标签提供者,不能为空 + * @param amt 观察值,不能为空 + * @param valueFunction 值转换函数,不能为空 + * @param 数值类型,必须继承自Number + */ + void gauge(String metricName, Supplier> labelsProvider, T amt, ToDoubleFunction valueFunction); + + /** + * 计时统计 + * @param metricName 指标名称,不能为空 + * @param labelsProvider 标签提供者,不能为空 + * @param amt 时间值,不能为null且不能为NaN或无穷大 + */ + void timer(String metricName, Supplier> labelsProvider, Double amt); + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusServiceImpl.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusServiceImpl.java new file mode 100644 index 0000000..81f3d61 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusServiceImpl.java @@ -0,0 +1,111 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; + + +@Slf4j +@AllArgsConstructor +public class PrometheusServiceImpl implements PrometheusService { + + /** + * @param metricName + * @param lablesProvider + */ + @Override + public void count(String metricName, Supplier> lablesProvider) { + if (metricName == null || lablesProvider == null) { + throw new IllegalArgumentException("metricName and lablesProvider cannot be null"); + } + try { + List labels = lablesProvider.get(); + if (labels == null) { + throw new IllegalArgumentException("Labels from lablesProvider cannot be null"); + } + Metrics.counter(metricName, labels).increment(); + } catch (Exception e) { + throw e; + } + } + + @Override + public void gauge(String metricName, Supplier> lablesProvider, T amt) { + if (metricName == null || lablesProvider == null || amt == null) { + throw new IllegalArgumentException("metricName, lablesProvider, and amt cannot be null"); + } + try { + List labels = lablesProvider.get(); + if (labels == null) { + throw new IllegalArgumentException("Labels from lablesProvider cannot be null"); + } + Metrics.gauge(metricName, labels, amt, (l) -> l.doubleValue()); + } catch (Exception e) { + throw e; + } + } + + @Override + public void gauge(String metricName, Supplier> lablesProvider, T amt, ToDoubleFunction valueFunction) { + if (metricName == null || lablesProvider == null || amt == null || valueFunction == null) { + throw new IllegalArgumentException("metricName, lablesProvider, amt, and valueFunction cannot be null"); + } + try { + List labels = lablesProvider.get(); + if (labels == null) { + throw new IllegalArgumentException("Labels from lablesProvider cannot be null"); + } + Metrics.gauge(metricName, labels, amt, valueFunction); + } catch (Exception e) { + throw e; + } + } + + + @Override + public void timer(String metricName, Supplier> lablesProvider, Double amt) { + if (metricName == null || lablesProvider == null || amt == null) { + throw new IllegalArgumentException("metricName, lablesProvider, and amt cannot be null"); + } + try { + List labels = lablesProvider.get(); + if (labels == null) { + throw new IllegalArgumentException("Labels from lablesProvider cannot be null"); + } + long duration = amt.longValue(); + if (duration < 0) { + throw new IllegalArgumentException("Timer duration cannot be negative: " + amt); + } + Metrics.timer(metricName, labels).record(duration, TimeUnit.MICROSECONDS); + } catch (Exception e) { + throw e; + } + } + + /** + * @param metricName + * @param lablesProvider + */ + @Override + public void summary(String metricName, Supplier> lablesProvider, double amt) { + if (metricName == null || lablesProvider == null) { + throw new IllegalArgumentException("metricName and lablesProvider cannot be null"); + } + try { + List labels = lablesProvider.get(); + if (labels == null) { + throw new IllegalArgumentException("Labels from lablesProvider cannot be null"); + } + Metrics.summary(metricName, labels).record(amt); + } catch (Exception e) { + throw e; + } + } + + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusSummaryAspect.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusSummaryAspect.java new file mode 100644 index 0000000..3974475 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/aspect/PrometheusSummaryAspect.java @@ -0,0 +1,98 @@ +package cn.lingniu.framework.plugin.prometheus.aspect; + +import cn.lingniu.framework.plugin.prometheus.PrometheusSummary; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import io.micrometer.core.instrument.Tag; +import lombok.extern.slf4j.Slf4j; +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.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.expression.EvaluationContext; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * @description: Summary实现类 + **/ +@Slf4j +@Aspect +public class PrometheusSummaryAspect extends BasePrometheusAspect { + + @Autowired + PrometheusService prometheusService; + + @Pointcut("@annotation(cn.lingniu.framework.plugin.prometheus.PrometheusSummary)") + public void prometheusSummaryAop() { + } + + + @Around("prometheusSummaryAop() && @annotation(prometheusSummary)") + public Object prometheusSummaryAspect(ProceedingJoinPoint point, PrometheusSummary prometheusSummary) throws Throwable { + MethodSignature signature = (MethodSignature) point.getSignature(); + Method signatureMethod = signature.getMethod(); + String summaryName = ObjectEmptyUtils.isEmpty(prometheusSummary.name()) ? signatureMethod.getName() : prometheusSummary.name(); + + if (ObjectEmptyUtils.isEmpty(prometheusSummary.parameterKeys())) { + return point.proceed(); + } + + long startTime = System.currentTimeMillis(); + boolean success = false; + Object result = null; + + try { + result = point.proceed(); + success = true; + return result; + } finally { + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + List tags = buildTags(point, signatureMethod, prometheusSummary, success ? "SUCCESS" : "FAILED"); + prometheusService.summary(summaryName, () -> tags, duration); + } + } + + private List buildTags(ProceedingJoinPoint point, Method method, PrometheusSummary prometheusSummary, String responseStatus) { + EvaluationContext context = getContext(point.getArgs(), method); + List tags = new ArrayList<>(); + + String[] parameterKeys = prometheusSummary.parameterKeys(); + String[] labels = prometheusSummary.labels(); + + // 构建标签,优先使用labels数组,否则使用parameterKeys作为标签名 + if (ObjectEmptyUtils.isEmpty(labels) || labels.length != parameterKeys.length) { + for (String paramKey : parameterKeys) { + if (!ObjectEmptyUtils.isEmpty(paramKey)) { + String cleanKey = sanitizeTagKey(paramKey.replace("#", "")); + String value = getValue(context, paramKey, String.class); + tags.add(Tag.of(cleanKey, value)); + } + } + } else { + for (int i = 0; i < labels.length; i++) { + String cleanLabel = sanitizeTagKey(labels[i]); + String value = getValue(context, parameterKeys[i], String.class); + tags.add(Tag.of(cleanLabel, value)); + } + } + tags.add(Tag.of("response", responseStatus)); + return tags; + } + + private String sanitizeTagKey(String key) { + // 防止标签键名中的特殊字符造成问题 + if (key == null) { + return "unknown"; + } + // 只保留字母数字下划线连字符,并限制长度 + return key.replaceAll("[^a-zA-Z0-9_-]", "_").substring(0, Math.min(key.length(), 64)); + } + + + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/config/PrometheusConfig.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/config/PrometheusConfig.java new file mode 100644 index 0000000..6a7384f --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/config/PrometheusConfig.java @@ -0,0 +1,32 @@ +package cn.lingniu.framework.plugin.prometheus.config; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.boot.context.properties.ConfigurationProperties; + + +@Data +@Accessors(chain = true) +@ConfigurationProperties(PrometheusConfig.PRE_FIX) +public class PrometheusConfig { + + public static final String PRE_FIX = "framework.lingniu.prometheus"; + + /** + * 是否启用 + */ + private Boolean enabled = true; + /** + * 端点暴露端口,对外应用接口必须指定 + */ + private Integer port = 30290; + /** + * 是否特殊节点:一般当同机器部署多应用时启用,true时才允许启用自定义管理端点端口 + */ + private Boolean allowAssignPort = false; + /** + * 端点暴露 + */ + private String exposures = "env, health, info, metrics, prometheus, threaddump"; + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusConfiguration.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusConfiguration.java new file mode 100644 index 0000000..886bcdc --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusConfiguration.java @@ -0,0 +1,81 @@ +package cn.lingniu.framework.plugin.prometheus.init; + + +import cn.lingniu.framework.plugin.prometheus.aspect.PrometheusCounterAspect; +import cn.lingniu.framework.plugin.prometheus.aspect.PrometheusMetricsAspect; +import cn.lingniu.framework.plugin.prometheus.aspect.PrometheusSummaryAspect; +import cn.lingniu.framework.plugin.prometheus.config.PrometheusConfig; +import cn.lingniu.framework.plugin.prometheus.aspect.PrometheusService; +import cn.lingniu.framework.plugin.prometheus.aspect.PrometheusServiceImpl; +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import io.micrometer.core.instrument.MeterRegistry; +import io.prometheus.client.CollectorRegistry; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + + +@Configuration +@Import({PrometheusCounterAspect.class, PrometheusMetricsAspect.class, PrometheusSummaryAspect.class}) +@EnableConfigurationProperties({PrometheusConfig.class}) +@Slf4j +public class PrometheusConfiguration implements CommandLineRunner { + + @Autowired + PrometheusConfig prometheusConfig; + @Value("${spring.application.name: 填写项目名称}") + private String applicationName; + + @Bean + MeterRegistryCustomizer appMetricsCommonTags() { + return registry -> registry.config().commonTags("application", applicationName); + } + + @Override + public void run(String... args) { + if (log.isInfoEnabled()) { + try { + String port = (String) PropertyUtils.getProperty("management.server.port"); + // 验证端口号格式 + if (port != null && !port.trim().isEmpty()) { + try { + int portNum = Integer.parseInt(port.trim()); + if (portNum < 1 || portNum > 65535) { + log.warn("Invalid management server port configuration: {}", port); + port = "unknown"; + } + } catch (NumberFormatException e) { + log.warn("Management server port is not a valid number: {}", port); + port = "invalid"; + } + } else { + port = "not configured"; + } + log.info("\r\n\t\t框架已开启Prometheus监控,信息收集端口:{},请至Grafana查询服务监控信息 ! ", port); + } catch (Exception e) { + log.warn("Failed to get management server port, error: {}", e.getMessage()); + } + } + } + + @Bean + @ConditionalOnMissingBean + CollectorRegistry metricRegistry() { + return CollectorRegistry.defaultRegistry; + } + + @Bean + @ConditionalOnMissingBean + public PrometheusService prometheusService() { + return new PrometheusServiceImpl(); + } + + +} diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusInit.java b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusInit.java new file mode 100644 index 0000000..138ed5b --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/java/cn/lingniu/framework/plugin/prometheus/init/PrometheusInit.java @@ -0,0 +1,116 @@ +package cn.lingniu.framework.plugin.prometheus.init; + + +import cn.hutool.core.util.RandomUtil; +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import cn.lingniu.framework.plugin.util.string.StringUtil; +import cn.lingniu.framework.plugin.util.config.PropertyUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Profiles; + + +@Slf4j +@Order(Integer.MIN_VALUE + 400) +public class PrometheusInit implements ApplicationContextInitializer { + //server port + public static final String SERVER_PORT_PROPERTY = "server.port"; + //默认管理端点端口 + public static final Integer DEFAULT_ACTUATOR_PORT = 30290; + private static final String DEFAULT_ENDPOINT = "prometheus,feign,metrics"; + private static boolean initialized = false; + private String applicationName; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + if (applicationContext == null) { + throw new IllegalArgumentException("Application context cannot be null"); + } + ConfigurableEnvironment environment = applicationContext.getEnvironment(); + if (environment == null) { + throw new IllegalStateException("Environment cannot be null"); + } + applicationName = environment.getProperty(CommonConstant.SPRING_APP_NAME_KEY); + if (applicationName == null || applicationName.trim().isEmpty()) { + throw new IllegalStateException("Application name property is required"); + } + String profile = environment.getProperty(CommonConstant.ACTIVE_PROFILES_PROPERTY); + // profile can be null, which is acceptable + this.initializeSystemProperty(environment, applicationContext, applicationName, profile); + } + + void initializeSystemProperty(ConfigurableEnvironment environment, ConfigurableApplicationContext applicationContext, String appName, String profile) { + // 使用volatile确保多线程环境下的可见性 + if (isInitialized()) { + return; + } + // 检查prometheus是否启用 + String prometheusEnabled = environment.getProperty("framework.lingniu.prometheus.enabled", "true"); + if (Boolean.FALSE.toString().equalsIgnoreCase(prometheusEnabled)) { + return; + } + Integer serverPort = environment.getProperty(SERVER_PORT_PROPERTY, Integer.class, 3001); + Integer configPort = determineConfigPort(environment, serverPort); + // 确保配置端口不与服务器端口冲突 + if (configPort.equals(serverPort)) { + configPort = getAvailableRandomPort(environment, serverPort); + } + setInitialized(true); + setManagementProperties(environment, configPort); + } + + private Integer determineConfigPort(ConfigurableEnvironment environment, Integer serverPort) { + Integer defaultPort = DEFAULT_ACTUATOR_PORT; + Boolean allowAssignPort = environment.acceptsProfiles(Profiles.of("sit", "dev", "uat")) || + environment.getProperty("framework.lingniu.prometheus.allow-assign-port", Boolean.class, false); + if (allowAssignPort) { + return environment.getProperty("framework.lingniu.prometheus.port", Integer.class, defaultPort); + } + return defaultPort; + } + + private Integer getAvailableRandomPort(ConfigurableEnvironment environment, Integer serverPort) { + Integer randomPort; + int attempts = 0; + int maxAttempts = 10; // 防止无限循环 + do { + randomPort = RandomUtil.randomInt(2000, 5000); + attempts++; + if (attempts >= maxAttempts) { + throw new RuntimeException("Failed to find available port after " + maxAttempts + " attempts"); + } + } while (randomPort.equals(serverPort)); + + return randomPort; + } + + private void setManagementProperties(ConfigurableEnvironment environment, Integer configPort) { + PropertyUtils.setDefaultInitProperty("management.server.port", configPort.toString()); + PropertyUtils.setDefaultInitProperty("management.endpoint.health.show-details", "always"); + String resultIncludes = buildEndpointIncludes(environment); + PropertyUtils.setDefaultInitProperty("management.endpoints.web.exposure.include", resultIncludes); + } + + private String buildEndpointIncludes(ConfigurableEnvironment environment) { + String resultIncludes = DEFAULT_ENDPOINT; + String configIncludes = environment.getProperty("framework.lingniu.prometheus.exposures"); + if (!StringUtil.isEmpty(configIncludes)) { + resultIncludes = resultIncludes + "," + configIncludes; + } + return resultIncludes; + } + + + private boolean isInitialized() { + return initialized; + } + + private void setInitialized(boolean value) { + initialized = value; + } + + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..b3b3b64 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.prometheus.init.PrometheusConfiguration +org.springframework.context.ApplicationContextInitializer=\ +cn.lingniu.framework.plugin.prometheus.init.PrometheusInit \ No newline at end of file diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/prometheus.md b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/prometheus.md new file mode 100644 index 0000000..0dfc200 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus/src/main/resources/prometheus.md @@ -0,0 +1,34 @@ +# 【重要】prometheus graf...更多使用请参考官方资料 + +## 概述 (Overview) + +1. 定位:集成 prometheus监控框架的应用级监控组件,性能指标统计及异常监控能 +2. 核心能力 + * 支持自定义业务埋点,记录关键业务流程的性能与异常信息 + * jvm指标数据 + * 与grafana联动,实现监控数据可视化与告警 +3. 适用场景 + * 用接口性能监控(响应时间、成功率) + * 业务流程异常追踪与问题定位 + * 系统整体运行状态监控 + * 业务指标大屏告警 + +## 如何配置--参考:PrometheusConfig + +```yaml +framework: + lingniu: + prometheus: + enabled: true + port: 30290 + allowAssignPort: false +``` + +## 如何使用 + +参考三个注解、或使用原生方法使用 +- PrometheusCounter +- PrometheusMetrics +- PrometheusSummary + + diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/pom.xml b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/pom.xml new file mode 100644 index 0000000..08f5db3 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-skywalking + ${project.artifactId} + + 9.2.0 + + + + + org.apache.skywalking + apm-toolkit-trace + ${skywalking.version} + + + org.apache.skywalking + apm-toolkit-opentracing + ${skywalking.version} + + + + + + diff --git a/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/src/main/resources/skywalking.md b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/src/main/resources/skywalking.md new file mode 100644 index 0000000..77152b1 --- /dev/null +++ b/lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking/src/main/resources/skywalking.md @@ -0,0 +1,4 @@ +# skywalking agent对接可参考官方文档 +# skywalking 服务度搭建对接可参考官方文档 + +# https://skywalking.apache.org/docs/skywalking-java/v9.2.0/en/setup/service-agent/java-agent/readme/ \ No newline at end of file diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/pom.xml b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/pom.xml new file mode 100644 index 0000000..197d407 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-rocketmq + lingniu-framework-plugin-rocketmq + http://maven.apache.org + + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + org.springframework.boot + spring-boot-autoconfigure + provided + + + com.alibaba + fastjson + + + org.apache.rocketmq + rocketmq-client + + + org.slf4j + slf4j-api + + + com.alibaba + fastjson + + + + + com.alibaba + fastjson + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.springframework.boot + spring-boot-autoconfigure + true + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + org.apache.rocketmq + rocketmq-tools + provided + true + + + diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqConsumer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqConsumer.java new file mode 100644 index 0000000..6a8e8d3 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqConsumer.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.rocketmq; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface RocketMqConsumer { + + /** + * 自定义消费端名称 + */ + String value(); + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqProducer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqProducer.java new file mode 100644 index 0000000..d564a2c --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/RocketMqProducer.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.rocketmq; + +import cn.lingniu.framework.plugin.rocketmq.core.producer.GeneralRocketMqProducerInner; +import lombok.extern.slf4j.Slf4j; + +import java.io.Serializable; + +/** + * 生产基类 + */ +@Slf4j +public class RocketMqProducer extends GeneralRocketMqProducerInner { +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumeMode.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumeMode.java new file mode 100644 index 0000000..36c01c9 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumeMode.java @@ -0,0 +1,15 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +/** + * 消费者模式 + */ +public enum ConsumeMode { + /** + * 并发消费 + */ + CONCURRENTLY, + /** + * 顺序消费 + */ + ORDERLY +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumerProperties.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumerProperties.java new file mode 100644 index 0000000..86e7c09 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ConsumerProperties.java @@ -0,0 +1,105 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +import lombok.Data; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; + +/** + * 消费端配置信息 + */ +@Data +public class ConsumerProperties { + + /** + * RocketMQ nameServer* + */ + private String nameServer; + /** + * 消费应用BeanName + */ + private String consumerBeanName; + /** + * Topic + */ + private String topic; + /** + * 消费组 + */ + private String consumerGroup; + /** + * 消息表达式类型 默认按照TAG + */ + private SelectorType selectorType = SelectorType.TAG; + /** + * 表达式 + */ + private String selectorExpress; + /** + * 默认同时消费 + */ + private ConsumeMode consumeMode = ConsumeMode.CONCURRENTLY; + /** + * 默认集群消费 + */ + private MessageModel messageModel = MessageModel.CLUSTERING; + /** + * 消费者最小线程 + */ + private Integer consumeThreadMin; + /** + * 消费者最大线程 + */ + private Integer consumeThreadMax; + /** + * 批量消费数量 + * @see MessageListenerConcurrently#consumeMessage + */ + private int consumeMessageBatchMaxSize; + /** + * 批量拉取消息数量 + */ + private int pullBatchSize = 32; + + /** + * 是否批量处理方法 + */ + private Boolean consumerbatchMode; + /** + * 消息的最大重试次数 + */ + private int maxRetryTimes = 15; + /** + * 客户端名称 + */ + private String clientUnitName; + + public ConsumerProperties() { + this("", "", "", "*", 20, 64, 1, 32, 4); + this.nameServer = ""; + } + + public ConsumerProperties( + String consumerGroup, + String consumerBeanName, + String topic, + String selectorExpress, + int consumeThreadMin, + int consumeThreadMax, + int consumeMessageBatchMaxSize, + int pullBatchSize, + int maxRetryTimes + ) { + this.selectorType = SelectorType.TAG; + this.messageModel = MessageModel.CLUSTERING; + this.consumerGroup = consumerGroup; + this.topic = topic; + this.consumerBeanName = consumerBeanName; + this.selectorExpress = selectorExpress; + this.consumeThreadMin = consumeThreadMin; + this.consumeThreadMax = consumeThreadMax; + this.consumeMessageBatchMaxSize = consumeMessageBatchMaxSize; + this.consumeMode = ConsumeMode.CONCURRENTLY; + this.pullBatchSize = pullBatchSize; + this.maxRetryTimes = maxRetryTimes; + } +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ProducerProperties.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ProducerProperties.java new file mode 100644 index 0000000..1cbe8e0 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/ProducerProperties.java @@ -0,0 +1,79 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + + +/** + * 生产端配置信息--很多优化参数可以扩展 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ProducerProperties { + + /** + * RocketMQ nameServer + */ + String nameServer; + /** + * name of producer + */ + private String group; + /** + * 生产端Topic + */ + private String topic; + private String clientUnitName; + /** + * millis of send message timeout + */ + private int sendMsgTimeout = 3000; + /** + * Compress message body threshold, namely, message body larger than 4k will be compressed on default. + */ + private int compressMsgBodyOverHowmuch = 1024 * 4; + /** + *

Maximum number of retry to perform internally before claiming sending failure in synchronous mode.

+ * This may potentially cause message duplication which is up to application developers to resolve. + */ + private int retryTimesWhenSendFailed = 2; + /** + *

Maximum number of retry to perform internally before claiming sending failure in asynchronous mode.

+ * This may potentially cause message duplication which is up to application developers to resolve. + */ + private int retryTimesWhenSendAsyncFailed = 2; + /** + * Indicate whether to retry another broker on sending failure internally. + */ + private boolean retryAnotherBrokerWhenNotStoreOk = false; + /** + * 是否开启异常切换 + */ + private boolean sendLatencyFaultEnable = false; + /** + * Maximum allowed message size in bytes. + */ + private int maxMessageSize = 1024 * 1024 * 4; // 4M + /** + * 事务消息线程配置 + */ + private Transaction transaction = new Transaction(); + /** + * 是否事务型 + */ + private Boolean isTransactionMQ = false; + + @Getter + @Setter + public static class Transaction { + private int corePoolSize = 5; + private int maximumPoolSize = 10; + private int keepAliveTime = 200; + private int queueCapacity = 2000; + } + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/RocketMqConfig.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/RocketMqConfig.java new file mode 100644 index 0000000..1e34ba6 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/RocketMqConfig.java @@ -0,0 +1,27 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.HashMap; +import java.util.Map; + + +@Setter +@Getter +@ConfigurationProperties(prefix = RocketMqConfig.PRE_FIX) +public class RocketMqConfig { + + public final static String PRE_FIX = "framework.lingniu.rocketmq"; + + @Getter + @Setter + private Map producers = new HashMap<>(); + + @Getter + @Setter + private Map consumers = new HashMap<>(); + + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/SelectorType.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/SelectorType.java new file mode 100644 index 0000000..c79a1c6 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/config/SelectorType.java @@ -0,0 +1,19 @@ +package cn.lingniu.framework.plugin.rocketmq.config; + +import org.apache.rocketmq.common.filter.ExpressionType; + +/** + * 过滤类型 + */ +public enum SelectorType { + + /** + * @see ExpressionType#TAG + */ + TAG, + + /** + * @see ExpressionType#SQL92 + */ + SQL92 +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodyJacksonSerializer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodyJacksonSerializer.java new file mode 100644 index 0000000..41a7129 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodyJacksonSerializer.java @@ -0,0 +1,63 @@ +package cn.lingniu.framework.plugin.rocketmq.core; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +/** + * Jackson 序列化 + **/ +@Slf4j +public class RocketMqBodyJacksonSerializer implements RocketMqBodySerializer { + + public static final String DEFAULT_ZONE = "GMT+08:00"; + public static final String DEFAULT_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; + private static ObjectMapper objectMapper; + + static { + objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + SimpleDateFormat smt = new SimpleDateFormat(DEFAULT_FORMAT); + TimeZone timeZone = TimeZone.getTimeZone(DEFAULT_ZONE); + objectMapper.setDateFormat(smt); + objectMapper.setTimeZone(timeZone); + objectMapper.registerModule(new ParameterNamesModule()).registerModule(new Jdk8Module()).registerModule(new JavaTimeModule()); + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + objectMapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false); + objectMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); + } + + + @Override + public byte[] serialize(Object t) { + try { + return objectMapper.writeValueAsBytes(t); + } catch (Exception ex) { + log.error("JacksonMq序列化异常:", ex); + throw new IllegalArgumentException(ex); + } + } + + + @Override + public T deserialize(byte[] bytes, Class clazz) { + try { + return objectMapper.readValue(bytes, clazz); + } catch (Exception ex) { + log.error(String.format("JacksonMq反序列化异常,原始Json: %s", new String(bytes, StandardCharsets.UTF_8)), ex); + throw new IllegalArgumentException(ex); + } + } + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodySerializer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodySerializer.java new file mode 100644 index 0000000..52a3564 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/RocketMqBodySerializer.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.rocketmq.core; + + +/** + * RocketMq 序列化 + **/ +public interface RocketMqBodySerializer { + + byte[] serialize(Object t); + + T deserialize(byte[] bytes, Class clazz); + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/DefaultRocketMqListenerContainer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/DefaultRocketMqListenerContainer.java new file mode 100644 index 0000000..87ccd54 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/DefaultRocketMqListenerContainer.java @@ -0,0 +1,315 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer; + +import cn.lingniu.framework.plugin.rocketmq.core.consumer.listener.MRocketMqListener; +import cn.lingniu.framework.plugin.rocketmq.core.consumer.listener.RocketMqListener; +import cn.lingniu.framework.plugin.rocketmq.config.ConsumeMode; +import cn.lingniu.framework.plugin.rocketmq.config.SelectorType; +import cn.lingniu.framework.plugin.rocketmq.core.RocketMqBodySerializer; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + + +@Slf4j +@SuppressWarnings("all") +public class DefaultRocketMqListenerContainer implements InitializingBean, RocketMqListenerContainer { + + @Setter + @Getter + private long suspendCurrentQueueTimeMillis = 1000; + /** + * Message consume retry strategy
-1,no retry,put into DLQ directly
0,broker control retry frequency
+ * >0,client control retry frequency + */ + @Setter + @Getter + private int delayLevelWhenNextConsume = 0; + @Setter + @Getter + private String consumerGroup; + @Setter + @Getter + private String nameServer; + @Setter + @Getter + private String topic; + @Setter + @Getter + private ConsumeMode consumeMode = ConsumeMode.CONCURRENTLY; + @Setter + @Getter + private SelectorType selectorType = SelectorType.TAG; + @Setter + @Getter + private String selectorExpress = "*"; + @Setter + @Getter + private MessageModel messageModel = MessageModel.CLUSTERING; + /** + * 最大线程数 + */ + @Setter + @Getter + private int consumeThreadMax = 20; + /** + * 最小线程数 + */ + @Setter + @Getter + private int consumeThreadMin = 20; + /** + * 最大重试次数 + */ + @Getter + @Setter + private int maxRetryTimes = 15; + /** + * 批量消费数量 + */ + @Setter + @Getter + private int consumeMessageBatchMaxSize = 1; + /** + * 批量拉取消息数量 + */ + @Setter + @Getter + private int pullBatchSize = 32; + @Setter + @Getter + private int pollNameServerInterval = 1000 * 30; + @Setter + @Getter + private int heartbeatBrokerInterval = 1000 * 30; + @Setter + @Getter + private Boolean isBatchMode; + @Getter + @Setter + private String charset = "UTF-8"; + @Getter + @Setter + private String clientUnitName; + @Getter + @Setter + private RocketMqBodySerializer serializer; + @Setter + @Getter + private boolean started; + @Setter + private RocketMqListener rocketMQListener; + private DefaultMQPushConsumer consumer; + private Class messageType; + + @Override + public void setupMessageListener(RocketMqListener rocketMQListener) { + this.rocketMQListener = rocketMQListener; + } + + @Override + public void destroy() { + this.setStarted(false); + if (Objects.nonNull(consumer)) { + consumer.shutdown(); + } + log.info("DefaultRocketMQListenerContainer destroyed, {}", this.toString()); + } + + + public synchronized void start() throws MQClientException { + if (this.isStarted()) { + throw new IllegalStateException("DefaultRocketMQListenerContainer already started. " + this.toString()); + } + initRocketMQPushConsumer(); + this.messageType = getMessageType(); + consumer.start(); + this.setStarted(true); + if (log.isInfoEnabled()) { + log.info("开启TOPIC: {} 监听端,消费组:{},消息类型:{},线程信息:{} ~ {},消费模式:{},过虑类型:{},标签:{}", this.getTopic(), this.getConsumerGroup(), messageType.getSimpleName(), this.getConsumeThreadMin() + , this.getConsumeThreadMax(), this.consumeMode.name(), this.getSelectorType().name(), this.selectorExpress + ); + } + } + + public class DefaultMessageListenerConcurrently implements MessageListenerConcurrently { + private final String topic; + private final String group; + public DefaultMessageListenerConcurrently(String topic, String group) { + this.topic = topic; + this.group = group; + } + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + List outRetrys = msgs.stream().filter(n -> n.getReconsumeTimes() > maxRetryTimes).collect(Collectors.toList()); + msgs = msgs.stream().filter(n -> n.getReconsumeTimes() <= maxRetryTimes).collect(Collectors.toList()); + if (ObjectEmptyUtils.isEmpty(msgs)) { + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + Throwable throwable = null; + try { + if (rocketMQListener instanceof MRocketMqListener) { + ((MRocketMqListener) rocketMQListener).onMessage( + msgs.stream().map(m -> { + RocketMqConsumerMessage item = new RocketMqConsumerMessage<>(this.topic); + item.setOriginMsg(m); + item.setMsg(doConvertMessage(m)); + return item; + }).collect(Collectors.toList()) + ); + } else { + msgs.forEach(p -> {rocketMQListener.onMessage(doConvertMessage(p), p);}); + } + } catch (Throwable ex) { + throwable = ex; + log.error(String.format("%s 消费 %s:%s 发生系统异常", this.getClass().getSimpleName(), this.topic, this.group), ex); + context.setDelayLevelWhenNextConsume(delayLevelWhenNextConsume); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + } + + public class DefaultMessageListenerOrderly implements MessageListenerOrderly { + private final String topic; + private final String group; + public DefaultMessageListenerOrderly(String topic, String group) { + this.topic = topic; + this.group = group; + } + + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + List outRetrys = msgs.stream().filter(n -> n.getReconsumeTimes() > maxRetryTimes).collect(Collectors.toList()); + msgs = msgs.stream().filter(n -> n.getReconsumeTimes() <= maxRetryTimes).collect(Collectors.toList()); + if (ObjectEmptyUtils.isEmpty(msgs)) { + return ConsumeOrderlyStatus.SUCCESS; + } + Throwable throwable = null; + try { + if (rocketMQListener instanceof MRocketMqListener) { + ((MRocketMqListener) rocketMQListener).onMessage( + msgs.stream().map(m -> { + RocketMqConsumerMessage item = new RocketMqConsumerMessage<>(this.topic); + item.setOriginMsg(m); + item.setMsg(doConvertMessage(m)); + return item; + }).collect(Collectors.toList()) + ); + } else { + msgs.forEach(p -> { + rocketMQListener.onMessage(doConvertMessage(p), p); + }); + } + } catch (Throwable ex) { + throwable = ex; + log.error(String.format("%s 消费 %s:%s 发生系统异常", this.getClass().getSimpleName(), this.topic, this.group), ex); + context.setSuspendCurrentQueueTimeMillis(suspendCurrentQueueTimeMillis); + return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; + } + return ConsumeOrderlyStatus.SUCCESS; + } + } + + @Override + public void afterPropertiesSet() throws Exception { + start(); + } + + @SuppressWarnings("unchecked") + private Object doConvertMessage(MessageExt messageExt) { + if (Objects.equals(messageType, MessageExt.class)) { + return messageExt; + } else { + if (Objects.equals(messageType, String.class)) { + return new String(messageExt.getBody(), Charset.forName(charset)); + } else { + return serializer.deserialize(messageExt.getBody(), messageType); + } + } + } + + private Class getMessageType() { + if (Proxy.isProxyClass(rocketMQListener.getClass())) { + return rocketMQListener.getClassType(); + } + Type[] interfaces = rocketMQListener.getClass().getGenericInterfaces(); + if (Objects.nonNull(interfaces)) { + for (Type type : interfaces) { + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + if (Objects.equals(parameterizedType.getRawType(), RocketMqListener.class) + || Objects.equals(parameterizedType.getRawType(), MRocketMqListener.class)) { + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (Objects.nonNull(actualTypeArguments) && actualTypeArguments.length > 0) { + return (Class) actualTypeArguments[0]; + } else { + return Object.class; + } + } + } + } + return Object.class; + } else { + return Object.class; + } + } + + private void initRocketMQPushConsumer() throws MQClientException { + Assert.notNull(rocketMQListener, "Property 'rocketMQListener' is required"); + Assert.notNull(consumerGroup, "Property 'consumerGroup' is required"); + Assert.notNull(nameServer, "Property 'nameServer' is required"); + Assert.notNull(topic, "Property 'topic' is required"); + consumer = new DefaultMQPushConsumer(consumerGroup); + consumer.setNamesrvAddr(nameServer); + consumer.setConsumeThreadMax(consumeThreadMax); + consumer.setConsumeThreadMin(consumeThreadMin); + consumer.setPullBatchSize(pullBatchSize); + consumer.setMessageModel(messageModel); + consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize); + consumer.setUnitName(clientUnitName); + switch (selectorType) { + case TAG: + consumer.subscribe(topic, selectorExpress); + break; + case SQL92: + consumer.subscribe(topic, MessageSelector.bySql(selectorExpress)); + break; + default: + throw new IllegalArgumentException("Property 'selectorType' was wrong."); + } + switch (consumeMode) { + case ORDERLY: + consumer.registerMessageListener(new DefaultMessageListenerOrderly(topic, consumerGroup)); + break; + case CONCURRENTLY: + consumer.registerMessageListener(new DefaultMessageListenerConcurrently(topic, consumerGroup)); + break; + default: + throw new IllegalArgumentException("Property 'consumeMode' was wrong."); + } + } + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqConsumerMessage.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqConsumerMessage.java new file mode 100644 index 0000000..7699333 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqConsumerMessage.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.apache.rocketmq.common.message.MessageExt; + +@Data +@Accessors(chain = true) +@NoArgsConstructor +public class RocketMqConsumerMessage { + + public RocketMqConsumerMessage(String topic) { + this.topic = topic; + } + + private T msg; + + private String topic; + + private MessageExt originMsg; +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqListenerContainer.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqListenerContainer.java new file mode 100644 index 0000000..7b33bbf --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/RocketMqListenerContainer.java @@ -0,0 +1,10 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer; + +import cn.lingniu.framework.plugin.rocketmq.core.consumer.listener.RocketMqListener; +import org.springframework.beans.factory.DisposableBean; + + +public interface RocketMqListenerContainer extends DisposableBean { + + void setupMessageListener(RocketMqListener messageListener); +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/GeneralMsgListener.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/GeneralMsgListener.java new file mode 100644 index 0000000..52896a9 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/GeneralMsgListener.java @@ -0,0 +1,55 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer.listener; + +import cn.lingniu.framework.plugin.rocketmq.core.consumer.RocketMqConsumerMessage; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.common.message.MessageExt; +import java.io.Serializable; +import java.lang.reflect.ParameterizedType; +import java.util.List; + +@Slf4j +public abstract class GeneralMsgListener implements MRocketMqListener, RocketMqListener { + + @Getter + private Class messageType; + + /** + * 获取真实参数类型 + */ + public GeneralMsgListener() { + ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass(); + this.messageType = (Class) pt.getActualTypeArguments()[0]; + } + + /** + * 消息消费主体 + * @param msg + */ + protected abstract void onMessage(RocketMqConsumerMessage msg); + + /** + * 解决代理模式范型丢失问题 + */ + @Override + public Class getClassType() { + return this.messageType; + } + + @Override + public void onMessage(List> msgs) { + if (ObjectEmptyUtils.isEmpty(msgs)) { + return; + } + msgs.stream().forEach(n -> onMessage(n)); + } + + @Override + public final void onMessage(T message, MessageExt messageExt) { + RocketMqConsumerMessage msg = new RocketMqConsumerMessage<>(); + msg.setMsg(message); + msg.setOriginMsg(messageExt); + onMessage(msg); + } +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/MRocketMqListener.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/MRocketMqListener.java new file mode 100644 index 0000000..20b05fb --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/MRocketMqListener.java @@ -0,0 +1,13 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer.listener; + + +import cn.lingniu.framework.plugin.rocketmq.core.consumer.RocketMqConsumerMessage; + +import java.util.List; + +/** + * 批量消费 + */ +public interface MRocketMqListener extends RocketMqListener { + void onMessage(List> msgs); +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/RocketMqListener.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/RocketMqListener.java new file mode 100644 index 0000000..4cc07c9 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/consumer/listener/RocketMqListener.java @@ -0,0 +1,18 @@ +package cn.lingniu.framework.plugin.rocketmq.core.consumer.listener; + +import org.apache.rocketmq.common.message.MessageExt; + +/** + * @param + */ +public interface RocketMqListener { + + void onMessage(T message, MessageExt messageExt); + + /** + * 消费端处理数据类型 + */ + default Class getClassType() { + return null; + } +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/GeneralRocketMqProducerInner.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/GeneralRocketMqProducerInner.java new file mode 100644 index 0000000..b709713 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/GeneralRocketMqProducerInner.java @@ -0,0 +1,164 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer; + + +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnFail; +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnSuccess; +import cn.lingniu.framework.plugin.util.json.JsonUtil; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.client.producer.selector.SelectMessageQueueByHash; +import org.apache.rocketmq.common.message.MessageConst; +import org.springframework.beans.factory.DisposableBean; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 生产内部基类 + */ +@Slf4j +public abstract class GeneralRocketMqProducerInner implements RocketMqProducerInner, DisposableBean { + + private RocketMqTemplate rocketMQTemplate; + private String topic; + private String tag; + + @Override + public void setRocketMQTemplate(RocketMqTemplate template, String topic) { + rocketMQTemplate = template; + this.topic = topic; + } + + @Override + public void asyncSend(RocketMqSendMsgBody message, SendMessageOnFail onMsgFail) { + asyncSend(message, (m, r) -> {}, onMsgFail); + } + + @Override + public void asyncSend(RocketMqSendMsgBody message) { + asyncSend(message, (m, r) -> {}, (m, ex) -> {}); + } + + @Override + public void asyncSend(RocketMqSendMsgBody message, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail) { + rocketMQTemplate.asyncSend(ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, + ObjectEmptyUtils.isNotEmpty(message.getMessageTag()) ? message.getMessageTag() : this.tag, + message.getBody(), buildMessageProperties(message), new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + if (null != onMsgSuccess) { + onMsgSuccess.call(message.getBody(), sendResult); + } + if (log.isInfoEnabled() && ObjectEmptyUtils.isNotEmpty(message) && ObjectEmptyUtils.isNotEmpty(message.getMessageKey())) { + log.info("消息:{} 异步写入:{} 成功", message.getMessageKey(), ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : topic); + } + } + @Override + public void onException(Throwable e) { + log.error(message.getClass().getSimpleName() + " MQ异步写入失败:" + JsonUtil.bean2Json(message), e); + if (null != onMsgFail) { + onMsgFail.call(message.getBody(), e); + } + } + }, rocketMQTemplate.getProducer().getSendMsgTimeout()); + } + + @Override + public SendResult syncSend(RocketMqSendMsgBody message, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail) { + SendResult result = rocketMQTemplate.syncSend(ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, + ObjectEmptyUtils.isNotEmpty(message.getMessageTag()) ? message.getMessageTag() : this.tag, message.getBody(), buildMessageProperties(message)); + if (!result.getSendStatus().equals(SendStatus.SEND_OK)) { + log.error("消息 {} 同步写入 {} 失败,结果:{}", message.getMessageKey(), ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, result.getSendStatus()); + if (!ObjectEmptyUtils.isEmpty(onMsgFail)) { + onMsgFail.call(message.getBody(), null); + } + } else { + onMsgSuccess.call(message.getBody(), result); + } + return result; + } + + @Override + public SendResult syncSend(RocketMqSendMsgBody message, MessageQueueSelector selector, String selectorKey, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail) { + SendResult result = rocketMQTemplate.syncSend(ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, + ObjectEmptyUtils.isNotEmpty(message.getMessageTag()) ? message.getMessageTag() : this.tag, message.getBody(), buildMessageProperties(message), selector, selectorKey); + if (!result.getSendStatus().equals(SendStatus.SEND_OK)) { + log.error("消息 {} 同步写入 {} 失败,结果:{}", message.getMessageKey(), ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, result.getSendStatus()); + if (!ObjectEmptyUtils.isEmpty(onMsgFail)) { + onMsgFail.call(message.getBody(), null); + } + } else { + onMsgSuccess.call(message.getBody(), result); + } + return result; + } + + @Override + public SendResult syncSend(RocketMqSendMsgBody message, String selectorKey, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail) { + return syncSend(message, new SelectMessageQueueByHash(), selectorKey, onMsgSuccess, onMsgFail); + } + + @Override + public SendResult syncSend(RocketMqSendMsgBody message) { + return syncSend(message, (m, r) -> {}, null); + } + + + @Override + public SendResult syncSendInTransaction(RocketMqSendMsgBody message, Object arg) { + return syncSendInTransaction(message, arg, (n, r) -> {}, (t, e) -> {}); + } + + @Override + public SendResult syncSendInTransaction(RocketMqSendMsgBody message, Object arg, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail) { + SendResult result = rocketMQTemplate.sendMessageInTransaction(ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, + ObjectEmptyUtils.isNotEmpty(message.getMessageTag()) ? message.getMessageTag() : this.tag, + message.getBody(), buildMessageProperties(message), arg); + if (log.isInfoEnabled() && ObjectEmptyUtils.isNotEmpty(message) && ObjectEmptyUtils.isNotEmpty(message.getMessageKey())) { + log.info("消息:{} 事务写入:{} 成功", message.getMessageKey(), ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : topic); + } + if (!result.getSendStatus().equals(SendStatus.SEND_OK)) { + log.error("消息 {} 事务写入 {} 失败,结果:{}", message.getMessageKey(), ObjectEmptyUtils.isNotEmpty(message.getTopic()) ? message.getTopic() : this.topic, result.getSendStatus()); + } else { + onMsgSuccess.call(message.getBody(), result); + } + return result; + } + + @Override + public void asyncSend(Collection> messageBodies) { + if (ObjectEmptyUtils.isEmpty(messageBodies)) { + return; + } + RocketMqSendMsgBody message = messageBodies.stream().findAny().get(); + rocketMQTemplate.asyncSend(message.getTopic(), message.getMessageTag(), + messageBodies.stream().filter(ObjectEmptyUtils::isNotEmpty).collect(Collectors.toList()), + n -> ((RocketMqSendMsgBody) n).getBody(), + n -> buildMessageProperties((RocketMqSendMsgBody) n), + rocketMQTemplate.getProducer().getSendMsgTimeout()); + } + + protected final Map buildMessageProperties(RocketMqSendMsgBody message) { + Map properties = new HashMap<>(); + if (ObjectEmptyUtils.isNotEmpty(message.getMessageKey())) { + properties.put(MessageConst.PROPERTY_KEYS, message.getMessageKey()); + } + if (ObjectEmptyUtils.isNotEmpty(message.getDelayLevel())) { + properties.put(MessageConst.PROPERTY_DELAY_TIME_LEVEL, message.getDelayLevel()); + } + return properties; + } + + @Override + public void destroy() { + Optional.ofNullable(this.rocketMQTemplate).ifPresent(r -> r.destroy()); + } +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqProducerInner.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqProducerInner.java new file mode 100644 index 0000000..9b16a55 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqProducerInner.java @@ -0,0 +1,36 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer; + +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnFail; +import cn.lingniu.framework.plugin.rocketmq.core.producer.call.SendMessageOnSuccess; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendResult; +import java.io.Serializable; +import java.util.Collection; + +/** + * 生产者接口 + */ +public interface RocketMqProducerInner { + + void setRocketMQTemplate(RocketMqTemplate template, String topic); + + void asyncSend(RocketMqSendMsgBody message, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail); + + void asyncSend(RocketMqSendMsgBody message, SendMessageOnFail onMsgFail); + + void asyncSend(RocketMqSendMsgBody message); + + SendResult syncSend(RocketMqSendMsgBody message); + + SendResult syncSend(RocketMqSendMsgBody message, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail); + + SendResult syncSend(RocketMqSendMsgBody message, MessageQueueSelector selector, String selectorKey, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail); + + SendResult syncSend(RocketMqSendMsgBody message, String selectorKey, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail); + + void asyncSend(Collection> msgs); + + SendResult syncSendInTransaction(RocketMqSendMsgBody message, Object args); + + SendResult syncSendInTransaction(RocketMqSendMsgBody message, Object args, SendMessageOnSuccess onMsgSuccess, SendMessageOnFail onMsgFail); +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqSendMsgBody.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqSendMsgBody.java new file mode 100644 index 0000000..743b0a2 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqSendMsgBody.java @@ -0,0 +1,27 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * mq 发送消息body + */ +@Data +@Accessors(chain = true) +public class RocketMqSendMsgBody { + + T body; + + String messageKey; + + String messageTag; + + String topic; + /** + * 级别的定时消息如下:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h + */ + Integer delayLevel; + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqTemplate.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqTemplate.java new file mode 100644 index 0000000..e29ca46 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/RocketMqTemplate.java @@ -0,0 +1,326 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer; + +import cn.lingniu.framework.plugin.rocketmq.core.RocketMqBodySerializer; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.TransactionSendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageConst; +import org.apache.rocketmq.common.message.MessageQueue; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * RockMQTemplate + */ +@Slf4j +public class RocketMqTemplate implements InitializingBean, DisposableBean { + + @Getter + @Setter + private DefaultMQProducer producer; + @Getter + @Setter + private RocketMqBodySerializer serializer; + @Getter + @Setter + private String charset = "UTF-8"; + + public SendResult syncSend(Message message) { + return this.syncSend(message, producer.getSendMsgTimeout()); + } + + public SendResult syncSend(Message message, long timeout) { + try { + SendResult sendResult = producer.send(message, timeout); + return sendResult; + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties) { + return this.syncSend(topic, tag, msgObj, properties, this.producer.getSendMsgTimeout()); + } + + /** + * 同步发送消息 + * @param topic + * @param tag + * @param msgObj + * @param properties + * @param timeout + * @return + */ + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + return this.syncSend(message, timeout); + } + + + public void asyncSend(Message message, SendCallback sendCallback) { + this.asyncSend(message, sendCallback, producer.getSendMsgTimeout()); + } + + public void asyncSend(Message message, SendCallback sendCallback, long timeout) { + try { + producer.send(message, sendCallback, timeout); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + public void asyncSend(String topic, String tag, Collection message, Function bodyFun, Function> properties, long timeout) { + List messages = message.stream().filter(ObjectEmptyUtils::isNotEmpty) + .map(n -> createMessage(topic, tag, bodyFun.apply(n), ObjectEmptyUtils.isNotEmpty(properties) ? properties.apply(n) : null)) + .collect(Collectors.toList()); + try { + producer.send(messages, timeout); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + public void asyncSend(String topic, String tag, Object msgObj, Map properties, SendCallback sendCallback) { + this.asyncSend(topic, tag, msgObj, properties, sendCallback, producer.getSendMsgTimeout()); + } + + /** + * 异步发送消息 + * @param topic + * @param tag + * @param msgObj + * @param properties + * @param sendCallback + * @param timeout + */ + public void asyncSend(String topic, String tag, Object msgObj, Map properties, SendCallback sendCallback, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + this.asyncSend(message, sendCallback, timeout); + } + + public void sendOneWay(Message message) { + try { + producer.sendOneway(message); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * 不关心消息是否送达 + * @param topic + * @param tag + * @param msgObj + * @param properties + */ + public void sendOneWay(String topic, String tag, Object msgObj, Map properties) { + Message message = createMessage(topic, tag, msgObj, properties); + sendOneWay(message); + } + + + public SendResult syncSend(Message message, MessageQueue messageQueue) { + return this.syncSend(message, messageQueue, producer.getSendMsgTimeout()); + } + + public SendResult syncSend(Message message, MessageQueue messageQueue, long timeout) { + try { + SendResult sendResult = producer.send(message, messageQueue, timeout); + return sendResult; + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties, MessageQueue mq) { + return this.syncSend(topic, tag, msgObj, properties, mq, producer.getSendMsgTimeout()); + } + + /** + * 同步发送到指定队列 + * @param topic + * @param tag + * @param msgObj + * @param properties + * @param mq + * @param timeout + */ + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties, MessageQueue mq, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + return this.syncSend(message, mq, timeout); + } + + public void asyncSend(Message message, MessageQueue messageQueue, SendCallback sendCallback) { + this.asyncSend(message, messageQueue, sendCallback, producer.getSendMsgTimeout()); + } + + + public void asyncSend(Message message, MessageQueue messageQueue, SendCallback sendCallback, long timeout) { + try { + producer.send(message, messageQueue, sendCallback, timeout); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * 异步发送到指定队列 + * + * @param topic + * @param tag + * @param msgObj + * @param properties + * @param mq + * @param sendCallback + */ + public void asyncSend(String topic, String tag, Object msgObj, Map properties, MessageQueue mq, SendCallback sendCallback) { + this.asyncSend(topic, tag, msgObj, properties, mq, sendCallback, producer.getSendMsgTimeout()); + } + + + public void asyncSend(String topic, String tag, Object msgObj, Map properties, MessageQueue mq, SendCallback sendCallback, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + this.asyncSend(message, mq, sendCallback, timeout); + } + + /** + * 不关心是否送达到指定队列 + * + * @param message + * @param messageQueue + */ + public void sendOneWay(Message message, MessageQueue messageQueue) { + try { + producer.sendOneway(message, messageQueue); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + + public void sendOneWay(String topic, String tag, Object msgObj, Map properties, MessageQueue mq) { + Message message = createMessage(topic, tag, msgObj, properties); + this.sendOneWay(message, mq); + } + + + public SendResult syncSend(Message message, MessageQueueSelector selector, Object arg) { + return syncSend(message, selector, arg, producer.getSendMsgTimeout()); + } + + public SendResult syncSend(Message message, MessageQueueSelector selector, Object arg, long timeout) { + try { + SendResult sendResult = producer.send(message, selector, arg, timeout); + return sendResult; + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties, MessageQueueSelector selector, Object arg) { + return this.syncSend(topic, tag, msgObj, properties, selector, arg, producer.getSendMsgTimeout()); + } + + public SendResult syncSend(String topic, String tag, Object msgObj, Map properties, MessageQueueSelector selector, Object arg, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + return this.syncSend(message, selector, arg, timeout); + } + + public void asyncSend(Message message, MessageQueueSelector selector, Object arg, SendCallback sendCallback) { + this.asyncSend(message, selector, arg, sendCallback, producer.getSendMsgTimeout()); + } + + public void asyncSend(Message message, MessageQueueSelector selector, Object arg, SendCallback sendCallback, long timeout) { + try { + producer.send(message, selector, arg, sendCallback, timeout); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + public void asyncSend(String topic, String tag, Object msgObj, Map properties, MessageQueueSelector selector, Object arg, SendCallback sendCallback) { + this.asyncSend(topic, tag, msgObj, properties, selector, arg, sendCallback, producer.getSendMsgTimeout()); + } + + public void asyncSend(String topic, String tag, Object msgObj, Map properties, MessageQueueSelector selector, Object arg, SendCallback sendCallback, long timeout) { + Message message = createMessage(topic, tag, msgObj, properties); + this.asyncSend(message, selector, arg, sendCallback, timeout); + } + + public TransactionSendResult sendMessageInTransaction(String topic, String tag, Object msgObj, Map properties, Object arg) { + Message message = createMessage(topic, tag, msgObj, properties); + try { + TransactionSendResult result = producer.sendMessageInTransaction(message, arg); + return result; + } catch (Exception e) { + log.error("send transaction message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + + public void sendOneWay(Message message, MessageQueueSelector selector, Object arg) { + try { + producer.sendOneway(message, selector, arg); + } catch (Exception e) { + log.error("send message failed. destination:{}, message:{} ", message.getTopic() + ":" + message.getTags(), message); + throw new RuntimeException(e.getMessage(), e); + } + } + + + public void sendOneWay(String topic, String tag, Object msgObj, Map properties, MessageQueueSelector selector, Object arg) { + Message message = createMessage(topic, tag, msgObj, properties); + this.sendOneWay(message, selector, arg); + } + + @Override + public void afterPropertiesSet() throws Exception { + Assert.notNull(producer, "Property 'producer' is required"); + producer.start(); + } + + @Override + public void destroy() { + Optional.ofNullable(producer).ifPresent(p -> p.shutdown()); + } + + private Message createMessage(String topic, String tag, Object msgObj, Map properties) { + Message rocketMsg = new Message(topic, tag, serializer.serialize(msgObj)); + if (!CollectionUtils.isEmpty(properties)) { + rocketMsg.setFlag((Integer) properties.getOrDefault("FLAG", 0)); + rocketMsg.setWaitStoreMsgOK((Boolean) properties.getOrDefault(MessageConst.PROPERTY_WAIT_STORE_MSG_OK, true)); + Optional.ofNullable((String) properties.get(MessageConst.PROPERTY_KEYS)).ifPresent(keys -> rocketMsg.setKeys(keys)); + Optional.ofNullable((Integer) properties.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL)).ifPresent(delay -> rocketMsg.setDelayTimeLevel(delay)); + properties.entrySet().stream().filter(entry -> !MessageConst.STRING_HASH_SET.contains(entry.getKey()) && !Objects.equals(entry.getKey(), "FLAG")) + .forEach(entry -> {rocketMsg.putUserProperty(entry.getKey(), String.valueOf(entry.getValue()));}); + } + return rocketMsg; + } +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnFail.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnFail.java new file mode 100644 index 0000000..81d4e5e --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnFail.java @@ -0,0 +1,8 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer.call; + +@FunctionalInterface +public interface SendMessageOnFail { + + void call(T message, Throwable ex); + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnSuccess.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnSuccess.java new file mode 100644 index 0000000..d00bf3b --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/SendMessageOnSuccess.java @@ -0,0 +1,9 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer.call; + +import org.apache.rocketmq.client.producer.SendResult; + +@FunctionalInterface +public interface SendMessageOnSuccess { + + void call(T message, SendResult result); +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/TransactionListenerImpl.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/TransactionListenerImpl.java new file mode 100644 index 0000000..dc29dc4 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/core/producer/call/TransactionListenerImpl.java @@ -0,0 +1,48 @@ +package cn.lingniu.framework.plugin.rocketmq.core.producer.call; + + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @description: TransactionListenerImpl + **/ +@Slf4j +public class TransactionListenerImpl implements TransactionListener { + private AtomicInteger transactionIndex = new AtomicInteger(0); + + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + localTrans.put(msg.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + Integer status = localTrans.get(msg.getTransactionId()); + if (null != status) { + switch (status) { + case 0: + return LocalTransactionState.UNKNOW; + case 1: + return LocalTransactionState.COMMIT_MESSAGE; + case 2: + return LocalTransactionState.ROLLBACK_MESSAGE; + default: + return LocalTransactionState.COMMIT_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } +} + diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqInit.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqInit.java new file mode 100644 index 0000000..2a08029 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqInit.java @@ -0,0 +1,33 @@ +package cn.lingniu.framework.plugin.rocketmq.init; + +import cn.lingniu.framework.plugin.rocketmq.config.RocketMqConfig; +import cn.lingniu.framework.plugin.rocketmq.core.RocketMqBodyJacksonSerializer; +import cn.lingniu.framework.plugin.rocketmq.core.RocketMqBodySerializer; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.impl.MQClientAPIImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + + +/** + * RocketMQ 自动装载 + */ +@Configuration +@EnableConfigurationProperties(RocketMqConfig.class) +@ConditionalOnClass({MQClientAPIImpl.class, DefaultMQPushConsumer.class}) +@Import({RocketMqStartAutoConfiguration.class}) +@Slf4j +public class RocketMqInit { + + @Bean + @ConditionalOnMissingBean + public RocketMqBodySerializer rocketMQSerializer() { + return new RocketMqBodyJacksonSerializer(); + } + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqStartAutoConfiguration.java b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqStartAutoConfiguration.java new file mode 100644 index 0000000..4510127 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/java/cn/lingniu/framework/plugin/rocketmq/init/RocketMqStartAutoConfiguration.java @@ -0,0 +1,208 @@ +package cn.lingniu.framework.plugin.rocketmq.init; + +import cn.lingniu.framework.plugin.core.context.SpringBeanApplicationContext; +import cn.lingniu.framework.plugin.rocketmq.RocketMqConsumer; +import cn.lingniu.framework.plugin.rocketmq.RocketMqProducer; +import cn.lingniu.framework.plugin.rocketmq.core.consumer.DefaultRocketMqListenerContainer; +import cn.lingniu.framework.plugin.rocketmq.core.consumer.listener.RocketMqListener; +import cn.lingniu.framework.plugin.rocketmq.config.ConsumerProperties; +import cn.lingniu.framework.plugin.rocketmq.config.ProducerProperties; +import cn.lingniu.framework.plugin.rocketmq.config.RocketMqConfig; +import cn.lingniu.framework.plugin.rocketmq.core.producer.RocketMqProducerInner; +import cn.lingniu.framework.plugin.rocketmq.core.producer.RocketMqTemplate; +import cn.lingniu.framework.plugin.rocketmq.core.RocketMqBodySerializer; +import cn.lingniu.framework.plugin.util.validation.ObjectEmptyUtils; +import cn.lingniu.framework.plugin.util.string.StringUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.client.producer.TransactionMQProducer; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.util.Assert; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; + +@Slf4j +@Configuration +@ConditionalOnClass(DefaultMQPushConsumer.class) +@EnableConfigurationProperties(RocketMqConfig.class) +public class RocketMqStartAutoConfiguration implements ApplicationContextAware { + + @Autowired + private RocketMqConfig rocketMQConfig; + @Autowired + private RocketMqBodySerializer mqSerializer; + + private AtomicLong counter = new AtomicLong(0); + private ConfigurableApplicationContext applicationContext; + + + @Async + @Order(1000) + @EventListener(WebServerInitializedEvent.class) + public void afterStart(WebServerInitializedEvent event) { + if ("management".equalsIgnoreCase(event.getApplicationContext().getServerNamespace())) { + return; + } + startContainer(); + } + + public void startContainer() { + Map producerBeans = SpringBeanApplicationContext.getBeans(RocketMqProducer.class); + Optional.ofNullable(producerBeans).ifPresent(b -> b.forEach((k, v) -> { + List keys = Arrays.asList(StringUtil.split(v.getClass().getName(), ".")); + if (ObjectEmptyUtils.isNotEmpty(keys)) { + String beanName = keys.get(keys.size() - 1); + if (ObjectEmptyUtils.isNotEmpty(rocketMQConfig.getProducers())) { + String mapName = rocketMQConfig.getProducers().keySet().stream().filter(m -> m.equalsIgnoreCase(beanName)).findAny().orElse(null); + if (ObjectEmptyUtils.isNotEmpty(mapName)) { + registerContainer(k, v, rocketMQConfig.getProducers().get(mapName)); + } else { + log.error("{} 生产端配置异常,未开启!", k); + } + } + } + })); + + //region 配置消费端 + Map beans = this.applicationContext.getBeansWithAnnotation(RocketMqConsumer.class); + Optional.ofNullable(beans).ifPresent(b -> b.forEach((k, v) -> { + List keys = Arrays.asList(StringUtil.split(replacePattern(v.toString(), "\\@[A-Za-z0-9]+$", ""), ".")); + if (ObjectEmptyUtils.isNotEmpty(keys)) { + String beanName = keys.get(keys.size() - 1); + if (ObjectEmptyUtils.isNotEmpty(rocketMQConfig.getConsumers()) + && (rocketMQConfig.getConsumers().entrySet().stream().anyMatch(n -> beanName.equalsIgnoreCase(n.getKey())) + || rocketMQConfig.getConsumers().entrySet().stream().anyMatch(n -> beanName.equalsIgnoreCase(n.getValue().getConsumerBeanName())))) { + if (rocketMQConfig.getConsumers().entrySet().stream().anyMatch(n -> beanName.equalsIgnoreCase(n.getValue().getConsumerBeanName()))) { + registerContainer(k, v, rocketMQConfig.getConsumers().entrySet().stream().filter(n -> beanName.equalsIgnoreCase(n.getValue().getConsumerBeanName())) + .findAny().get().getValue()); + return; + } + registerContainer(k, v, rocketMQConfig.getConsumers().entrySet().stream().filter(r -> beanName.equalsIgnoreCase(r.getKey())).findAny().get().getValue()); + } else { + log.error("{} 消费端配置异常,未开启!", k); + } + } + })); + } + + + private void registerContainer(String beanName, Object bean, ProducerProperties producerProperties) { + Class clazz = AopUtils.getTargetClass(bean); + if (!RocketMqProducerInner.class.isAssignableFrom(bean.getClass())) { + throw new IllegalStateException(clazz + " is not instance of " + RocketMqProducerInner.class.getName()); + } + RocketMqProducerInner producer = (RocketMqProducerInner) bean; + String groupName = producerProperties.getGroup(); + Assert.hasText(groupName, "[framework.lingniu.rocketmq.producers.xxx.group] must not be null"); + DefaultMQProducer defaultMQProducer = !(producer instanceof TransactionListener) ? + new DefaultMQProducer(producerProperties.getGroup()) + : new TransactionMQProducer(producerProperties.getGroup()); + if (producer instanceof TransactionListener) { + ((TransactionMQProducer) defaultMQProducer).setExecutorService(new ThreadPoolExecutor( + producerProperties.getTransaction().getCorePoolSize(), producerProperties.getTransaction().getMaximumPoolSize(), + producerProperties.getTransaction().getKeepAliveTime(), TimeUnit.SECONDS, + new ArrayBlockingQueue<>(producerProperties.getTransaction().getQueueCapacity()), r -> { + Thread thread = new Thread(r); + thread.setName(String.format("client-transaction-msg-check-thread-%s", groupName)); + return thread; + }) + ); + ((TransactionMQProducer) defaultMQProducer).setTransactionListener((TransactionListener) producer); + } + defaultMQProducer.setNamesrvAddr(producerProperties.getNameServer()); + defaultMQProducer.setSendMsgTimeout(producerProperties.getSendMsgTimeout()); + defaultMQProducer.setRetryTimesWhenSendFailed(producerProperties.getRetryTimesWhenSendFailed()); + defaultMQProducer.setRetryTimesWhenSendAsyncFailed(producerProperties.getRetryTimesWhenSendAsyncFailed()); + defaultMQProducer.setMaxMessageSize(producerProperties.getMaxMessageSize()); + defaultMQProducer.setCompressMsgBodyOverHowmuch(producerProperties.getCompressMsgBodyOverHowmuch()); + defaultMQProducer.setRetryAnotherBrokerWhenNotStoreOK(producerProperties.isRetryAnotherBrokerWhenNotStoreOk()); + defaultMQProducer.setUnitName(producerProperties.getClientUnitName()); + defaultMQProducer.setSendLatencyFaultEnable(producerProperties.isSendLatencyFaultEnable()); + RocketMqTemplate template = new RocketMqTemplate(); + template.setProducer(defaultMQProducer); + template.setSerializer(mqSerializer); + producer.setRocketMQTemplate(template, producerProperties.getTopic()); + try { + template.afterPropertiesSet(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void registerContainer(String beanName, Object bean, ConsumerProperties consumerProperties) { + Class clazz = AopUtils.getTargetClass(bean); + if (!RocketMqListener.class.isAssignableFrom(bean.getClass())) { + log.warn("{} is not instance of {}", clazz, RocketMqListener.class.getName()); + return; + } + RocketMqListener rocketMQListener = (RocketMqListener) bean; + BeanDefinitionBuilder beanBuilder = BeanDefinitionBuilder.rootBeanDefinition(DefaultRocketMqListenerContainer.class); + if (ObjectEmptyUtils.isNotEmpty(consumerProperties)) { + beanBuilder.addPropertyValue("nameServer", consumerProperties.getNameServer()); + beanBuilder.addPropertyValue("topic", consumerProperties.getTopic()); + beanBuilder.addPropertyValue("consumerGroup", consumerProperties.getConsumerGroup()); + beanBuilder.addPropertyValue("consumeMode", consumerProperties.getConsumeMode()); + beanBuilder.addPropertyValue("consumeThreadMax", consumerProperties.getConsumeThreadMax()); + beanBuilder.addPropertyValue("consumeThreadMin", consumerProperties.getConsumeThreadMin()); + beanBuilder.addPropertyValue("messageModel", consumerProperties.getMessageModel()); + beanBuilder.addPropertyValue("selectorExpress", consumerProperties.getSelectorExpress()); + beanBuilder.addPropertyValue("selectorType", consumerProperties.getSelectorType()); + beanBuilder.addPropertyValue("rocketMQListener", rocketMQListener); + beanBuilder.addPropertyValue("consumeMessageBatchMaxSize", consumerProperties.getConsumeMessageBatchMaxSize()); + beanBuilder.addPropertyValue("pullBatchSize", consumerProperties.getPullBatchSize()); + beanBuilder.addPropertyValue("maxRetryTimes", consumerProperties.getMaxRetryTimes()); + beanBuilder.addPropertyValue("isBatchMode", consumerProperties.getConsumerbatchMode()); + beanBuilder.addPropertyValue("clientUnitName", consumerProperties.getClientUnitName()); + } else { + log.error("消费端 {} 缺少配置信息,请查看!", rocketMQListener.getClass().getSimpleName()); + } + + beanBuilder.addPropertyValue("serializer", mqSerializer); + beanBuilder.setDestroyMethodName("destroy"); + String containerBeanName = String.format("%s_%s", DefaultRocketMqListenerContainer.class.getName(), counter.incrementAndGet()); + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory(); + beanFactory.registerBeanDefinition(containerBeanName, beanBuilder.getBeanDefinition()); + DefaultRocketMqListenerContainer container = beanFactory.getBean(containerBeanName, DefaultRocketMqListenerContainer.class); + if (!container.isStarted()) { + try { + container.start(); + } catch (Exception e) { + log.error("started container failed. {}", container, e); + throw new RuntimeException(e); + } + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = (ConfigurableApplicationContext) applicationContext; + } + + private String replacePattern(final String source, final String regex, final String replacement) { + return Pattern.compile(regex, Pattern.DOTALL).matcher(source).replaceAll(replacement); + } + +} diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..f157573 --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.rocketmq.init.RocketMqInit diff --git a/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/mq-config.md b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/mq-config.md new file mode 100644 index 0000000..555de0e --- /dev/null +++ b/lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq/src/main/resources/mq-config.md @@ -0,0 +1,118 @@ +# 【重要】RocketMq服务端搭建和更多详细资料---参考官网 + +## 概述 (Overview) + +1. 基于 RocketMqClient 封装的分布式消息处理组件 +2. 核心能力 + +* 消息处理能力 + +- 双模式监听:支持批量消息处理(MRocketMQListener)和单条消息处理(RocketMQListener) +- 生产端支持:通过继承 GeneralMqProducer 快速实现消息生产功能 +- 消费端支持:通过继承 GeneralMsgListener 和实现监听接口处理消息 +- 多样化消息类型:支持普通消息、定时消息、延迟消息等多种消息类型 + +* 配置管理 + - 统一配置前缀:所有配置项使用 framework.lingniu.rocketmq 作为根路径 + - NameServer配置:支持直接配置 nameServer 地址连接RocketMQ集群 + - 消费策略配置:支持配置消费线程数、批量大小、重试次数等参数 + +3. 适用场景: + - 异步任务处理:数据同步等后台任务处理 + - 系统解耦:微服务间通过消息队列进行松耦合通信 + - 流量削峰:应对突发业务高峰,平滑处理大量消息 + - 可靠消息传输:通过重试机制保证重要业务消息不丢失 + +## 如何配置--更多参数参考:RedissonConfig/RedissonProperties + +```yaml 最小配置例子 +framework: + lingniu: + # rocketmq 最小化配置示例 + rocketmq: + consumers: + TestC1Consumer: + nameServer: mq_test_n1.tst.mid:9876 + consumerGroup: consumer1 + topic: test1-yw-topic + consumerBeanName: TestC1Consumer + producers: + TestMqProducer: + nameServer: mq_test_n1.tst.mid:9876 + group: test1-yw-topic-producer + topic: test1-yw-topic + TestTransProducer: + nameServer: mq_test_n1.tst.mid:9876 + group: test1-yw-trans-topic-producer + topic: test1-yw-trans-topic + isTransactionMQ: true +``` +```yaml 详细配置 + + + +framework: + lingniu: + # rocketmq所有配置示例 + rocketmq: + # 是否开启数据源加密 + encryptEnabled: false + # 消费者配置 + consumers: + # 消费者bean名称 + consumer1: + nameServer: mq_test_n1.tst.mid:9876 + # 消费组 + consumerGroup: consumer1 + # 消费对象名字 + consumerBeanName: consumer1 + # Topic + topic: test1-yw-topic + # 订阅表达式,默认为 * + selectorExpress: "*" + # 消费者最小线程数 + consumeThreadMin: 20 + # 消费者最大线程数 + consumeThreadMax: 32 + # 批量消费数量 + consumeMessageBatchMaxSize: 1 + # 批量拉取消息数量 + pullBatchSize: 32 + # 消息的最大重试次数 + maxRetryTimes: 16 + + # 生产者配置 + producers: + # 生产者的bean名称 + TestMqProducer: + nameServer: mq_test_n1.tst.mid:9876 + # 生产者组 + group: test1-yw-topic-producer + # 生产端Topic + topic: test1-yw-topic + # 发送超时(ms),默认 3000 + sendMsgTimeout: 3000 + # 消息体压缩阀值(kb),默认 4096 + compressMsgBodyOverHowmuch: 4096 + # 同步发送消息失败,是否重新发送,默认 2 + retryTimesWhenSendFailed: 2 + # 异步发送消息失败,是否重新发送,默认 2 + retryTimesWhenSendAsyncFailed: 2 + # 重试另一个Broker当发送消息失败, 默认 false + retryAnotherBrokerWhenNotStoreOk: false + # 默认不启用延迟容错,通过统计每个队列的发送耗时情况来计算broker是否可用 + sendLatencyFaultEnable: false + TestTransMqProducer: + group: test1-yw-trans-topic-producer + topic: test1-yw-trans-topic + # 是否事务型 + isTransactionMQ: true + # 事务消息线程配置 + transaction: + corePoolSize: 5 + # 本地事务执行结果查询线程池配置 + maximumPoolSize: 10 + keepAliveTime: 200 + queueCapacity: 2000 +``` + diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/pom.xml b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/pom.xml new file mode 100644 index 0000000..6047c26 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../../../lingniu-framework-dependencies/pom.xml + + lingniu-framework-plugin-web + ${project.artifactId} + + + + cn.lingniu.framework + lingniu-framework-plugin-util + + + cn.lingniu.framework + lingniu-framework-plugin-core + + + cn.hutool + hutool-all + + + org.springframework.boot + spring-boot + + + org.springframework + spring-core + + + org.springframework.boot + spring-boot-actuator + + + org.springframework + spring-aop + + + org.springframework.boot + spring-boot-autoconfigure + compile + true + + + org.springframework.boot + spring-boot-starter-aop + + + com.google.guava + guava + + + com.google.code.gson + gson + + + + org.apache.httpcomponents + httpclient + + + commons-io + commons-io + + + + org.springframework + spring-webmvc + compile + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + javax.servlet + javax.servlet-api + provided + true + + + + diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/ApplicationStartEventListener.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/ApplicationStartEventListener.java new file mode 100644 index 0000000..bfbcfce --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/ApplicationStartEventListener.java @@ -0,0 +1,45 @@ +package cn.lingniu.framework.plugin.web; + +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.Async; +import org.springframework.util.StringUtils; + +@Slf4j +@Configuration +public class ApplicationStartEventListener { + + @Async + @Order + @EventListener(WebServerInitializedEvent.class) + public void afterStart(WebServerInitializedEvent event) { + try { + if (event == null || event.getWebServer() == null) { + log.warn("Web server initialized event is null or web server is null"); + return; + } + + Environment environment = event.getApplicationContext().getEnvironment(); + if (environment == null) { + log.warn("Application environment is null"); + return; + } + + String appName = environment.getProperty(CommonConstant.SPRING_APP_NAME_KEY, "UNKNOWN").toUpperCase(); + int localPort = event.getWebServer().getPort(); + String profile = StringUtils.arrayToCommaDelimitedString(environment.getActiveProfiles()); + String containerType = event.getWebServer().getClass().getSimpleName(); + + log.info("Application [{}] started successfully on port [{}] with profiles [{}], container type: [{}]", + appName, localPort, profile, containerType); + + } catch (Exception e) { + log.error("Error occurred while handling web server initialized event", e); + } + } +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/ApiAccessLog.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/ApiAccessLog.java new file mode 100644 index 0000000..ada80f0 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/ApiAccessLog.java @@ -0,0 +1,49 @@ +package cn.lingniu.framework.plugin.web.apilog; + + +import cn.lingniu.framework.plugin.web.apilog.enums.OperateTypeEnum; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 访问日志注解 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiAccessLog { + + // ========== 开关字段 ========== + + /** + * 是否记录访问日志 + */ + boolean enable() default true; + /** + * 是否记录请求参数 + * + * 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭 + */ + boolean requestEnable() default true; + /** + * 是否记录响应结果 + * + * 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开 + */ + boolean responseEnable() default false; + /** + * 敏感参数数组 + * + * 添加后,请求参数、响应结果不会记录该参数 + */ + String[] sanitizeKeys() default {}; + /** + * 操作分类 + * + * 实际并不是数组,因为枚举不能设置 null 作为默认值 + */ + OperateTypeEnum[] operateType() default {}; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/enums/OperateTypeEnum.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/enums/OperateTypeEnum.java new file mode 100644 index 0000000..34c70b1 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/enums/OperateTypeEnum.java @@ -0,0 +1,51 @@ +package cn.lingniu.framework.plugin.web.apilog.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作日志的操作类型 + * + * @author portal + */ +@Getter +@AllArgsConstructor +public enum OperateTypeEnum { + + /** + * 查询 + */ + GET(1), + /** + * 新增 + */ + CREATE(2), + /** + * 修改 + */ + UPDATE(3), + /** + * 删除 + */ + DELETE(4), + /** + * 导出 + */ + EXPORT(5), + /** + * 导入 + */ + IMPORT(6), + /** + * 其它 + * + * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 + */ + OTHER(0); + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/filter/ApiAccessLogFilter.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/filter/ApiAccessLogFilter.java new file mode 100644 index 0000000..d471c6f --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/filter/ApiAccessLogFilter.java @@ -0,0 +1,242 @@ +package cn.lingniu.framework.plugin.web.apilog.filter; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.BooleanUtil; +import cn.hutool.core.util.StrUtil; +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants; +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import cn.lingniu.framework.plugin.web.apilog.ApiAccessLog; +import cn.lingniu.framework.plugin.web.apilog.enums.OperateTypeEnum; +import cn.lingniu.framework.plugin.web.apilog.interceptor.ApiAccessLogInterceptor; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogCommonApi; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import cn.lingniu.framework.plugin.web.bae.filter.ApiRequestFilter; +import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Iterator; +import java.util.Map; + + +/** + * API 访问日志 Filter + * todo 目的:记录 API 访问日志输出---后续可以输出db + * + */ +@Slf4j +public class ApiAccessLogFilter extends ApiRequestFilter { + + private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"}; + + private final String applicationName; + + private final ApiAccessLogCommonApi apiAccessLogApi; + + public ApiAccessLogFilter(FrameworkWebConfig frameworkWebConfig, String applicationName, ApiAccessLogCommonApi apiAccessLogApi) { + super(frameworkWebConfig); + this.applicationName = applicationName; + this.apiAccessLogApi = apiAccessLogApi; + } + + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // 获得开始时间 + LocalDateTime beginTime = LocalDateTime.now(); + // 提前获得参数,避免 XssFilter 过滤处理 + Map queryString = ServletUtils.getParamMap(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; + + try { + // 继续过滤器 + filterChain.doFilter(request, response); + // 正常执行,记录日志 + createApiAccessLog(request, beginTime, queryString, requestBody, null); + } catch (Exception ex) { + // 异常执行,记录日志 + createApiAccessLog(request, beginTime, queryString, requestBody, ex); + throw ex; + } + } + + private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime, + Map queryString, String requestBody, Exception ex) { + ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO(); + try { + boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex); + if (!enable) { + return; + } + apiAccessLogApi.createApiAccessLogAsync(accessLog); + } catch (Throwable th) { + log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), JsonUtils.toJsonString(accessLog), th); + } + } + + private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime, + Map queryString, String requestBody, Exception ex) { + // 判断:是否要记录操作日志 + HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ApiAccessLogInterceptor.ATTRIBUTE_HANDLER_METHOD); + ApiAccessLog accessLogAnnotation = null; + if (handlerMethod != null) { + accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class); + if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) { + return false; + } + } + + // 处理用户信息 + accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + accessLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置访问结果 + CommonResult result = WebFrameworkUtils.getCommonResult(request); + if (result != null) { + accessLog.setResultCode(result.getCode()); + accessLog.setResultMsg(result.getMsg()); + } else if (ex != null) { + accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()); + accessLog .setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); + } else { + accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + accessLog.setResultMsg(""); + } + // 设置请求字段 + accessLog.setRequestUrl(request.getRequestURI()); + accessLog.setRequestMethod(request.getMethod()); + accessLog.setApplicationName(ApplicationNameContext.getApplicationName()); + accessLog.setUserAgent(ServletUtils.getUserAgent(request)); + accessLog.setUserIp(ServletUtils.getClientIP(request)); + String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null; + Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE; + if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false + Map requestParams = MapUtil.builder() + .put("query", sanitizeMap(queryString, sanitizeKeys)) + .put("body", sanitizeJson(requestBody, sanitizeKeys)).build(); + accessLog.setRequestParams(JsonUtils.toJsonString(requestParams)); + } + Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE; + if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true + accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys)); + } + // 持续时间 + accessLog.setBeginTime(beginTime); + accessLog.setEndTime(LocalDateTime.now()); + accessLog.setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS)); + // 操作模块 + OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ? + accessLogAnnotation.operateType()[0] : parseOperateLogType(request); + accessLog.setOperateType(operateType.getType()); + return true; + } + + // ========== 解析 @ApiAccessLog、@Swagger 注解 ========== + + private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) { + RequestMethod requestMethod = ArrayUtil.firstMatch(method -> + StrUtil.equalsAnyIgnoreCase(method.name(), request.getMethod()), RequestMethod.values()); + if (requestMethod == null) { + return OperateTypeEnum.OTHER; + } + switch (requestMethod) { + case GET: + return OperateTypeEnum.GET; + case POST: + return OperateTypeEnum.CREATE; + case PUT: + return OperateTypeEnum.UPDATE; + case DELETE: + return OperateTypeEnum.DELETE; + default: + return OperateTypeEnum.OTHER; + } + } + + // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ========== + + private static String sanitizeMap(Map map, String[] sanitizeKeys) { + if (CollUtil.isEmpty(map)) { + return null; + } + if (sanitizeKeys != null) { + MapUtil.removeAny(map, sanitizeKeys); + } + MapUtil.removeAny(map, SANITIZE_KEYS); + return JsonUtils.toJsonString(map); + } + + private static String sanitizeJson(String jsonString, String[] sanitizeKeys) { + if (StrUtil.isEmpty(jsonString)) { + return null; + } + try { + JsonNode rootNode = JsonUtils.parseTree(jsonString); + sanitizeJson(rootNode, sanitizeKeys); + return JsonUtils.toJsonString(rootNode); + } catch (Exception e) { + // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 + log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); + return jsonString; + } + } + + private static String sanitizeJson(CommonResult commonResult, String[] sanitizeKeys) { + if (commonResult == null) { + return null; + } + String jsonString = JsonUtils.toJsonString(commonResult); + try { + JsonNode rootNode = JsonUtils.parseTree(jsonString); + sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉 + return JsonUtils.toJsonString(rootNode); + } catch (Exception e) { + // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 + log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); + return jsonString; + } + } + + private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) { + // 情况一:数组,遍历处理 + if (node.isArray()) { + for (JsonNode childNode : node) { + sanitizeJson(childNode, sanitizeKeys); + } + return; + } + // 情况二:非 Object,只是某个值,直接返回 + if (!node.isObject()) { + return; + } + // 情况三:Object,遍历处理 + Iterator> iterator = node.fields(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if (ArrayUtil.contains(sanitizeKeys, entry.getKey()) + || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) { + iterator.remove(); + continue; + } + sanitizeJson(entry.getValue(), sanitizeKeys); + } + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/interceptor/ApiAccessLogInterceptor.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/interceptor/ApiAccessLogInterceptor.java new file mode 100644 index 0000000..e81adbb --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/interceptor/ApiAccessLogInterceptor.java @@ -0,0 +1,100 @@ +package cn.lingniu.framework.plugin.web.apilog.interceptor; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.StrUtil; +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import cn.lingniu.framework.plugin.util.spring.SpringUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StopWatch; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.IntStream; + +/** + * API 访问日志 Interceptor + * + * todo 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。 + */ +@Slf4j +public class ApiAccessLogInterceptor implements HandlerInterceptor { + + public static final String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD"; + + private static final String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch"; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用 + HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null; + if (handlerMethod != null) { + request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod); + } + + // 打印 request 日志 + if (!SpringUtils.isProd()) { + Map queryString = ServletUtils.getParamMap(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; + if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) { + log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI()); + } else { + log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(), + StrUtil.blankToDefault(requestBody, queryString.toString())); + } + // 计时 + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch); + // 打印 Controller 路径 + printHandlerMethodPosition(handlerMethod); + } + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + // 打印 response 日志 + if (!SpringUtils.isProd()) { + StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH); + stopWatch.stop(); + log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]", + request.getRequestURI(), stopWatch.getTotalTimeMillis()); + } + } + + /** + * 打印 Controller 方法路径 + */ + private void printHandlerMethodPosition(HandlerMethod handlerMethod) { + if (handlerMethod == null) { + return; + } + Method method = handlerMethod.getMethod(); + Class clazz = method.getDeclaringClass(); + try { + // 获取 method 的 lineNumber + List clazzContents = FileUtil.readUtf8Lines( + ResourceUtil.getResource(null, clazz).getPath().replace("/target/classes/", "/src/main/java/") + + clazz.getSimpleName() + ".java"); + Optional lineNumber = IntStream.range(0, clazzContents.size()) + .filter(i -> clazzContents.get(i).contains(" " + method.getName() + "(")) // 简单匹配,不考虑方法重名 + .mapToObj(i -> i + 1) // 行号从 1 开始 + .findFirst(); + if (!lineNumber.isPresent()) { + return; + } + // 打印结果 + System.out.printf("\tController 方法路径:%s(%s.java:%d)\n", clazz.getName(), clazz.getSimpleName(), lineNumber.get()); + } catch (Exception ignore) { + // 忽略异常。原因:仅仅打印,非重要逻辑 + } + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogApiImpl.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogApiImpl.java new file mode 100644 index 0000000..8c61148 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogApiImpl.java @@ -0,0 +1,26 @@ +package cn.lingniu.framework.plugin.web.apilog.logger; + +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogCommonApi; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogService; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * API 访问日志的 API 实现类 + */ +@Service +@Validated +public class ApiAccessLogApiImpl implements ApiAccessLogCommonApi { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @Override + public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { + apiAccessLogService.createApiAccessLog(createDTO); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogServiceImpl.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogServiceImpl.java new file mode 100644 index 0000000..59f2fe7 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiAccessLogServiceImpl.java @@ -0,0 +1,32 @@ +package cn.lingniu.framework.plugin.web.apilog.logger; + +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import cn.lingniu.framework.plugin.util.object.BeanUtils; +import cn.lingniu.framework.plugin.util.string.StrUtils; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogService; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogDO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + + +/** + * API 访问日志 Service 实现类 todo 可以扩展 + */ +@Slf4j +@Service +@Validated +public class ApiAccessLogServiceImpl implements ApiAccessLogService { + + + @Override + public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { + ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class); + apiAccessLog.setRequestParams(StrUtils.maxLength(apiAccessLog.getRequestParams(), ApiAccessLogDO.REQUEST_PARAMS_MAX_LENGTH)); + apiAccessLog.setResultMsg(StrUtils.maxLength(apiAccessLog.getResultMsg(), ApiAccessLogDO.RESULT_MSG_MAX_LENGTH)); + log.info("api请求正常,详细信息:{}", JsonUtils.toJsonString(createDTO)); + } + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogApiImpl.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogApiImpl.java new file mode 100644 index 0000000..c25fe7d --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogApiImpl.java @@ -0,0 +1,25 @@ +package cn.lingniu.framework.plugin.web.apilog.logger; + +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogCommonApi; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogService; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * API 访问日志的 API 接口 + */ +@Service +public class ApiErrorLogApiImpl implements ApiErrorLogCommonApi { + + @Resource + private ApiErrorLogService apiErrorLogService; + + @Override + public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { + apiErrorLogService.createApiErrorLog(createDTO); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogServiceImpl.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogServiceImpl.java new file mode 100644 index 0000000..246c0de --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/ApiErrorLogServiceImpl.java @@ -0,0 +1,32 @@ +package cn.lingniu.framework.plugin.web.apilog.logger; + +import cn.hutool.json.JSONObject; +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import cn.lingniu.framework.plugin.util.object.BeanUtils; +import cn.lingniu.framework.plugin.util.string.StrUtils; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogService; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogDO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + + +/** + * API 错误日志 Service 实现类 todo 可以扩展 + */ +@Service +@Validated +@Slf4j +public class ApiErrorLogServiceImpl implements ApiErrorLogService { + + + @Override + public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { + ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class) ; + apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(), ApiErrorLogDO.REQUEST_PARAMS_MAX_LENGTH)); + log.error("api请求异常,详细信息:{}", JsonUtils.toJsonString(apiErrorLog)); + } + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogCommonApi.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogCommonApi.java new file mode 100644 index 0000000..c35871b --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogCommonApi.java @@ -0,0 +1,30 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.api; + +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO; +import org.springframework.scheduling.annotation.Async; + +import javax.validation.Valid; + +/** + * API 访问日志的 API 接口 + */ +public interface ApiAccessLogCommonApi { + + /** + * 创建 API 访问日志 + * + * @param createDTO 创建信息 + */ + void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO); + + /** + * 【异步】创建 API 访问日志 + * + * @param createDTO 访问日志 DTO + */ + @Async + default void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) { + createApiAccessLog(createDTO); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogService.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogService.java new file mode 100644 index 0000000..7c88617 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiAccessLogService.java @@ -0,0 +1,18 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.api; + + +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiAccessLogCreateReqDTO; + +/** + * API 访问日志 Service 接口 + */ +public interface ApiAccessLogService { + + /** + * 创建 API 访问日志 + * + * @param createReqDTO API 访问日志 + */ + void createApiAccessLog(ApiAccessLogCreateReqDTO createReqDTO); + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogCommonApi.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogCommonApi.java new file mode 100644 index 0000000..0759e2f --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogCommonApi.java @@ -0,0 +1,29 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.api; + +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO; +import org.springframework.scheduling.annotation.Async; + +import javax.validation.Valid; + +/** + * API 错误日志的 API 接口 + */ +public interface ApiErrorLogCommonApi { + + /** + * 创建 API 错误日志 + * + * @param createDTO 创建信息 + */ + void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO); + + /** + * 【异步】创建 API 异常日志 + * + * @param createDTO 异常日志 DTO + */ + @Async + default void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) { + createApiErrorLog(createDTO); + } +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogService.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogService.java new file mode 100644 index 0000000..f60c67b --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/api/ApiErrorLogService.java @@ -0,0 +1,18 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.api; + + +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO; + +/** + * API 错误日志 Service 接口 + */ +public interface ApiErrorLogService { + + /** + * 创建 API 错误日志 + * + * @param createReqDTO API 错误日志 + */ + void createApiErrorLog(ApiErrorLogCreateReqDTO createReqDTO); + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogCreateReqDTO.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogCreateReqDTO.java new file mode 100644 index 0000000..1c9f26a --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogCreateReqDTO.java @@ -0,0 +1,89 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.model; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 访问日志---后续可以跟用户登陆信息互通 + */ +@Data +public class ApiAccessLogCreateReqDTO { + + /** + * 用户编号 + */ + private Long userId = 0L; + /** + * 用户类型 + */ + private Integer userType = 0; + + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + private String requestParams; + /** + * 响应结果 + */ + private String responseBody; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + /** + * 开始请求时间 + */ + @NotNull(message = "开始请求时间不能为空") + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + @NotNull(message = "结束请求时间不能为空") + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + /** + * 结果码 + */ + @NotNull(message = "错误码不能为空") + private Integer resultCode; + /** + * 结果提示 + */ + private String resultMsg; + + /** + * 操作分类 + * + * 枚举,参见 OperateTypeEnum 类 + */ + private Integer operateType; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogDO.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogDO.java new file mode 100644 index 0000000..c154901 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiAccessLogDO.java @@ -0,0 +1,99 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.model; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * API 访问日志 + */ +@Data +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiAccessLogDO { + + /** + * {@link #requestParams} 的最大长度 + */ + public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000; + + /** + * {@link #resultMsg} 的最大长度 + */ + public static final Integer RESULT_MSG_MAX_LENGTH = 512; + + + /** + * 应用名 + * + * 目前读取 `spring.application.name` 配置项 + */ + private String applicationName; + + // ========== 请求相关字段 ========== + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 响应结果 + */ + private String responseBody; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 执行相关字段 ========== + + + /** + * 开始请求时间 + */ + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + + /** + * 结果码 + * + * 目前使用的 {@link CommonResult#getCode()} 属性 + */ + private Integer resultCode; + /** + * 结果提示 + * + * 目前使用的 {@link CommonResult#getMsg()} 属性 + */ + private String resultMsg; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogCreateReqDTO.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogCreateReqDTO.java new file mode 100644 index 0000000..c6ab3e2 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogCreateReqDTO.java @@ -0,0 +1,101 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.model; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 错误日志 + */ +@Data +public class ApiErrorLogCreateReqDTO { + + /** + * 账号编号 + */ + private Long userId = 0L; + /** + * 用户类型 + */ + private Integer userType = 0; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 异常时间 + */ + @NotNull(message = "异常时间不能为空") + private LocalDateTime exceptionTime; + /** + * 异常名 + */ + @NotNull(message = "异常名不能为空") + private String exceptionName; + /** + * 异常发生的类全名 + */ + @NotNull(message = "异常发生的类全名不能为空") + private String exceptionClassName; + /** + * 异常发生的类文件 + */ + @NotNull(message = "异常发生的类文件不能为空") + private String exceptionFileName; + /** + * 异常发生的方法名 + */ + @NotNull(message = "异常发生的方法名不能为空") + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + */ + @NotNull(message = "异常发生的方法所在行不能为空") + private Integer exceptionLineNumber; + /** + * 异常的栈轨迹异常的栈轨迹 + */ + @NotNull(message = "异常的栈轨迹不能为空") + private String exceptionStackTrace; + /** + * 异常导致的根消息 + */ + @NotNull(message = "异常导致的根消息不能为空") + private String exceptionRootCauseMessage; + /** + * 异常导致的消息 + */ + @NotNull(message = "异常导致的消息不能为空") + private String exceptionMessage; + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogDO.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogDO.java new file mode 100644 index 0000000..fb1054f --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/apilog/logger/model/ApiErrorLogDO.java @@ -0,0 +1,118 @@ +package cn.lingniu.framework.plugin.web.apilog.logger.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * API 异常数据 + */ +@Data +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor + +public class ApiErrorLogDO { + + /** + * {@link #requestParams} 的最大长度 + */ + public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000; + + + /** + * 应用名 + * + * 目前读取 spring.application.name + */ + private String applicationName; + + // ========== 请求相关字段 ========== + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 异常相关字段 ========== + + /** + * 异常发生时间 + */ + private LocalDateTime exceptionTime; + /** + * 异常名 + * + * {@link Throwable#getClass()} 的类全名 + */ + private String exceptionName; + /** + * 异常导致的消息 + * + * {@link cn.hutool.core.exceptions.ExceptionUtil#getMessage(Throwable)} + */ + private String exceptionMessage; + /** + * 异常导致的根消息 + * + * {@link cn.hutool.core.exceptions.ExceptionUtil#getRootCauseMessage(Throwable)} + */ + private String exceptionRootCauseMessage; + /** + * 异常的栈轨迹 + * + * {@link org.apache.commons.lang3.exception.ExceptionUtils#getStackTrace(Throwable)} + */ + private String exceptionStackTrace; + /** + * 异常发生的类全名 + * + * {@link StackTraceElement#getClassName()} + */ + private String exceptionClassName; + /** + * 异常发生的类文件 + * + * {@link StackTraceElement#getFileName()} + */ + private String exceptionFileName; + /** + * 异常发生的方法名 + * + * {@link StackTraceElement#getMethodName()} + */ + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + * + * {@link StackTraceElement#getLineNumber()} + */ + private Integer exceptionLineNumber; + + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/ApiRequestFilter.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/ApiRequestFilter.java new file mode 100644 index 0000000..d846815 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/ApiRequestFilter.java @@ -0,0 +1,38 @@ +package cn.lingniu.framework.plugin.web.bae.filter; + +import cn.hutool.core.util.StrUtil; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import lombok.RequiredArgsConstructor; +import org.springframework.util.CollectionUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.http.HttpServletRequest; + +/** + * 可以过滤排除指定的url + * + */ +@RequiredArgsConstructor +public abstract class ApiRequestFilter extends OncePerRequestFilter { + + protected final FrameworkWebConfig frameworkWebConfig; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + if (Boolean.FALSE.equals(frameworkWebConfig.getApiLog().getEnable())) { + return true; + } + if (CollectionUtils.isEmpty(frameworkWebConfig.getApiLog().getExcludeUrls())) { + return false; + } + // 过滤 API 请求的地址 + String apiUri = request.getRequestURI().substring(request.getContextPath().length()); + for (String exclude : frameworkWebConfig.getApiLog().getExcludeUrls()) { + if (StrUtil.startWithAny(apiUri, exclude)) { + return true; + } + } + return false; + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyFilter.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyFilter.java new file mode 100644 index 0000000..9ff0a11 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyFilter.java @@ -0,0 +1,41 @@ +package cn.lingniu.framework.plugin.web.bae.filter; + +import cn.hutool.core.util.StrUtil; +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Request Body 缓存 Filter,实现它的可重复读取 + */ +public class CacheRequestBodyFilter extends OncePerRequestFilter { + + /** + * 需要排除的 URI + * 1. 排除 Spring Boot Admin 相关请求,避免客户端连接中断导致的异常。 + */ + private static final String[] IGNORE_URIS = {"/admin/", "/actuator/"}; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(new CacheRequestBodyWrapper(request), response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 1. 校验是否为排除的 URL + String requestURI = request.getRequestURI(); + if (StrUtil.startWithAny(requestURI, IGNORE_URIS)) { + return true; + } + // 2. 只处理 json 请求内容 + return !ServletUtils.isJsonRequest(request); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyWrapper.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyWrapper.java new file mode 100644 index 0000000..a7906b2 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/filter/CacheRequestBodyWrapper.java @@ -0,0 +1,75 @@ +package cn.lingniu.framework.plugin.web.bae.filter; + + +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; + +/** + * Request Body 缓存 Wrapper + */ +public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { + + /** + * 缓存的内容 + */ + private final byte[] body; + + public CacheRequestBodyWrapper(HttpServletRequest request) { + super(request); + body = ServletUtils.getBodyBytes(request); + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public int getContentLength() { + return body.length; + } + + @Override + public long getContentLengthLong() { + return body.length; + } + + @Override + public ServletInputStream getInputStream() { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); + // 返回 ServletInputStream + return new ServletInputStream() { + + @Override + public int read() { + return inputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) {} + + @Override + public int available() { + return body.length; + } + + }; + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalExceptionHandler.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..4a18093 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalExceptionHandler.java @@ -0,0 +1,383 @@ +package cn.lingniu.framework.plugin.web.bae.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.context.ApplicationNameContext; +import cn.lingniu.framework.plugin.core.exception.ServerException; +import cn.lingniu.framework.plugin.core.exception.ServiceException; +import cn.lingniu.framework.plugin.core.exception.util.ServiceExceptionUtil; +import cn.lingniu.framework.plugin.util.collection.SetUtils; +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiErrorLogCommonApi; +import cn.lingniu.framework.plugin.web.apilog.logger.model.ApiErrorLogCreateReqDTO; +import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.google.common.util.concurrent.UncheckedExecutionException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.util.Assert; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.servlet.NoHandlerFoundException; +import javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.ValidationException; +import java.nio.file.AccessDeniedException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static cn.lingniu.framework.plugin.core.exception.enums.GlobalErrorCodeConstants.*; + + +/** + * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 + */ +//@RestControllerAdvice +@ControllerAdvice +@ResponseBody +@AllArgsConstructor +@Slf4j +public class GlobalExceptionHandler { + + /** + * 忽略的 ServiceException 错误提示,避免打印过多 logger + */ + public static final Set IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌"); + + private final ApiErrorLogCommonApi apiErrorLogApi; + + /** + * 处理所有异常,主要是提供给 Filter 使用 + * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 + * + * @param request 请求 + * @param ex 异常 + * @return 通用返回 + */ + public CommonResult allExceptionHandler(HttpServletRequest request, Throwable ex) { + if (ex instanceof MissingServletRequestParameterException) { + return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); + } + if (ex instanceof MethodArgumentTypeMismatchException) { + return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); + } + if (ex instanceof MethodArgumentNotValidException) { + return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); + } + if (ex instanceof BindException) { + return bindExceptionHandler((BindException) ex); + } + if (ex instanceof ConstraintViolationException) { + return constraintViolationExceptionHandler((ConstraintViolationException) ex); + } + if (ex instanceof ValidationException) { + return validationException((ValidationException) ex); + } + if (ex instanceof MaxUploadSizeExceededException) { + return maxUploadSizeExceededExceptionHandler((MaxUploadSizeExceededException) ex); + } + if (ex instanceof NoHandlerFoundException) { + return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); + } + if (ex instanceof HttpRequestMethodNotSupportedException) { + return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); + } + if (ex instanceof HttpMediaTypeNotSupportedException) { + return httpMediaTypeNotSupportedExceptionHandler((HttpMediaTypeNotSupportedException) ex); + } + if (ex instanceof ServiceException) { + return serviceExceptionHandler((ServiceException) ex); + } + if (ex instanceof AccessDeniedException) { + return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); + } + return defaultExceptionHandler(request, ex); + } + + /** + * 处理 SpringMVC 请求参数缺失 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 + */ + @ExceptionHandler(value = MissingServletRequestParameterException.class) + public CommonResult missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { + log.warn("[missingServletRequestParameterExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); + } + + /** + * 处理 SpringMVC 请求参数类型错误 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public CommonResult methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { + log.warn("[methodArgumentTypeMismatchExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); + } + + /** + * 处理 SpringMVC 参数校验不正确 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { + log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); + // 获取 errorMessage + String errorMessage = null; + FieldError fieldError = ex.getBindingResult().getFieldError(); + if (fieldError == null) { + // 组合校验,参考自 https://t.zsxq.com/3HVTx + List allErrors = ex.getBindingResult().getAllErrors(); + if (CollUtil.isNotEmpty(allErrors)) { + errorMessage = allErrors.get(0).getDefaultMessage(); + } + } else { + errorMessage = fieldError.getDefaultMessage(); + } + // 转换 CommonResult + if (StrUtil.isEmpty(errorMessage)) { + return CommonResult.error(BAD_REQUEST); + } + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", errorMessage)); + } + + /** + * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 + */ + @ExceptionHandler(BindException.class) + public CommonResult bindExceptionHandler(BindException ex) { + log.warn("[handleBindException]", ex); + FieldError fieldError = ex.getFieldError(); + assert fieldError != null; // 断言,避免告警 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + } + + /** + * 处理 SpringMVC 请求参数类型错误 + * + * 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @SuppressWarnings("PatternVariableCanBeUsed") + public CommonResult methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { + log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); + if (ex.getCause() instanceof InvalidFormatException) { + InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause(); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue())); + } + if (StrUtil.startWith(ex.getMessage(), "Required request body is missing")) { + return CommonResult.error(BAD_REQUEST.getCode(), "请求参数类型错误: request body 缺失"); + } + return defaultExceptionHandler(ServletUtils.getRequest(), ex); + } + + /** + * 处理 Validator 校验不通过产生的异常 + */ + @ExceptionHandler(value = ConstraintViolationException.class) + public CommonResult constraintViolationExceptionHandler(ConstraintViolationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + ConstraintViolation constraintViolation = ex.getConstraintViolations().iterator().next(); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); + } + + /** + * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 + */ + @ExceptionHandler(value = ValidationException.class) + public CommonResult validationException(ValidationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 + return CommonResult.error(BAD_REQUEST); + } + + /** + * 处理上传文件过大异常 + */ + @ExceptionHandler(MaxUploadSizeExceededException.class) + public CommonResult maxUploadSizeExceededExceptionHandler(MaxUploadSizeExceededException ex) { + return CommonResult.error(BAD_REQUEST.getCode(), "上传文件过大,请调整后重试"); + } + + /** + * 处理 SpringMVC 请求地址不存在 + * + * 注意,它需要设置如下两个配置项: + * 1. spring.mvc.throw-exception-if-no-handler-found 为 true + * 2. spring.mvc.static-path-pattern 为 /statics/** + */ + @ExceptionHandler(NoHandlerFoundException.class) + public CommonResult noHandlerFoundExceptionHandler(NoHandlerFoundException ex) { + log.warn("[noHandlerFoundExceptionHandler]", ex); + return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); + } + + /** + * 处理 SpringMVC 请求方法不正确 + * + * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public CommonResult httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { + log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); + return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); + } + + /** + * 处理 SpringMVC 请求的 Content-Type 不正确 + * + * 例如说,A 接口的 Content-Type 为 application/json,结果请求的 Content-Type 为 application/octet-stream,导致不匹配 + */ + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public CommonResult httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException ex) { + log.warn("[httpMediaTypeNotSupportedExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求类型不正确:%s", ex.getMessage())); + } + + /** + * 处理 Spring Security 权限不足的异常 + * + * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 + */ + @ExceptionHandler(value = AccessDeniedException.class) + public CommonResult accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { + log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req), + req.getRequestURL(), ex); + return CommonResult.error(FORBIDDEN); + } + + /** + * 处理 Guava UncheckedExecutionException + * + * 例如说,缓存加载报错,可见 https://t.zsxq.com/UszdH + */ + @ExceptionHandler(value = UncheckedExecutionException.class) + public CommonResult uncheckedExecutionExceptionHandler(HttpServletRequest req, UncheckedExecutionException ex) { + return allExceptionHandler(req, ex.getCause()); + } + + /** + * 处理业务异常 ServiceException 例如说,商品库存不足,用户手机号已存在。 + */ + @ExceptionHandler(value = ServiceException.class) + public CommonResult serviceExceptionHandler(ServiceException ex) { + // 不包含的时候,才进行打印,避免 ex 堆栈过多 + if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) { + // 即使打印,也只打印第一层 StackTraceElement,并且使用 warn 在控制台输出,更容易看到 + try { + StackTraceElement[] stackTraces = ex.getStackTrace(); + for (StackTraceElement stackTrace : stackTraces) { + if (ObjUtil.notEqual(stackTrace.getClassName(), ServiceExceptionUtil.class.getName())) { + log.warn("[serviceExceptionHandler]\n\t{}", stackTrace); + break; + } + } + } catch (Exception ignored) { + // 忽略日志,避免影响主流程 + } + } + return CommonResult.error(ex.getCode(), ex.getMessage()); + } + + /** + * 处理业务异常 ServiceException 例如说,商品库存不足,用户手机号已存在。 + */ + @ExceptionHandler(value = ServerException.class) + public CommonResult serverExceptionHandler(ServerException ex) { + try { + StackTraceElement[] stackTraces = ex.getStackTrace(); + for (StackTraceElement stackTrace : stackTraces) { + if (ObjUtil.notEqual(stackTrace.getClassName(), ServiceExceptionUtil.class.getName())) { + log.warn("[serverExceptionHandler]\n\t{}", stackTrace); + break; + } + } + } catch (Exception ignored) { + // 忽略日志,避免影响主流程 + } + return CommonResult.error(ex.getCode(), ex.getMessage()); + } + + /** + * 处理系统异常,兜底处理所有的一切 + */ + @ExceptionHandler(value = Exception.class) + public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { + // 特殊:如果是 ServiceException 的异常,则直接返回 + if (ex.getCause() != null && ex.getCause() instanceof ServiceException) { + return serviceExceptionHandler((ServiceException) ex.getCause()); + } + if (ex.getCause() != null && ex.getCause() instanceof ServerException) { + return serviceExceptionHandler((ServiceException) ex.getCause()); + } + // 情况二:处理异常 + log.error("[defaultExceptionHandler]", ex); + // 插入异常日志 + createExceptionLog(req, ex); + // 返回 ERROR CommonResult + return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + + private void createExceptionLog(HttpServletRequest req, Throwable e) { + // 插入错误日志 + ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO(); + try { + // 初始化 errorLog + buildExceptionLog(errorLog, req, e); + // 执行插入 errorLog + apiErrorLogApi.createApiErrorLogAsync(errorLog); + } catch (Throwable th) { + log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); + } + } + + private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) { + // 处理用户信息 + errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置异常字段 + errorLog.setExceptionName(e.getClass().getName()); + errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); + errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); + errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e)); + StackTraceElement[] stackTraceElements = e.getStackTrace(); + Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); + StackTraceElement stackTraceElement = stackTraceElements[0]; + errorLog.setExceptionClassName(stackTraceElement.getClassName()); + errorLog.setExceptionFileName(stackTraceElement.getFileName()); + errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); + errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); + // 设置其它字段 + errorLog.setApplicationName(ApplicationNameContext.getApplicationName()); + errorLog.setRequestUrl(request.getRequestURI()); + Map requestParams = MapUtil.builder() + .put("query", ServletUtils.getParamMap(request)) + .put("body", ServletUtils.getBody(request)).build(); + errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); + errorLog.setRequestMethod(request.getMethod()); + errorLog.setUserAgent(ServletUtils.getUserAgent(request)); + errorLog.setUserIp(ServletUtils.getClientIP(request)); + errorLog.setExceptionTime(LocalDateTime.now()); + } + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalResponseBodyHandler.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalResponseBodyHandler.java new file mode 100644 index 0000000..98df1b4 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/handler/GlobalResponseBodyHandler.java @@ -0,0 +1,42 @@ +package cn.lingniu.framework.plugin.web.bae.handler; + +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +/** + * 全局响应结果(ResponseBody)处理器 + * + */ +@ControllerAdvice +public class GlobalResponseBodyHandler implements ResponseBodyAdvice { + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public boolean supports(MethodParameter returnType, Class converterType) { + if (returnType.getMethod() == null) { + return false; + } + // 只拦截返回结果为 CommonResult 类型 + return returnType.getMethod().getReturnType() == CommonResult.class; + } + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + // 记录 Controller 结果 + if (returnType.getMethod().getReturnType() == CommonResult.class) { + WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body); + return body; + } + return body; + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/interceptor/ResponseCheckInterceptor.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/interceptor/ResponseCheckInterceptor.java new file mode 100644 index 0000000..5b79ec6 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/interceptor/ResponseCheckInterceptor.java @@ -0,0 +1,39 @@ +package cn.lingniu.framework.plugin.web.bae.interceptor; + +import cn.hutool.core.util.ClassUtil; +import cn.lingniu.framework.plugin.core.base.CommonResult; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +@Slf4j +@Order(Ordered.LOWEST_PRECEDENCE + 1000) +public class ResponseCheckInterceptor extends HandlerInterceptorAdapter implements HandlerInterceptor { + + @Override + public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { + if (!(o instanceof HandlerMethod)) { + return; + } + HandlerMethod handlerMethod = (HandlerMethod) o; + boolean flag = ClassUtil.isAssignable(CommonResult.class, handlerMethod.getMethod().getReturnType()) + || ClassUtil.isAssignable(Iterable.class, handlerMethod.getMethod().getReturnType()); + if (log.isErrorEnabled() && !flag) { + log.error("\r\n\t {} 接口响应返回类型为:{} 不符合规范\r\n\t\t返回类型必须是:CommonResult,响应规范请查看:GlobalResponseBodyHandler, 异常响应体实现细则请查看:cn.lingniu.framework.plugin.web.base.handler.GlobalExceptionHandler", + handlerMethod.getMethod().getName(), handlerMethod.getMethod().getReturnType().getName()); + } + } + + @Override + public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/util/WebFrameworkUtils.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/util/WebFrameworkUtils.java new file mode 100644 index 0000000..b50bad0 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/bae/util/WebFrameworkUtils.java @@ -0,0 +1,123 @@ +package cn.lingniu.framework.plugin.web.bae.util; + +import cn.hutool.core.util.NumberUtil; +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.base.TerminalEnum; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +/** + * 专属于 web 包的工具类 + */ +public class WebFrameworkUtils { + + //todo 后续扩展 + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type"; + //todo 后续扩展 + + private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; + + /** + * 终端的 Header + * + * @see TerminalEnum + */ + public static final String HEADER_TERMINAL = "terminal"; + + private static FrameworkWebConfig frameworkWebConfig; + + public WebFrameworkUtils(FrameworkWebConfig frameworkWebConfig) { + WebFrameworkUtils.frameworkWebConfig = frameworkWebConfig; + } + + public static void setLoginUserId(ServletRequest request, Long userId) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); + } + + /** + * 设置用户类型 + * + * @param request 请求 + * @param userType 用户类型 + */ + public static void setLoginUserType(ServletRequest request, Integer userType) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType); + } + + /** + * 获得当前用户的编号,从请求中 + * 注意:该方法仅限于 framework 框架使用!!! + * + * @param request 请求 + * @return 用户编号 + */ + public static Long getLoginUserId(HttpServletRequest request) { + if (request == null) { + return null; + } + return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); + } + + /** + * 获得当前用户的类型 + * 注意:该方法仅限于 web 相关的 framework 组件使用!!! + * + * @param request 请求 + * @return 用户编号 + */ + public static Integer getLoginUserType(HttpServletRequest request) { + if (request == null) { + return null; + } + // 1. 优先,从 Attribute 中获取 + Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); + if (userType != null) { + return userType; + } + return null; + } + + public static Integer getLoginUserType() { + HttpServletRequest request = getRequest(); + return getLoginUserType(request); + } + + public static Long getLoginUserId() { + HttpServletRequest request = getRequest(); + return getLoginUserId(request); + } + + public static Integer getTerminal() { + HttpServletRequest request = getRequest(); + if (request == null) { + return TerminalEnum.UNKNOWN.getTerminal(); + } + String terminalValue = request.getHeader(HEADER_TERMINAL); + return NumberUtil.parseInt(terminalValue, TerminalEnum.UNKNOWN.getTerminal()); + } + + public static void setCommonResult(ServletRequest request, CommonResult result) { + request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result); + } + + public static CommonResult getCommonResult(ServletRequest request) { + return (CommonResult) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); + } + + @SuppressWarnings("PatternVariableCanBeUsed") + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (!(requestAttributes instanceof ServletRequestAttributes)) { + return null; + } + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; + return servletRequestAttributes.getRequest(); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiEncryptProperties.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiEncryptProperties.java new file mode 100644 index 0000000..55dd661 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiEncryptProperties.java @@ -0,0 +1,63 @@ +package cn.lingniu.framework.plugin.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * HTTP API 加解密配置 + * + */ +@Data +public class ApiEncryptProperties { + + /** + * 是否开启 + */ + private Boolean enable = false; + + /** + * 请求头(响应头)名称不能为空 + * + * 1. 如果该请求头非空,则表示请求参数已被「前端」加密,「后端」需要解密 + * 2. 如果该响应头非空,则表示响应结果已被「后端」加密,「前端」需要解密 + */ + private String header = "X-Api-Encrypt"; + + /** + * 对称加密算法,用于请求/响应的加解密---对称加密算法不能为空 + * + * 目前支持 + * 【对称加密】: + * 1. {@link cn.hutool.crypto.symmetric.SymmetricAlgorithm#AES} + * 2. {@link cn.hutool.crypto.symmetric.SM4#ALGORITHM_NAME} (需要自己二次开发,成本低) + * 【非对称加密】 + * 1. {@link cn.hutool.crypto.asymmetric.AsymmetricAlgorithm#RSA} + * 2. {@link cn.hutool.crypto.asymmetric.SM2} (需要自己二次开发,成本低) + * + * @see 什么是公钥和私钥? + */ + private String algorithm = "AES"; + + /** + * 请求的解密密钥---请求的解密密钥不能为空 + * + * 注意: + * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 + * 2. 如果是【非对称加密】时,它「后端」对应的是“私钥”。对应的,「前端」对应的是“公钥”。(重要!!!) + */ + private String requestKey = ""; + + /** + * 响应的加密密钥-- 响应的加密密钥不能为空 + * + * 注意: + * 1. 如果是【对称加密】时,它「后端」对应的是“密钥”。对应的,「前端」也对应的也是“密钥”。 + * 2. 如果是【非对称加密】时,它「后端」对应的是“公钥”。对应的,「前端」对应的是“私钥”。(重要!!!) + */ + private String responseKey = ""; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiLogProperties.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiLogProperties.java new file mode 100644 index 0000000..f37a613 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/ApiLogProperties.java @@ -0,0 +1,22 @@ +package cn.lingniu.framework.plugin.web.config; + +import com.google.common.collect.Lists; +import lombok.Data; + +import java.util.List; + +@Data +public class ApiLogProperties { + + /** + * 是否开启 + */ + private Boolean enable = false; + + /** + * 排除的url + */ + private List excludeUrls = Lists.newArrayList(); + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/FrameworkWebConfig.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/FrameworkWebConfig.java new file mode 100644 index 0000000..8f63174 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/config/FrameworkWebConfig.java @@ -0,0 +1,25 @@ +package cn.lingniu.framework.plugin.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 可扩展多种公共属性配置,时区等问题 + */ +@ConfigurationProperties(prefix = FrameworkWebConfig.PRE_FIX) +@Data +public class FrameworkWebConfig { + + public static final String PRE_FIX = "framework.lingniu.web"; + + + /** + * 接口请求日志 + */ + private ApiLogProperties apiLog = new ApiLogProperties(); + + /** + * api接口加解密 + */ + private ApiEncryptProperties apiEncrypt = new ApiEncryptProperties(); +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/ApiEncrypt.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/ApiEncrypt.java new file mode 100644 index 0000000..04899e6 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/ApiEncrypt.java @@ -0,0 +1,27 @@ +package cn.lingniu.framework.plugin.web.encrypt; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * HTTP API 加解密注解 + */ +@Documented +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiEncrypt { + + /** + * 是否对请求参数进行解密,默认 true + */ + boolean request() default true; + + /** + * 是否对响应结果进行加密,默认 true + */ + boolean response() default true; + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiDecryptRequestWrapper.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiDecryptRequestWrapper.java new file mode 100644 index 0000000..1c0bd78 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiDecryptRequestWrapper.java @@ -0,0 +1,83 @@ +package cn.lingniu.framework.plugin.web.encrypt.filter; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.asymmetric.AsymmetricDecryptor; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.symmetric.SymmetricDecryptor; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * 解密请求 {@link HttpServletRequestWrapper} 实现类 + */ +public class ApiDecryptRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] body; + + public ApiDecryptRequestWrapper(HttpServletRequest request, + SymmetricDecryptor symmetricDecryptor, + AsymmetricDecryptor asymmetricDecryptor) throws IOException { + super(request); + // 读取 body,允许 HEX、BASE64 传输 + String requestBody = StrUtil.utf8Str( + IoUtil.readBytes(request.getInputStream(), false)); + + // 解密 body + body = symmetricDecryptor != null ? symmetricDecryptor.decrypt(requestBody) + : asymmetricDecryptor.decrypt(requestBody, KeyType.PrivateKey); + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public int getContentLength() { + return body.length; + } + + @Override + public long getContentLengthLong() { + return body.length; + } + + @Override + public ServletInputStream getInputStream() { + ByteArrayInputStream stream = new ByteArrayInputStream(body); + return new ServletInputStream() { + + @Override + public int read() { + return stream.read(); + } + + @Override + public int available() { + return body.length; + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + + }; + } +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptFilter.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptFilter.java new file mode 100644 index 0000000..013bd4e --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptFilter.java @@ -0,0 +1,155 @@ +package cn.lingniu.framework.plugin.web.encrypt.filter; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.AsymmetricDecryptor; +import cn.hutool.crypto.asymmetric.AsymmetricEncryptor; +import cn.hutool.crypto.symmetric.SymmetricDecryptor; +import cn.hutool.crypto.symmetric.SymmetricEncryptor; +import cn.lingniu.framework.plugin.core.base.CommonResult; +import cn.lingniu.framework.plugin.core.exception.util.ServiceExceptionUtil; +import cn.lingniu.framework.plugin.util.object.ObjectUtils; +import cn.lingniu.framework.plugin.util.servlet.ServletUtils; +import cn.lingniu.framework.plugin.web.config.ApiEncryptProperties; +import cn.lingniu.framework.plugin.web.encrypt.ApiEncrypt; +import cn.lingniu.framework.plugin.web.bae.handler.GlobalExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.ServletRequestPathUtils; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + + +/** + * API 加密过滤器,处理 {@link ApiEncrypt} 注解。 + * 1. 解密请求参数 + * 2. 加密响应结果 + * + * 疑问:为什么不使用 SpringMVC 的 RequestBodyAdvice 或 ResponseBodyAdvice 机制呢? + * 回答:考虑到项目中会记录访问日志、异常日志,以及 HTTP API 签名等场景,最好是全局级、且提前做解析!!! + */ +@Slf4j +public class ApiEncryptFilter extends OncePerRequestFilter { + + private final ApiEncryptProperties apiEncryptProperties; + + private final RequestMappingHandlerMapping requestMappingHandlerMapping; + + private final GlobalExceptionHandler globalExceptionHandler; + + private final SymmetricDecryptor requestSymmetricDecryptor; + private final AsymmetricDecryptor requestAsymmetricDecryptor; + + private final SymmetricEncryptor responseSymmetricEncryptor; + private final AsymmetricEncryptor responseAsymmetricEncryptor; + + public ApiEncryptFilter(ApiEncryptProperties apiEncryptProperties, + RequestMappingHandlerMapping requestMappingHandlerMapping, + GlobalExceptionHandler globalExceptionHandler) { + this.apiEncryptProperties = apiEncryptProperties; + this.requestMappingHandlerMapping = requestMappingHandlerMapping; + this.globalExceptionHandler = globalExceptionHandler; + if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "AES")) { + this.requestSymmetricDecryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getRequestKey())); + this.requestAsymmetricDecryptor = null; + this.responseSymmetricEncryptor = SecureUtil.aes(StrUtil.utf8Bytes(apiEncryptProperties.getResponseKey())); + this.responseAsymmetricEncryptor = null; + } else if (StrUtil.equalsIgnoreCase(apiEncryptProperties.getAlgorithm(), "RSA")) { + this.requestSymmetricDecryptor = null; + this.requestAsymmetricDecryptor = SecureUtil.rsa(apiEncryptProperties.getRequestKey(), null); + this.responseSymmetricEncryptor = null; + this.responseAsymmetricEncryptor = SecureUtil.rsa(null, apiEncryptProperties.getResponseKey()); + } else { + // 补充说明:如果要支持 SM2、SM4 等算法,可在此处增加对应实例的创建,并添加相应的 Maven 依赖即可。 + throw new IllegalArgumentException("不支持的加密算法:" + apiEncryptProperties.getAlgorithm()); + } + } + + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + // 获取 @ApiEncrypt 注解 + ApiEncrypt apiEncrypt = getApiEncrypt(request); + boolean requestEnable = apiEncrypt != null && apiEncrypt.request(); + boolean responseEnable = apiEncrypt != null && apiEncrypt.response(); + String encryptHeader = request.getHeader(apiEncryptProperties.getHeader()); + if (!requestEnable && !responseEnable && StrUtil.isBlank(encryptHeader)) { + chain.doFilter(request, response); + return; + } + + // 1. 解密请求 + if (ObjectUtils.equalsAny(HttpMethod.valueOf(request.getMethod()), + HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)) { + try { + if (StrUtil.isNotBlank(encryptHeader)) { + request = new ApiDecryptRequestWrapper(request, + requestSymmetricDecryptor, requestAsymmetricDecryptor); + } else if (requestEnable) { + throw ServiceExceptionUtil.invalidParamException("请求未包含加密标头,请检查是否正确配置了加密标头"); + } + } catch (Exception ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; + } + } + + // 2. 执行过滤器链 + if (responseEnable) { + // 特殊:仅包装,最后执行。目的:Response 内容可以被重复读取!!! + response = new ApiEncryptResponseWrapper(response); + } + chain.doFilter(request, response); + + // 3. 加密响应(真正执行) + if (responseEnable) { + ((ApiEncryptResponseWrapper) response).encrypt(apiEncryptProperties, + responseSymmetricEncryptor, responseAsymmetricEncryptor); + } + } + + /** + * 获取 @ApiEncrypt 注解 + * + * @param request 请求 + */ + @SuppressWarnings("PatternVariableCanBeUsed") + private ApiEncrypt getApiEncrypt(HttpServletRequest request) { + try { + // 特殊:兼容 SpringBoot 2.X 版本会报错的问题 https://t.zsxq.com/kqyiB + if (!ServletRequestPathUtils.hasParsedRequestPath(request)) { + ServletRequestPathUtils.parseAndCache(request); + } + + // 解析 @ApiEncrypt 注解 + HandlerExecutionChain mappingHandler = requestMappingHandlerMapping.getHandler(request); + if (mappingHandler == null) { + return null; + } + Object handler = mappingHandler.getHandler(); + if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + ApiEncrypt annotation = handlerMethod.getMethodAnnotation(ApiEncrypt.class); + if (annotation == null) { + annotation = handlerMethod.getBeanType().getAnnotation(ApiEncrypt.class); + } + return annotation; + } + } catch (Exception e) { + log.error("[getApiEncrypt][url({}/{}) 获取 @ApiEncrypt 注解失败]", + request.getRequestURI(), request.getMethod(), e); + } + return null; + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptResponseWrapper.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptResponseWrapper.java new file mode 100644 index 0000000..62eea00 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/encrypt/filter/ApiEncryptResponseWrapper.java @@ -0,0 +1,108 @@ +package cn.lingniu.framework.plugin.web.encrypt.filter; + +import cn.hutool.crypto.asymmetric.AsymmetricEncryptor; +import cn.hutool.crypto.asymmetric.KeyType; +import cn.hutool.crypto.symmetric.SymmetricEncryptor; +import cn.lingniu.framework.plugin.web.config.ApiEncryptProperties; + +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +/** + * 加密响应 {@link HttpServletResponseWrapper} 实现类 + * + */ +public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper { + + private final ByteArrayOutputStream byteArrayOutputStream; + private final ServletOutputStream servletOutputStream; + private final PrintWriter printWriter; + + public ApiEncryptResponseWrapper(HttpServletResponse response) { + super(response); + this.byteArrayOutputStream = new ByteArrayOutputStream(); + this.servletOutputStream = this.getOutputStream(); + this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream)); + } + + public void encrypt(ApiEncryptProperties properties, + SymmetricEncryptor symmetricEncryptor, + AsymmetricEncryptor asymmetricEncryptor) throws IOException { + // 1.1 清空 body + HttpServletResponse response = (HttpServletResponse) this.getResponse(); + response.resetBuffer(); + // 1.2 获取 body + this.flushBuffer(); + byte[] body = byteArrayOutputStream.toByteArray(); + // 2. 添加加密 header 标识 + this.addHeader(properties.getHeader(), "true"); + // 特殊:特殊:https://juejin.cn/post/6867327674675625992 + this.addHeader("Access-Control-Expose-Headers", properties.getHeader()); + + // 3.1 加密 body + String encryptedBody = symmetricEncryptor != null ? symmetricEncryptor.encryptBase64(body) + : asymmetricEncryptor.encryptBase64(body, KeyType.PublicKey); + // 3.2 输出加密后的 body:(设置 header 要放在 response 的 write 之前) + response.getWriter().write(encryptedBody); + } + + @Override + public PrintWriter getWriter() { + return printWriter; + } + + @Override + public void flushBuffer() throws IOException { + if (servletOutputStream != null) { + servletOutputStream.flush(); + } + if (printWriter != null) { + printWriter.flush(); + } + } + + @Override + public void reset() { + byteArrayOutputStream.reset(); + } + + @Override + public ServletOutputStream getOutputStream() { + return new ServletOutputStream() { + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + } + + @Override + public void write(int b) { + byteArrayOutputStream.write(b); + } + + @Override + @SuppressWarnings("NullableProblems") + public void write(byte[] b) throws IOException { + byteArrayOutputStream.write(b); + } + + @Override + @SuppressWarnings("NullableProblems") + public void write(byte[] b, int off, int len) { + byteArrayOutputStream.write(b, off, len); + } + + }; + } + +} \ No newline at end of file diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiEncryptAutoConfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiEncryptAutoConfiguration.java new file mode 100644 index 0000000..4047d74 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiEncryptAutoConfiguration.java @@ -0,0 +1,34 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.core.base.WebFilterOrderEnum; +import cn.lingniu.framework.plugin.web.config.ApiEncryptProperties; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import cn.lingniu.framework.plugin.web.encrypt.filter.ApiEncryptFilter; +import cn.lingniu.framework.plugin.web.bae.handler.GlobalExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + + +@AutoConfiguration +@Slf4j +@EnableConfigurationProperties(ApiEncryptProperties.class) +@ConditionalOnProperty(prefix = "framework.lingniu.web.apiEncrypt", name = "enable", havingValue = "true") +public class ApiEncryptAutoConfiguration { + + @Bean + public FilterRegistrationBean apiEncryptFilter(FrameworkWebConfig frameworkWebConfig, + RequestMappingHandlerMapping requestMappingHandlerMapping, + GlobalExceptionHandler globalExceptionHandler) { + ApiEncryptFilter filter = new ApiEncryptFilter(frameworkWebConfig.getApiEncrypt(), + requestMappingHandlerMapping, globalExceptionHandler); + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(WebFilterOrderEnum.API_ENCRYPT_FILTER); + return bean; + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiLogAutoConfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiLogAutoConfiguration.java new file mode 100644 index 0000000..b1af019 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ApiLogAutoConfiguration.java @@ -0,0 +1,44 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.core.base.WebFilterOrderEnum; +import cn.lingniu.framework.plugin.web.apilog.filter.ApiAccessLogFilter; +import cn.lingniu.framework.plugin.web.apilog.interceptor.ApiAccessLogInterceptor; +import cn.lingniu.framework.plugin.web.apilog.logger.api.ApiAccessLogCommonApi; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.servlet.Filter; + +@AutoConfiguration(after = WebAutoConfiguration.class) +public class ApiLogAutoConfiguration implements WebMvcConfigurer { + + /** + * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 + */ + @Bean + @ConditionalOnProperty(prefix = "framework.lingniu.web.apiLog", value = "enable", matchIfMissing = false) + public FilterRegistrationBean apiAccessLogFilter(FrameworkWebConfig frameworkWebConfig, + @Value("${spring.application.name}") String applicationName, + ApiAccessLogCommonApi apiAccessLogApi) { + ApiAccessLogFilter filter = new ApiAccessLogFilter(frameworkWebConfig, applicationName, apiAccessLogApi); + return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new ApiAccessLogInterceptor()); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/EnableGlobalResponseBody.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/EnableGlobalResponseBody.java new file mode 100644 index 0000000..1d026e3 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/EnableGlobalResponseBody.java @@ -0,0 +1,20 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.web.bae.handler.GlobalResponseBodyHandler; +import org.springframework.context.annotation.Import; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 待扩展 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Import({GlobalResponseBodyHandler.class}) +public @interface EnableGlobalResponseBody { +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/JacksonAutoConfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/JacksonAutoConfiguration.java new file mode 100644 index 0000000..c8f8e60 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/JacksonAutoConfiguration.java @@ -0,0 +1,77 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.util.json.JsonUtils; +import cn.lingniu.framework.plugin.util.json.databind.NumberSerializer; +import cn.lingniu.framework.plugin.util.json.databind.TimestampLocalDateTimeDeserializer; +import cn.lingniu.framework.plugin.util.json.databind.TimestampLocalDateTimeSerializer; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@AutoConfiguration(after = org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class) +@Slf4j +public class JacksonAutoConfiguration { + + /** + * 从 Builder 源头定制(关键:使用 *ByType,避免 handledType 要求) + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer ldtEpochMillisCustomizer() { + return builder -> builder + // Long -> Number + .serializerByType(Long.class, NumberSerializer.INSTANCE) + .serializerByType(Long.TYPE, NumberSerializer.INSTANCE) + // LocalDate / LocalTime + .serializerByType(LocalDate.class, LocalDateSerializer.INSTANCE) + .deserializerByType(LocalDate.class, LocalDateDeserializer.INSTANCE) + .serializerByType(LocalTime.class, LocalTimeSerializer.INSTANCE) + .deserializerByType(LocalTime.class, LocalTimeDeserializer.INSTANCE) + // LocalDateTime < - > EpochMillis + .serializerByType(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE) + .deserializerByType(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); + } + + /** + * 以 Bean 形式暴露 Module(Boot 会自动注册到所有 ObjectMapper) + */ + @Bean + public Module timestampSupportModuleBean() { + SimpleModule m = new SimpleModule("TimestampSupportModule"); + // Long -> Number,避免前端精度丢失 + m.addSerializer(Long.class, NumberSerializer.INSTANCE); + m.addSerializer(Long.TYPE, NumberSerializer.INSTANCE); + // LocalDate / LocalTime + m.addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE); + m.addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE); + m.addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE); + m.addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE); + // LocalDateTime < - > EpochMillis + m.addSerializer(LocalDateTime.class, TimestampLocalDateTimeSerializer.INSTANCE); + m.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); + return m; + } + + /** + * 初始化全局 JsonUtils,直接使用主 ObjectMapper + */ + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public JsonUtils jsonUtils(ObjectMapper objectMapper) { + JsonUtils.init(objectMapper); + log.debug("[init][初始化 JsonUtils 成功]"); + return new JsonUtils(); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseDetectAutoconfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseDetectAutoconfiguration.java new file mode 100644 index 0000000..c47b0a9 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseDetectAutoconfiguration.java @@ -0,0 +1,52 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.core.config.CommonConstant; +import cn.lingniu.framework.plugin.web.bae.interceptor.ResponseCheckInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + + +@Slf4j +@Configuration +@ConditionalOnMissingClass({"org.springframework.security.authentication.AuthenticationManager"}) +public class ResponseDetectAutoconfiguration implements CommandLineRunner, WebMvcConfigurer { + + @Autowired + ResponseCheckInterceptor responseCheckInterceptor; + + /** + * 默认排除URL清单 + */ + public static final Set excludesPattern= new HashSet<>( + Arrays.asList("*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*,/doc.html,/webjars/**,/swagger-ui.html/**,/swagger-resources/**,/v2/api-docs,/actuator/**,,/favicon.ico" + .split("\\s*,\\s*"))); + + @Override + public void addInterceptors(InterceptorRegistry registry) { + List urls = new ArrayList<>(); + urls.addAll(excludesPattern.stream().collect(Collectors.toList())); + registry.addInterceptor(responseCheckInterceptor) + .addPathPatterns( Collections.singletonList("/**") ) + .excludePathPatterns(urls); + } + + @Override + public void run(String... args) { + if(log.isInfoEnabled()) { + log.info("\r\n\t\tDEV/SIT/UAT/TEST 环境已开启返回响应体规范检测功能,请按lingniu-web框架规范使用响应体结构~~"); + } + } +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseInterceptorAutoconfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseInterceptorAutoconfiguration.java new file mode 100644 index 0000000..bbb8e42 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/ResponseInterceptorAutoconfiguration.java @@ -0,0 +1,19 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.web.bae.interceptor.ResponseCheckInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Slf4j +@Configuration +@ConditionalOnMissingClass({"org.springframework.security.authentication.AuthenticationManager"}) +public class ResponseInterceptorAutoconfiguration { + @Bean + public ResponseCheckInterceptor responseInterceptor(){ + return new ResponseCheckInterceptor(); + } + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/WebAutoConfiguration.java b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/WebAutoConfiguration.java new file mode 100644 index 0000000..6d3742b --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/java/cn/lingniu/framework/plugin/web/init/WebAutoConfiguration.java @@ -0,0 +1,74 @@ +package cn.lingniu.framework.plugin.web.init; + +import cn.lingniu.framework.plugin.core.base.WebFilterOrderEnum; +import cn.lingniu.framework.plugin.web.ApplicationStartEventListener; +import cn.lingniu.framework.plugin.web.config.FrameworkWebConfig; +import cn.lingniu.framework.plugin.web.bae.filter.CacheRequestBodyFilter; +import cn.lingniu.framework.plugin.web.bae.handler.GlobalExceptionHandler; +import cn.lingniu.framework.plugin.web.bae.util.WebFrameworkUtils; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.annotation.Order; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import javax.servlet.Filter; + +@AutoConfiguration +@EnableConfigurationProperties(FrameworkWebConfig.class) +@Import({GlobalExceptionHandler.class, ApplicationStartEventListener.class}) +public class WebAutoConfiguration { + + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public WebFrameworkUtils webFrameworkUtils(FrameworkWebConfig frameworkWebConfig) { + // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean + return new WebFrameworkUtils(frameworkWebConfig); + } + + // ========== Filter 相关 ========== + + /** + * 创建 CorsFilter Bean,解决跨域问题 + */ + @Bean + @Order(value = WebFilterOrderEnum.CORS_FILTER) // 特殊:修复因执行顺序影响到跨域配置不生效问题 + public FilterRegistrationBean corsFilterBean() { + // 创建 CorsConfiguration 对象 + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); // 设置访问源地址 + config.addAllowedHeader("*"); // 设置访问源请求头 + config.addAllowedMethod("*"); // 设置访问源请求方法 + // 创建 UrlBasedCorsConfigurationSource 对象 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 + return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER); + } + + /** + * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 + */ + @Bean + public FilterRegistrationBean requestBodyCacheFilter() { + return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER); + } + + + public static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + + + +} diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/META-INF/spring.factories b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..b95b522 --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +cn.lingniu.framework.plugin.web.init.JacksonAutoConfiguration,\ +cn.lingniu.framework.plugin.web.init.WebAutoConfiguration,\ +cn.lingniu.framework.plugin.web.init.ResponseInterceptorAutoconfiguration,\ +cn.lingniu.framework.plugin.web.init.ResponseDetectAutoconfiguration,\ +cn.lingniu.framework.plugin.web.init.ApiLogAutoConfiguration,\ +cn.lingniu.framework.plugin.web.init.ApiEncryptAutoConfiguration + diff --git a/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/web.md b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/web.md new file mode 100644 index 0000000..000726a --- /dev/null +++ b/lingniu-framework-plugin/web/lingniu-framework-plugin-web/src/main/resources/web.md @@ -0,0 +1,57 @@ +# 框架核心web应用模块 + + +## 概述 (Overview) + +1. 定位: 基于 springmvc封装的规范web组件 +2. 核心能力: + * 统一返回结果检测:非生产环境接口返回参数不规范,控制台会打印error日志。 + * 请求日志:支持注解方式规范打印请求日志,可扩展调用db。 + * 参数加解密:可解密请求参数,加密响应结果。 + * 全局异常处理机制:GlobalExceptionHandler + * 统一结果返回处理:GlobalResponseBodyHandler +3. 适用场景: + * Web应用公共功能 + + +## 统一返回结果检测 +- 检测条件:profile:test","dev","uat","sit +- 检测实现类:ResponseCheckInterceptor +- 加载类: + ResponseDetectAutoconfiguration + ResponseInterceptorAutoconfiguration + +## 请求日志 +- 使用方式:@ApiAccessLog +- 参数配置请查看:FrameworkWebConfig.ApiLogProperties +- filter:ApiAccessLogFilter +- 最终输出打印:ApiAccessLogServiceImpl + + +## 统一返回结果处理:CommonResult +- 实现参考:GlobalResponseBodyHandler + +## 统一异常处理:GlobalExceptionHandler,并记录日志-ApiErrorLogServiceImpl + +## api 加解密 +- 使用方式:@ApiEncrypt +- 参数配置请查看:FrameworkWebConfig.ApiEncryptProperties +- filter:ApiEncryptFilter +- 逻辑处理:ApiDecryptRequestWrapper、ApiEncryptResponseWrapper + +## 如何配置 + +```yaml +framework: + lingniu: + web: + apiLog: + #开关 + enable: true + apiEncrypt: + #开关 + enable: false + algorithm: "AES" + requestKey: "xxx" + responseKey: "xxx" +``` diff --git a/lingniu-framework-starters/lingniu-framework-starters-nacos/pom.xml b/lingniu-framework-starters/lingniu-framework-starters-nacos/pom.xml new file mode 100644 index 0000000..87aa422 --- /dev/null +++ b/lingniu-framework-starters/lingniu-framework-starters-nacos/pom.xml @@ -0,0 +1,32 @@ + + + + cn.lingniu.framework + lingniu-framework-starters-web + 1.0.0-SNAPSHOT + ../lingniu-framework-starters-web + + 4.0.0 + + lingniu-framework-starters-nacos + pom + + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-common + + + cn.lingniu.framework + lingniu-framework-plugin-microservice-nacos + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + + + \ No newline at end of file diff --git a/lingniu-framework-starters/lingniu-framework-starters-web/pom.xml b/lingniu-framework-starters/lingniu-framework-starters-web/pom.xml new file mode 100644 index 0000000..b088381 --- /dev/null +++ b/lingniu-framework-starters/lingniu-framework-starters-web/pom.xml @@ -0,0 +1,62 @@ + + + + cn.lingniu.framework + lingniu-framework-starters + 1.0.0-SNAPSHOT + ../ + + 4.0.0 + + lingniu-framework-starters-web + pom + + + + javax.validation + validation-api + + + + org.hibernate.validator + hibernate-validator + compile + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + cn.lingniu.framework + lingniu-framework-plugin-web + + + + org.yaml + snakeyaml + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + \ No newline at end of file diff --git a/lingniu-framework-starters/pom.xml b/lingniu-framework-starters/pom.xml new file mode 100644 index 0000000..cc6bb03 --- /dev/null +++ b/lingniu-framework-starters/pom.xml @@ -0,0 +1,20 @@ + + + + cn.lingniu.framework + lingniu-framework-dependencies + 1.0.0-SNAPSHOT + ../lingniu-framework-dependencies + + + 4.0.0 + pom + lingniu-framework-starters + + + + lingniu-framework-starters-web + lingniu-framework-starters-nacos + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6e439d9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + cn.lingniu.framework + lingniu-framework-parent + 1.0.0-SNAPSHOT + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + lingniu-framework-dependencies + lingniu-framework-plugin/lingniu-framework-plugin-apollo + lingniu-framework-plugin/lingniu-framework-plugin-util + lingniu-framework-plugin/file/lingniu-framework-plugin-easyexcel + lingniu-framework-plugin/lingniu-framework-plugin-core + lingniu-framework-plugin/job/lingniu-framework-plugin-xxljob + lingniu-framework-plugin/db/lingniu-framework-plugin-mybatis + lingniu-framework-plugin/cache/lingniu-framework-plugin-redisson + lingniu-framework-plugin/cache/lingniu-framework-plugin-jetcache + lingniu-framework-plugin/mq/lingniu-framework-plugin-rocketmq + lingniu-framework-plugin/monitor/lingniu-framework-plugin-skywalking + lingniu-framework-plugin/monitor/lingniu-framework-plugin-prometheus + lingniu-framework-plugin/web/lingniu-framework-plugin-web + lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-common + lingniu-framework-plugin/microservice/lingniu-framework-plugin-microservice-nacos + + + pom + lingniu-framework-parent + http://maven.apache.org + + + + + + releases + http://nexus.xx.net.cn:8081/repository/releases/ + + + snapshots + http://nexus.xx.net.cn:8081/repository/snapshots/ + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + + maven-source-plugin + + true + + + + compile + + jar + + + + + + + \ No newline at end of file