前言
如果说设计模式、设计思想,是属于java的“内功”锻炼的范畴。那么本文档则主要是以解决实际问题的角度出发,进行“外功”的锻炼。
外功1-masterworker多人同时干一个活
练功场景
在日常的工作中,经常会遇到需要处理大批量数据的情况,这些操作往往比较耗时。例如,导入大量用户信息、批量生成报告、同步海量数据等。这类操作如果仅由单个线程完成,可能会耗费大量时间,影响系统性能甚至导致超时。为了解决这个问题,我们可以采用Master-Worker模式,将任务分解为多个子任务,并通过多线程并发处理来加快任务执行速度。
这种方式不仅能够提高系统的处理效率,还可以避免由于单线程操作导致的性能瓶颈,是处理大批量任务的常见策略。
练功代码
下面我们以“批量导入Excel用户信息”为例,展示如何使用Master-Worker模式来处理大批量数据。
-
任务类
任务类封装了需要执行的具体任务。在这个例子中,每个任务代表一批需要导入的用户信息。
@Data @Builder public class UserImportTask { /** * 任务名称 */ private String taskName; /** * 要导入的用户信息 */ private List<UserEntity> userInfos; /** * 执行任务 */ public void execute() { try { // 打印worker的名字 String workerName = Thread.currentThread().getName(); System.out.println(workerName + "开始处理任务:" + this.getTaskName()); // 具体的导入逻辑.随机休眠1秒以内的时间 int workTime = RandomUtil.randomInt(1000); System.out.println(workerName + "正在导入用户信息,耗时:" + workTime + "ms"); Thread.sleep(workTime); System.out.println(workerName + "处理完成任务:" + this.getTaskName()); } catch (Exception e) { System.out.println("记录错误日志"); } } } -
worker类
Worker类负责从任务队列中取出任务并执行。每个Worker都是一个线程,负责处理具体的任务。
@Setter public class UserImportWorker implements Runnable { /** * 任务队列 */ private ConcurrentLinkedQueue<UserImportTask> taskQueue; @Override public void run() { while(true) { UserImportTask task = this.taskQueue.poll(); if(task == null) { break; } //实际的业务处理 task.execute(); } } } -
master类
Master类负责初始化任务并将其分发给多个Worker进行处理。它控制着任务的整体流程。
public class UserImportMaster { /** * 任务队列。由于多线程,所以这里用线程安全的无界队列 */ private final ConcurrentLinkedQueue<UserImportTask> taskQueue = new ConcurrentLinkedQueue<>(); private final List <Thread> workers = new ArrayList<>(); public UserImportMaster(int workerCount) { for(int i=0; i<workerCount; i++) { UserImportWorker worker = new UserImportWorker(); worker.setTaskQueue(taskQueue); Thread workerThread = new Thread(worker); workers.add(workerThread); } } /** * 提交任务 * @param tasks 任务列表 */ public void submitTasks(List<UserImportTask> tasks) { taskQueue.addAll(tasks); } /** * 执行所有任务 */ public void execute() { for(Thread worker : workers) { worker.start(); } for (Thread worker : workers) { try { worker.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } -
测试类
public static void main(String[] args) { // 模拟从excel读取到的数据 List<UserEntity> userInfos = getUserInfos(); // 对userInfos 进行分批处理,每批处理500个用户信息 List<List<UserEntity>> userInfoBatches = splitList(userInfos, 500); // 创建任务列表. 任务名称为task + 从1开始的序列号 AtomicInteger counter = new AtomicInteger(1); List<UserImportTask> tasks = userInfoBatches.stream() .map(batch -> UserImportTask.builder().taskName("task" + counter.getAndIncrement()).userInfos(batch).build()) .collect(Collectors.toList()); // 创建Master,并开始执行 UserImportMaster master = new UserImportMaster(3); master.submitTasks(tasks); master.execute(); }
练功心得
- 合理分配任务:在使用Master-Worker模式时,需要根据任务的复杂度和数据量合理分配任务的粒度。粒度过细会增加线程管理的开销,粒度过大则可能无法充分利用多线程的优势。
- 线程管理:在实际应用中,需要特别注意线程的管理和资源的释放。尤其是在处理大批量数据时,应该避免线程泄漏或内存泄漏的问题。
- 异常处理:在任务执行过程中,可能会遇到各种异常情况。需要在设计Worker时考虑到这些异常,并进行适当的处理,比如重试机制或记录失败日志。
- 性能调优:可以通过调整Worker的数量、任务的大小等参数来调优系统性能,找到最适合的并发策略。
- 负载均衡:在一些情况下,可以考虑使用更高级的负载均衡策略,确保每个Worker的任务量大致均衡,以避免某些Worker过载。
外功2-CompletableFuture每人干一个活
练功场景
在处理耗时任务时,异步编程能够提升应用的响应速度。CompletableFuture是Java 8引入的一个功能强大的类,支持链式调用和回调处理,使得异步编程变得更加容易。
练功代码
这里模拟实现,一个交易表中按照交易日期+参考号进行拼接字符串,从数据库中查询数据。
-
实体类
@Data @AllArgsConstructor public class TranInfoEntity { /** * 交易唯一索引 */ private Long id; /** * 交易日期 */ private String tranDate; /** * 参考号 */ private String referNo; /** * 交易金额 */ private Long tranAmount; } -
应用类
public static void main(String[] args) { // 模拟查询的条件 List<String> searchList = Arrays.asList("20211007000000000001", "20211007000000000002", "20211007000000000004"); // 模拟数据库中表的数据 List<TranInfoEntity> tranInfos = new ArrayList<>(); tranInfos.add(new TranInfoEntity(1L, "20211007", "000000000001", 111L)); tranInfos.add(new TranInfoEntity(2L, "20211007", "000000000002", 112L)); tranInfos.add(new TranInfoEntity(3L, "20211007", "000000000003", 113L)); // 并行发出所有的交易查询 List<CompletableFuture<TranInfoEntity>> futures = searchList.stream() .map(search -> CompletableFuture.supplyAsync(() -> searchTranInfo(search, tranInfos))) .collect(Collectors.toList()); // 等待所有查询完成,并获取查询结果 List<TranInfoEntity> results = futures.stream() .map(CompletableFuture::join) .filter(Objects::nonNull) .collect(Collectors.toList()); // 输出查询结果 System.out.println(results); } /** * 根据交易日期+参考号查询交易信息 * @param search 拼接后的查询字符串 * @param tranInfos 数据库中的数据 * @return 查询到的交易信息 */ public static TranInfoEntity searchTranInfo(String search, List<TranInfoEntity> tranInfos) { return tranInfos.stream() .filter(tranInfo -> StrUtil.equals(tranInfo.getTranDate()+tranInfo.getReferNo(), search)) .findFirst() .orElse(null); }
练功心得
CompletableFuture在处理可以并行处理的多个事情的时候比较合适。但是这个类中的方法很多,这里总结下记忆规则:
- 以Async结尾的方法,都是异步方法,对应的没有Async则是同步方法,一般都是一个异步方法对应一个同步方法。
- 以Async后缀结尾的方法,都有两个重载的方法,一个是使用内容的forkjoin线程池,一种是使用自定义线程池
- 以run开头的方法,其入口参数一定是无参的,并且没有返回值,类似于执行Runnable方法。
- 以supply开头的方法,入口也是没有参数的,但是有返回值
- 以Accept开头或者结尾的方法,入口参数是有参数,但是没有返回值
- 以Apply开头或者结尾的方法,入口有参数,有返回值
- 带有either后缀的方法,表示谁先完成就消费谁
外功3-Optional优雅处理Null值
练功场景
Optional 类的设计初衷是为了减少代码中的空指针异常(NullPointerException)。它提供一种优雅的方式来处理可能为空的对象,使得代码更加健壮和易于维护。
练功代码
/**
* 创建Optional对象
*/
@Test
public void createOptional() {
Optional<String> hello = Optional.of("hello");
System.out.println(hello);
// of()方法创建的Optional对象,如果传入的参数为null,则会抛出NullPointerException异常
try {
Optional<String> helloNull = Optional.of(null);
}catch (NullPointerException e) {
System.out.println("这里会抛出空指针异常: " + e.getMessage());
}
// ofNullable()方法创建的Optional对象,如果传入的参数为null,
// 则创建的Optional对象为空.不会抛出异常
Optional<String> helloNull = Optional.ofNullable(null);
System.out.println(helloNull);
// 创建空的Optional对象
Optional<String> empty = Optional.empty();
System.out.println(empty);
}
/**
* 获取Optional对象中的值
*/
@Test
public void getOptional() {
// get()方法获取Optional对象中的值,如果Optional对象为空,则会抛出NoSuchElementException异常
Optional<String> hello = Optional.of("hello");
System.out.println(hello.get());
Optional<String> empty = Optional.empty();
try {
empty.get();
}catch (NoSuchElementException e) {
System.out.println("这里会抛出NoSuchElementException异常: " + e.getMessage());
}
// 使用orElse()方法获取Optional对象中的值,如果Optional对象为空,则返回指定的值
Optional<UserInfo> userInfo = Optional.ofNullable(null);
System.out.println(userInfo.orElse(new UserInfo(2L, "李四", "女", 19)));
// 使用orElseGet()方法获取Optional对象中的值,如果Optional对象为空,则返回提供的Supplier接口实现类的返回值
System.out.println(userInfo.orElseGet(() -> new UserInfo(3L, "王五", "男", 20)));
// 使用orElseThrow()方法获取Optional对象中的值,如果Optional对象为空,则抛出指定的异常
try {
userInfo.orElseThrow(() -> new RuntimeException("Optional对象为空"));
}catch (RuntimeException e) {
System.out.println("这里会抛出RuntimeException异常: " + e.getMessage());
}
}
/**
* 检查Optional的值
*/
@Test
public void checkOptional() {
// isPresent()方法检查Optional对象是否为空,如果为空,则返回false,否则返回true
Optional<UserInfo> userInfo = Optional.ofNullable(new UserInfo(1L, "张三", "男", 18));
if (userInfo.isPresent()) {
System.out.println("isPresent-->" + userInfo.get());
}else {
System.out.println("Optional对象为空");
}
// ifPresent()方法检查Optional对象是否为空,如果为空,则不执行指定的操作,否则执行指定的操作
userInfo.ifPresent(user -> System.out.println("ifPresent-->" + user));
}
/**
* 处理Optional的值
*/
@Test
public void handleOptional() {
Optional<UserInfo> userInfo = Optional.ofNullable(new UserInfo(1L, "张三", "男", 18));
// map()方法,如果 Optional 对象包含值,应用Function返回一个新的 Optional 对象。
Optional<String> userName = userInfo.map(UserInfo::getName);
System.out.println(userName);
// flatMap()方法,如果 Optional 对象包含值,应用Lamda表达式返回一个新的 Optional 对象。
Optional<String> userName2 = userInfo.flatMap(user -> Optional.of(user.getName()));
System.out.println(userName2);
// filter()方法,如果 Optional 对象包含值,应用Predicate接口返回一个新的 Optional 对象。
Optional<UserInfo> userInfo2 = userInfo.filter(user -> user.getAge() > 17);
System.out.println(userInfo2);
}
练功心得
-
不要在类的属性中使用Optional
public class User { // 不推荐 private Optional<String> name; } -
避免在集合的元素用Optional
在集合中使用
Optional会导致不必要的嵌套。List<Optional<String>> names = Arrays.asList(Optional.of("Alice"), Optional.empty()); -
方法返回Optional,而不是返回null
当方法的返回值可能为空时,使用
Optional可以更清晰地表达这一点,避免空指针异常。public static <T, V> Optional<V> convertOptional(T source, Class<V> target) { if (ObjectUtil.hasNull(source, target)) { return Optional.empty(); } V result = CONVERTER.convert(source, target); return Optional.ofNullable(result); } -
尽量不用Optional的get方法
Optional.get方法会在值不存在时抛出异常,应该尽量避免直接使用,而是使用其他更安全的方法。如:String value = optional.orElse("Default Value");当然可以直接抛出一个自己可控的异常,供全局异常捕获处理,如:
UserEntity userEntity = MapstructUtil.convertOptional(request, UserEntity.class) .orElseThrow(() -> new RRException("转化请求对象到实体对象时有问题!")); -
尽量避免Optional作为参数
虽然
Optional可以作为方法参数,但一般不推荐这样做,因为这会增加方法的复杂性。如:public void processUser(Optional<User> user) { // 处理用户的逻辑 }
外功4-枚举策略模式
练功场景
策略模式的一个简易版。解决了在代码中使用if-else或switch-case来判断并执行对应的逻辑。如果使用if-else或switch-case方式,代码的可读性和可维护性会大大降低。但是这种枚举策略模式,最大的缺点的是虽然符合“开放-关闭原则”,但是并不能完美做到。但是对于场景固定的情况,还是蛮方便的。
练功代码
-
枚举类
@Getter @AllArgsConstructor public enum NotifyStrategy { /** * 邮件通知 */ EMAIL(msg ->{ System.out.println(StrUtil.format("通过Email进行通知:{}", msg)); }), /** * 短信通知 */ SMS(msg -> { System.out.println(StrUtil.format("通过短信进行通知:{}", msg)); }), /** * 微信通知 */ WECHAT(msg -> { System.out.println(StrUtil.format("通过微信进行通知:{}", msg)); }); /** * 通知器 */ private final Notifier notifier; } -
接口类
public interface Notifier { /** * 通知 * * @param msg 消息 */ void sendMessage(String msg); } -
服务类
public class NotifyService { /** * 发送通知 * * @param msg 消息 */ public void sendMessage(NotifyStrategy strategy, String msg) { strategy.getNotifier().sendMessage(msg); } } -
测试类
@Test public void testSendMessage() { NotifyService notifyService = new NotifyService(); notifyService.sendMessage(NotifyStrategy.EMAIL, "hello 邮件"); notifyService.sendMessage(NotifyStrategy.SMS, "hello 短信"); notifyService.sendMessage(NotifyStrategy.WECHAT, "hello 微信"); }
练功心得
通过使用枚举策略模式,我们可以轻松地根据不同的支付方式执行不同的逻辑。相对于传统的if-else或switch-case结构,这种实现方式有以下几个优点:
- 代码简洁易读:策略的定义与逻辑的实现集中在枚举类中,使代码结构更加清晰,减少了冗余的判断语句。
- 便于扩展:添加新的支付方式时,只需在枚举类中新增一个枚举常量及其实现,而无需修改现有代码,符合“开放-关闭原则”。
- 易于维护:将策略的具体实现和业务逻辑解耦,增强了代码的可维护性。当某个支付方式的逻辑发生变更时,只需修改对应的枚举常量实现即可。
外功5-hutool简化代码
练功场景
这个工具包,几乎能涵盖我们日常所需的常用功能类。例如字符串处理、日期转换、文件操作、数据校验等。
练功代码
这里仅仅举一个在实际开发中遇到的问题。更多 测试代码,在本地gitlab上下载java-external项目后查看。
要解决的问题:把一段json字符串中的中settlegroup值在原有基础上+1,如下图所示:
String jsonStr = "[\n" +
" {\n" +
" "curdate": "2022-01-01",\n" +
" "isholiday": 1,\n" +
" "settlegroup": 3754\n" +
" },\n" +
" {\n" +
" "curdate": "2022-01-02",\n" +
" "isholiday": 1,\n" +
" "settlegroup": 3754\n" +
" },\n" +
" {\n" +
" "curdate": "2022-01-03",\n" +
" "isholiday": 1,\n" +
" "settlegroup": 3754\n" +
" },\n" +
" {\n" +
" "curdate": "2022-01-04",\n" +
" "isholiday": 0,\n" +
" "settlegroup": 3755\n" +
" }\n" +
"]";
String result = ReUtil.replaceAll(jsonStr, "(3\d{3})", p -> "" + (Convert.toInt(p.group()) + 1));
System.out.println(result);
练功心得
虽然hutool包很好用,但是他的漏洞通报也是最勤的。很多的时候不一定是它代码的问题,而是它的依赖包的问题。所以,在使用hutool工具包时,尽量遵循“最小依赖”原则-用到什么功能,依赖对应的hutool包。
| 模块 | 介绍 |
|---|---|
| hutool-aop | JDK动态代理封装,提供非IOC下的切面支持 |
| hutool-bloomFilter | 布隆过滤,提供一些Hash算法的布隆过滤 |
| hutool-cache | 简单缓存实现 |
| hutool-core | 核心,包括Bean操作、日期、各种Util等 |
| hutool-cron | 定时任务模块,提供类Crontab表达式的定时任务 |
| hutool-crypto | 加密解密模块,提供对称、非对称和摘要算法封装 |
| hutool-db | JDBC封装后的数据操作,基于ActiveRecord思想 |
| hutool-dfa | 基于DFA模型的多关键字查找 |
| hutool-extra | 扩展模块,对第三方封装(模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等) |
| hutool-http | 基于HttpUrlConnection的Http客户端封装 |
| hutool-log | 自动识别日志实现的日志门面 |
| hutool-script | 脚本执行封装,例如Javascript |
| hutool-setting | 功能更强大的Setting配置文件和Properties封装 |
| hutool-system | 系统参数调用封装(JVM信息等) |
| hutool-json | JSON实现 |
| hutool-captcha | 图片验证码实现 |
| hutool-poi | 针对POI中Excel和Word的封装 |
| hutool-socket | 基于Java的NIO和AIO的Socket封装 |
| hutool-jwt | JSON Web Token (JWT)封装实现 |
外功6-泛型
练功场景
泛型既“参数化类型”,在方法定义时,将方法中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量。泛型的本质是将类型参数化。
在实际的使用中,可能带来的好处主要有:
-
类型安全
泛型允许在编译时进行类型检查,从而减少运行时错误。使用泛型时,可以确保只有特定类型的对象可以被添加到集合中,这样可以避免类型转换异常。
-
避免强制类型转换
在使用泛型之前,为了实现类似的功能,常常需要使用“对象”类型的数组或集合,并在取出元素后进行强制类型转换。使用泛型后,可以直接指定集合中元素的类型,从而消除了这种强制类型转换的需求,使代码更加简洁、易读,并减少了出错的机会。
-
提高代码复用
泛型允许你编写能够处理不同类型的通用类或方法,这意味着你可以编写一个泛型类或方法来处理多种数据类型,而无需为每种类型重复编写相同的逻辑。
练功代码
-
泛型类
public class GenericsClass<E> { private final E[] array = (E[]) new Object[10]; private int size = 0; /** * 获取元素 * * @param index 索引值 * @return 返回元素 */ public E get(int index) { return array[index]; } /** * 设置元素 * * @param value 增加的值 */ public void add(E value) { array[size++] = value; } }泛型类中定义的E,可以在所有方法中使用
-
泛型方法
public class GenericsMethod { Object[] array = new Object[10]; int size = 0; /** * 获取元素 * @param index 索引值 * @return 返回元素 */ public <E> E get(int index) { return (E) array[index]; } /** * 增加元素 * * @param value 增加的值 */ public <E> void add(E value) { array[size++] = value; } }泛型方法中的E,只有在本方法中可以使用
-
泛型接口
自定义一个泛型接口
public interface GenericsInterface<K, V> { /** * 添加元素 * * @param key 键 * @param value 值 */ void put(K key, V value); /** * 获取元素 * @param key 键 * @return 值 */ V get(K key); }接口实现类
public class GenericsInterfaceImpl implements GenericsInterface<String, Integer>{ private final Map<String, Integer> map = new HashMap<>(); /** * 添加元素 * * @param key 键 * @param value 值 */ @Override public void put(String key, Integer value) { map.put(key, value); } /** * 获取元素 * * @param key 键 * @return 值 */ @Override public Integer get(String key) { return map.get(key); } } -
泛型不具备继承性
准备3个空类:A类,B类继承A,C继承B类。演示泛型不具备继承性。
public class GenericsExtend { public static void fanxing(List<B> list) { } public static void main(String[] args) { List<A> listA = new ArrayList<>(); List<B> listB = new ArrayList<>(); List<C> listC = new ArrayList<>(); fanxing(listA); fanxing(listB); fanxing(listC); } }编译报错:
可以得出结论,泛型是不具备继承性的,也就是说,一个方法传入的对象泛型是什么类型,我们不能把参数泛型的子类泛型对象作为参数传递给方法,该泛型是不具备继承性的,传入编译器会报错。
-
数据具备继承性
准备3个空类:A类,B类继承A,C继承B类。演示数据具备继承性。
public class GenericsExtend2 { public static void main(String[] args) { List<A> listA = new ArrayList<>(); List<B> listB = new ArrayList<>(); List<C> listC = new ArrayList<>(); listA.add(new A()); listA.add(new B()); listA.add(new C()); listB.add(new B()); listB.add(new C()); listC.add(new C()); } }当我们为一个类指定泛型并创建对象之后,对象中不仅可以加入泛型所指定的类对象,还可以加入泛型类子类的类对象,这就是数据的继承性。
注意这里说的是对象,上面不具备继承性中说的是参数
-
泛型的通配符
为了解决参数上面泛型不具备继承性的问题。可以使用通配符:
在Java中,泛型的通配符是一个 "?","?" 也代表不确定的类型,它配合关键词 extend 或 super 可以对类型做出限定。
我们可以写出如下两种写法
?extend <E>:这个写法表示可以传递泛型E包括泛型E的所有子类类型。 ?super <E>:这个写法表示可以传递泛型E包括泛型E的所有父类类型。通过上面的说明,前面的例子可以修改为:
public class GenericsExtend { public static void fanxing(List<? extends A> list) { } public static void main(String[] args) { List<A> listA = new ArrayList<>(); List<B> listB = new ArrayList<>(); List<C> listC = new ArrayList<>(); fanxing(listA); fanxing(listB); fanxing(listC); } }如果类型不确定,但是知道要传入的参数类型是某个继承体系中的一个,就可以使用泛型通配符来表示。
练功心得
常用的泛型标识
| 泛型标识 | 解释 |
|---|---|
| T | 表示“Type”。代表一般的任何类 |
| E | 表示 "Element"(元素),通常用于集合类。 |
| K | 表示 "Key"(键),通常用于映射类。 |
| V | 表示 "Value"(值),通常与K一起使用。 |
| N | 表示 "Number"(数字),通常用于数字类型。 |
类型擦除
- 泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类)
- 在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)
ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
arrayInteger.add(111); //类型擦除
Integer n = arrayInteger.get(0);
====================================================
Integer n = arrayInteger.get(0);// 这条代码底层如下:
//(1)get() 方法的返回值返回的是 Object 类型
Object object = arrayInteger.get(0);
//(2)编译器自动插入 Integer 的强制类型转换
Integer n = (Integer) object;
外功7-位与鉴权
练功场景
在某些验证权限的时候,经常用字符串判断。但是效率并不是很高而且浪费存储空间,可以利用位运算(尤其是位与运算),我们可以用一个整数的每一位表示一种权限,这样不仅节省了存储空间,还能高效地判断用户是否具备某种权限。
例如,将权限值定义为1、2、4、8等2的幂数,当用户的权限值为7时,即表示用户具有权限1、2、4(因为7 = 1 + 2 + 4)。通过位与运算,我们可以快速判断用户是否具有某个特定的权限。
练功代码
public class Permission {
// 定义权限常量,每个权限对应一个位
// 0001
public static final int READ = 1;
// 0010
public static final int WRITE = 2;
// 0100
public static final int DELETE = 4;
// 1000
public static final int EXECUTE = 8;
/**
* 检查用户是否具有特定权限
* @param userPermissions 用户的权限值
* @param permissionToCheck 要检查的权限
* @return 是否具有该权限
*/
public static boolean hasPermission(int userPermissions, int permissionToCheck) {
return (userPermissions & permissionToCheck) != 0;
}
public static void main(String[] args) {
// 用户拥有权限:1 (READ), 2 (WRITE), 4 (DELETE)
int userPermissions = 7;
System.out.println("用户是否有READ权限? " + hasPermission(userPermissions, READ));
System.out.println("用户是否有WRITE权限? " + hasPermission(userPermissions, WRITE));
System.out.println("用户是否有DELETE权限? " + hasPermission(userPermissions, DELETE));
System.out.println("用户是否有EXECUTE权限? " + hasPermission(userPermissions, EXECUTE));
}
}
练功心得
利用位与操作进行权限判断是一种高效而优雅的方案,特别适用于需要管理大量权限且频繁进行权限校验的系统中。这种方法具有高效、节省内存、代码简洁等特点。
外功8-DBeaver中定义变量
练功场景
DBeaver是一个通用的数据库管理工具和SQL客户端。它的社区版本是免费的,但是功能却非常强大。它有一个非常显著的特点是能根据环境的不同,区分不同的操作权限。
在开发库的数据库可以修改、删除等操作。而在生产库,可以设置成只读库。即便设置成可以修改,也会弹出确认对话框,避免在生产库出现误操作。
有的时候我们同时查询几个关联库的信息,但是条件值是一致的。这种情况就很适合自定义变量。
练功代码
-- 在DBeaver的SQL编辑器中预定义变量值
@set myvar=1826455679497400322
-- 使用该变量的地方直接使用变量占位符
SELECT * FROM discount_activity_review WHERE id = ${myvar};
SELECT * FROM discount_award_review WHERE acitivity_id = ${myvar};
SELECT * FROM discount_day_summary_review WHERE activity_id = ${myvar};
练功心得
DBeaver是属于越用越好用的一款数据库工具。