🍀 学习不使用数据库外键的情况下保证有关联的表之间的数据的一致性
一、AOP 动态代理切入方法
(1) Aspect Oriented Programming
🎄 AOP(Aspect Oriented Programming)面向切面编程
🎄 Spring 使用 AOP 技术封装了动态代理功能
🎄 它依赖 AspectJ 库
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.6</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.6</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
(2) 切入点表达式
🍀 ① 任意公共方法:
execution(public * *(..))
🍀 ② 方法名以 set 开头的全部方法:
execution(* set*(..))
🍀 ③ UserService 接口定义的全部方法:
execution(* com.guoqing.service.UserService.*(..))
🍀 ④ service 包下定义的全部方法(不包括子包):
execution(* com.guoqing.service.*.*(..))
🍀 ⑤ service 包下定义的全部方法(包括子包):
execution(* com.guoqing.service..*.*(..))
🍀 ⑥ 包含两个 String 类型参数的全部方法:
execution(* *(String, String))
🍀 ⑦ 只有一个 Serializable 参数的全部方法:
args(java.io.Serializable)
🍀 ⑧ service 包中的全部方法:
within(com.guoqing.service.*)
🍀 ⑨ service 包中的全部方法(包括子包):
within(com.guoqing.service..*)
@Slf4j @Aspect @Component public class GForeignAspect { @Around("execution(* com.guoqing.service..*.remove*(..))") public Object handleRemove(ProceedingJoinPoint point) throws Throwable { Object target = point.getTarget(); if (!(target instanceof IService)) return point.proceed(); IService<?> service = (IService<?>) target; // MP 泛型的类型(genericType)是 PO, 是要被删除的【表】 Class<?> genericType = service.getEntityClass(); // eg: class com.guoqing.po.WechatUser // 知道要被删除的类型(PO)后, 就可以知道该 PO 被哪些表引用着 log.info("【庆】{}", genericType.toString()); return point.proceed(); } }
二、SpringBoot 项目扫描类
🍀 ① 实现(implements)ApplicationContextAware 接口,并实现它的 void setApplicationContext(ApplicationContext)
方法
🎄 这是 Spring Bean 的生命周期方法
🎄 通过该方法可以获取 Spring 的 IoC 容器,进而获取到某个类的实例(通过它的
getBean()
方法)
@Component public class TestLifecycle implements ApplicationContextAware { private ApplicationContext iocContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.iocContext = applicationContext; UserController userController = (UserController) iocContext.getBean("Users"); } }
🍀 ② 实现(implements) InitializingBean 接口,并实现它的 void afterPropertiesSet()
方法
🎄 这是 Spring Bean 的生命周期方法
🎄 当实现该接口的类的属性被设置(注入)完毕后,
afterPropertiesSet()
方法被调用
@Component public class GForeignAspect implements ApplicationContextAware, InitializingBean { private ApplicationContext iocContext; // 属性被设置(注入)完毕时,InitializingBean 接口的 afterPropertiesSet 被调用 @Autowired private ResourceLoader resourceLoader; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.iocContext = applicationContext; } @Override public void afterPropertiesSet() throws Exception { } }
(1) ResourceLoader 扫描类
🍀 Spring 框架的
org.springframework.core.io.ResourceLoader
类
@Slf4j @Aspect @Component public class GForeignAspect implements InitializingBean { // 要被扫描的类的 class 文件 private static final String SCAN_CLASS = "classpath*:com/guoqing/po/**/*.class"; @Autowired private ResourceLoader resourceLoader; /** * 扫描全部的 PO 类 */ @Override public void afterPropertiesSet() throws Exception { // 通过资源加载器拿到资源匹配解析器 ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); // 通过资源加载器拿到元数据读取工厂类 CachingMetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader); // 通过资源匹配解析器读取到类的 class 资源 Resource[] resources = resolver.getResources(SCAN_CLASS); if (resources.length == 0) { log.info("没有可供扫描的 PO"); throw new Exception("没有可供扫描的 PO"); } // 遍历 class 资源, 通过元数据解析器读取 class 资源的信息(如 类名) for (Resource resource : resources) { // 通过元数据读取工厂获取到元数据读取器 MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource); // 通过元数据读取器拿到 class 元数据读取器 ClassMetadata classMetadata = metadataReader.getClassMetadata(); // 通过元数据读取器拿到 class 的类名 String className = classMetadata.getClassName(); /* output: className = com.guoqing.po.People className = com.guoqing.po.WechatUser */ System.out.println("className = " + className); } } }
🎄 上面的一段代码都是比较固定的,直接复制使用即可
🎄 通过元数据读取器还可调用下面的方法获取其他 class 信息
🎄 上面的代码中,获取到了 class(或类)的完整路径。可通过 Java 的反射机制创建相应类的实例
(2) Map 的 computeIfAbsent 方法
- compute: 计算
- absent: 不存在
先看懂下面的代码,然后用 computeIfAbsent 改写下面的代码:
public class TestTest { @Test public void testComputeIfAbsent() { HashMap<String, Set<String>> hashMap = new HashMap<>(); Set<String> set = new HashSet<>(); set.add("杨天荣"); hashMap.put("boy", set); // 判断 Map 中是否存在 key 是【boy】的键值对? if (hashMap.containsKey("boy")) { // 如果有, 获取该 key(boy)对应的 value(Set 类型) // 并往其中添加值【庆医】 hashMap.get("boy").add("庆医"); } else { // 如果没有, 新创建一个 Set, 把值【庆医】添加其中 // 然后把新创建的这个 Set 添加到 Map 中 Set<String> tmpSet = new HashSet<>(); tmpSet.add("庆医"); hashMap.put("boy", tmpSet); } // output: {boy=[杨天荣, 庆医]} System.out.println(hashMap.toString()); } }
✏️ 上面代码的逻辑很容易,但是写起来比较麻烦
✏️ Map 提供了
computeIfAbsent
方法,用以实现类型上面的逻辑功能,且写法很优美 🍀
public class TestTest { @Test public void testComputeIfAbsent() { HashMap<String, Set<String>> hashMap = new HashMap<>(); HashSet<String> set = new HashSet<>(); set.add("杨天荣"); hashMap.put("boy", set); // hashMap.computeIfAbsent("boy", this::getSetInstance).add("庆医"); hashMap.computeIfAbsent("boy", key -> getSetInstance(key)).add("庆医"); // output: {boy=[杨天荣, 庆医]} System.out.println(hashMap.toString()); } private Set<String> getSetInstance(String key) { return new HashSet<>(); } }
🔋 computeIfAbsent: 如果存在映射的值,则返回该值,否则返回计算值
(3) 反射几个常用 api
① 创建一个测试注解
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyTestAnnotation { String function() default "该类作用显而易见, 不予解释"; }
✏️ 当和反射结合的时候,注解的作用非常强大
✏️ 注解没有和反射结合的时候,注解毫无作用
✏️ 上面的代码是一个简单的注解案例,如需了解更多 💯 关于注解: https://zgqwillbeverylucky.blog.csdn.net/article/details/127098256
② 创建测试 PO 类
@Data public class BasePo { private Integer id; private String name; private String createTime; }
@EqualsAndHashCode(callSuper = true) @MyTestAnnotation(function = "人类, 用于构建人") @Data public class People extends BasePo { private String phone; private String job; }
③ 反射 api 获取指定类的指定注解信息
📝 已知 People 类的完整类名是:
com.guoqing.po.People
📝 已知 People 类有
@MyTestAnnotation
注解📝 获取出 People 类的
@MyTestAnnotation
注解信息
public class TestReflection { private static final String CLASS_FULL_NAME = "com.guoqing.po.People"; @Test public void test() throws ClassNotFoundException { // 使用 Class 类的 forName 静态方法, 传入完整类名, 创建该类的【类对象】 Class<?> clsObj = Class.forName(CLASS_FULL_NAME); // 通过【类对象】的 getAnnotation 方法获取注解信息 MyTestAnnotation annotation = clsObj.getAnnotation(MyTestAnnotation.class); // annotation = @com.guoqing.common.aop.foreign.annotation.MyTestAnnotation(function=人类, 用于构建人) System.out.println("annotation = " + annotation); // annotation.function = 人类, 用于构建人 System.out.println("annotation.function = " + annotation.function()); } }
(4) 返回第一个不为空的字符串
public class GCommonUtil { /** * 判断一个字符串是否为 null 或空串 */ public static boolean isEmptyStr(String string) { return string == null || "".equals(string); } /** * 传入多个字符串 (可以是可变参数, 或数组), 返回第一个不为空的字符串 */ public static String firstNotEmptyStr(String... strings) { if (strings == null || strings.length < 1) return null; if (strings.length == 1) return strings[0]; for (String string : strings) { if (!isEmptyStr(string)) { return string; } } return null; } }
(5) 判断一个【字符】是大写字母还是小写字母
public class GCommonUtil { /** * 当一个字符是【大】写英文字母的时候返回 true */ public static boolean isBigLetter(char source) { return source >= 'A' && source <= 'Z'; } /** * 当一个字符是【小】写英文字母的时候返回 true */ public static boolean isSmallLetter(char source) { return source >= 'a' && source <= 'z'; } }
(6) 让英文单词的首字母变小写
大写英文字符(如 A、E、F、B)加上数字 32 后变为小写英文字符
public class GCommonUtil { // 大写英文字符加上数字 32 后变为小写英文字符 private static final int DELTA = 'a' - 'A'; // 32 /** * 传入一个字符串, 若它的首字母是小写, 通过它的首字母创建 StringBuilder 对象返回 * 若它的首字母是大写, 把首字母转换为小写后通过首字母创建 StringBuilder 对象返回 */ public static StringBuilder firstLetterLowerStringBuilder(String source) { StringBuilder sb = new StringBuilder(); if (isEmptyStr(source)) return sb; // 取出字符串的首字母 char firstLetter = source.charAt(0); if (isBigLetter(firstLetter)) { // 如果首字母是大写的 sb.append((char) (firstLetter + DELTA)); } else { sb.append(firstLetter); } return sb; } /** * 返回英文字符串的首字母小写形式 * 【 BOY-> bOY】 */ public static String firstLetterLowercase(String source) { if (isEmptyStr(source)) return source; StringBuilder sb = firstLetterLowerStringBuilder(source); int length = source.length(); for (int i = 1; i < length; i++) { sb.append(source.charAt(i)); } return sb.toString(); } }
(7) 驼峰转下划线形式
/** * 驼峰转下划线形式 * LoveYou -> love_you */ public static String camel2underline(String source) { if (isEmptyStr(source)) return null; StringBuilder sb = firstLetterLowerStringBuilder(source); int len = source.length(); for (int i = 1; i < len; i++) { char curChar = source.charAt(i); if (isBigLetter(curChar)) { sb.append("_"); sb.append((char) (curChar + DELTA)); } else { sb.append(curChar); } } return sb.toString(); }
(8) GForeignTableInfo(外键表信息)
✏️ 该类用以描述类和表的信息
✏️ 该类的内容很多,该节先介绍一部分(后面慢慢补充)
🍀① cacheClassTableMap 该属性缓存某个表的 class 对象和外键表(GForeignTableInfo)信息。它是 static 属性(常量
static final
),所以它的 JVM 中只占用独一无二的一份内存🍀 ② clsObj 该属性记录类对象(class)信息。它作为 key,和对应的 GForeignTableInfo 相映射
🍀③ tableName 该属性记录了该外键表信息对应的表的名字
@Getter @Setter public class GForeignTableInfo { // 缓存【类】和【表】的映射关系 private static final Map<Class<?>, GForeignTableInfo> CACHE_CLASS_TABLE_MAP = new HashMap<>(); private Class<?> clsObj; private String tableName; public static GForeignTableInfo getInfo(Class<?> clsObj, boolean newIfAbsent) { if (!newIfAbsent) return CACHE_CLASS_TABLE_MAP.get(clsObj); return CACHE_CLASS_TABLE_MAP.computeIfAbsent(clsObj, k -> { GForeignTableInfo foreignTableInfo = new GForeignTableInfo(); foreignTableInfo.setClsObj(clsObj); // 设置表名 String tableName; // 获取 clsObj 的 @GForeignTable 注解信息 GForeignTable annotation = clsObj.getAnnotation(GForeignTable.class); if (annotation != null) { // 获取 @GForeignTable 注解中 name 或 value 注解信息 // name 或 value 注解信息都可以表示表名 tableName = GCommonUtil.firstNotEmptyStr(annotation.name(), annotation.value()); } else { String clsSimpleName = clsObj.getSimpleName(); tableName = GCommonUtil.firstLetterLowercase(clsSimpleName); } foreignTableInfo.setTableName(tableName); return foreignTableInfo; }); } }