开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java基础学习16之定时器、加密、File类 Java基础

发表于 2021-11-17

Java基础学习16之定时器、加密、File类

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」。

关于作者

  • 作者介绍

🍓 博客主页:作者主页

🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆。

🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻。


定时器

推动整个计算机硬件的发展的核心关键性技术就是时钟。所以在企业开发中定时操作往往成为开发重点。而在JDK本身也支持这种定时调度的处理操作,这种操作不会直接使用。还是和多线程有关。

如果想要定时调度处理操作,需要两个类

  • 定时调度任务:java.util.TimerTask
  • 定时调度操作:java.util.Timer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package com.day14.demo;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

class MyTask extends TimerTask{

@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(new SimpleDateFormat("YYYY-MM-dd HH:mm:ss.SSS").format(new Date()));
}

}
public class TaskDemo {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new MyTask(), 1000,2000);
}
}

UUID类

UUID类是根据你当前的地址还有时间戳自动生成一个几乎不会重复的字符串。

1
2
3
4
5
6
7
8
9
10
java复制代码package com.day14.demo;

import java.util.UUID;

public class UUIDDemo {
public static void main(String[] args) {
UUID uuid = UUID.randomUUID();
System.out.println(uuid);
}
}

以后再文件上传等类似的操作中通过UUID类来进行简单的文件名称,以防止重名的问题。

Base64加密处理

Base64是一种数据加密算法,使用整个算法可以使我们的数据进行安全处理。如果要想进行加密处理可以使用两个:加密器、解密器。

加密处理

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码package com.day14.demo;

import java.util.Base64;

public class Base64Demo {
public static void main(String[] args) {
String msg = "123456";
String emsg = Base64.getEncoder().encodeToString(msg.getBytes());
System.out.println("加密:" + emsg);
byte[] decode = Base64.getDecoder().decode(emsg);
System.out.println("解密:" + new String(decode));
}
}

多次加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.day14.demo;

import java.util.Base64;

public class Base64Demo {
public static void main(String[] args) {
String msg = encode("123456");
String emsg = encode(encode(encode(msg)));
System.out.println("加密:" + emsg);
byte[] decode = Base64.getDecoder().decode(emsg);
System.out.println("解密:" + new String(decode));
}
public static String encode(String code){
return Base64.getEncoder().encodeToString(code.getBytes());
}
}

还有一个做法就是种子树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码package com.day14.demo;

import java.util.Base64;

public class Base64Demo {
public static void main(String[] args) {
String sed = encode("zsr--rsz");
String msg = "123456"+sed;
String emsg = encode(msg);
System.out.println("加密:" + emsg);
byte[] decode = Base64.getDecoder().decode(emsg);
System.out.println("解密:" + new String(decode));
}
public static String encode(String code){
return Base64.getEncoder().encodeToString(code.getBytes());
}
}

必须保证长度,以后的开发将Base64和MD5一起开发,长度是32位。

ThreadLocal类

TheadLocal类不继承Thread类,也不实现Runable接口,ThreadLocal类为每一个线程都维护了自己独有的变量拷贝。每个线程都拥有自己独立的变量。

ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

ThreadLocal的实现是这样的:每个Thread维护了一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身。Value是真正需要存储的变量。也就是说,ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。注意,ThreadLocalMap是使用ThreadLocal的弱引用作为key的,弱引用的对象在GC时会被回收。

通过给方法传递参数,调用两个线程输出不同的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码package com.day14.demo;
class Message{
private String note;



public String getNote() {
return note;
}

public void setNote(String note) {
this.note = note;
};
}
class GetMessage{
public void print(Message msg){
System.out.println(Thread.currentThread().getName() + msg.getNote());
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
new Thread(()->{
Message message = new Message();
message.setNote("Hello,world!!");
new GetMessage().print(message);
},"用户A").start();

new Thread(()->{
Message message = new Message();
message.setNote("Hello,world!!zsr");
new GetMessage().print(message);
},"用户B").start();
}
}

但是我现在的需求是不希望通过传递传输给GetMessage的print方法,还希望实现相同的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码package com.day14.demo;
class Message{
private String note;



public String getNote() {
return note;
}

public void setNote(String note) {
this.note = note;
};
}
class GetMessage{
public void print(){
System.out.println(Thread.currentThread().getName() + MyUtil.message.getNote());
}
}
class MyUtil{
public static Message message;
}
public class ThreadLocalDemo {
public static void main(String[] args) {
new Thread(()->{
Message msg = new Message();
msg.setNote("Hello,world!!");
MyUtil.message = msg;
new GetMessage().print();
},"用户A").start();

new Thread(()->{
Message msg = new Message();
msg.setNote("Hello,world!!zsr");
MyUtil.message = msg;
new GetMessage().print();
},"用户B").start();
}
}

我们发现两个线程的内容并没有同步输出,所以我们会想到通过ThreadLocal类来解决此数据不同步的问题。

image-20210818114329415

1
java复制代码public class ThreadLocal<T> extends Object

这个类里面有几个重要的方法:

  • 取得数据:public T get()
  • 存放数据:public void set(T value)
  • 删除数据:public void remove()

利用ThreadLocal来解决当前我问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码package com.day14.demo;
class Message{
private String note;



public String getNote() {
return note;
}

public void setNote(String note) {
this.note = note;
};
}
class GetMessage{
public void print(){
System.out.println(Thread.currentThread().getName() + MyUtil.get().getNote());
}
}
class MyUtil{
private static ThreadLocal<Message> tl = new ThreadLocal<Message>();
public static void set(Message msg){
tl.set(msg);
}
public static Message get(){
return tl.get();
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
new Thread(()->{
Message msg = new Message();
msg.setNote("Hello,world!!");
MyUtil.set(msg);
new GetMessage().print();
},"用户A").start();

new Thread(()->{
Message msg = new Message();
msg.setNote("Hello,world!!zsr");
MyUtil.set(msg);
new GetMessage().print();
},"用户B").start();
}
}

这样我们就解决了数据不同步的问题了。

IO—File类

基本操作

如果要想学好IO,必须要清楚抽象类、IO的操作部分掌握两个代码模型。IO的核心组成五个类(File、OutputStream、InputStream、Writer、Reader)一个接口(Serializable)。

再java.io是一个与文本本身操作有关的程序(创建、删除、信息取得—)

如果要想使用File类操作文件的话,那么肯定要通过构造方法实例化File类对象,而实例化File类对象的过程之中主要使用以下两种构造方法:

  1. 方法一:public File(String pathname);
  2. 方法二:public File(File parent,String child).

文件的基本操作,主要有以下几种功能:

  • 创建一个新文件:public boolean createNewFile() throws IOException
  • 删除文件:public Boolean delete();
  • 判断路径是否存在:public Boolean exists();

创建新文件

1
2
3
4
5
6
7
8
9
10
11
java复制代码package com.day14.demo;

import java.io.File;
import java.io.IOException;

public class FileDemo {
public static void main(String[] args) throws IOException {
File file = new File("f:\\hello.txt");
file.createNewFile();
}
}

如果文件存在进行删除,不存在进行创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.day14.demo;

import java.io.File;
import java.io.IOException;

public class FileDemo {
public static void main(String[] args) throws IOException {
File file = new File("f:\\hello.txt");
if(file.exists()){//文件存在
file.delete();
}else{//文件不存在
file.createNewFile();//创建文件
}
}
}

本程序操作就表示如果文件存在则删除,如果不存在则创建一个新的文件,此时基本功能是实现了,不过这个程序此时存在三个问题:

问题一:关于路径的分隔符

在windows操作之中,使用“\”作为路径分隔符,而在Linux系统下使用“/”作为路径分隔符,而实际的开发而言,大部分情况下会在windows中做开发,而后将项目部署在Linus,那么此时,路径的分隔符都需要进行修改,这样实在是国语麻烦,为此在File类之中提供了一个常量:public static final String separator(按照Java的命名规范来讲,对于全局常量应该使用大写字母的方式定义,而此处使用的是小写,是由Java的发展历史所带来的问题)。

1
java复制代码//由不同的操作系统的JVM来决定最终的separator是什么内容File file = new File("f:" + File.separator + "hello.txt");

问题二:是有可能会出现的延迟问题

发现程序执行完成之后,对于文件的创建或者是删除是会存在一些操作上的延迟,如果现在假设先删除了一个文件,而后立刻判断此文件是否存在,那么可能得到的结果就是错误的(为true),一位所有的*.class文件都要通过JVM与操作系统间接操作,这样就有可能会出现延迟的问题。

image-20210818141728897

问题三:目录问题

之前进行文件创建的时候都是在根目录下创建完成的,如果说现在要创建的文件有目录呢?例如,现在要创建一个f:\hello\hello.txt文件,而此时在执行程序的时候hello目录不存在,这个时候执行的话就会出现错误提示:

1
java复制代码Exception in thread "main" java.io.IOException: 系统找不到指定的路径。

因为现在目录不存在,所以不能创建,那么这个时候必须首先判断要创建文件的父路径是否存在,如果不存在应该创建一个目录,之后再进行文件的创建,而要想完成这样的操作,需要一下几个方法支持:

找到一个指定文件的父路径:public File getParentFile();

创建目录:public boolean mldirs()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.day14.demo;

import java.io.File;
import java.io.IOException;

public class FileDemo {
public static void main(String[] args) throws IOException {
//由不同的操作系统的JVM来决定最终的separator是什么内容
File file = new File("f:" + File.separator +"hello" + File.separator + "hello.txt");
if(!file.getParentFile().exists()){//父目录
file.getParentFile().mkdirs();//创建父目录
}
if(file.exists()){//文件存在
file.delete();
}else{//文件不存在
file.createNewFile();//创建文件
}
}
}

以后在任何的java.io.File类开发过程之中,都一定要考虑文件目录的问题。

取得文件信息

在File类之中还可以通过以下的方法取得一些文件的基本信息:

方法名称 描述
public String getName() 取得文件的名称
public boolean isDirectory() 给定的路径是否为文件夹
public boolean isFile() 给定的路径是否是文件
public boolean isHidden() 是否是隐藏文件
public long lastModified() 文件的最后一次修改日期
public long length() 取得文件大小,是以字节为单位返回的。

获取文件的大小信息以及核心信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package com.day14.demo;
import java.io.File;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
class MyMath{
public static double round(double num, int scale){
return Math.round(num * Math.pow(10, scale)) / Math.pow(10, scale);
}
}
public class FileDemo2 {
public static void main(String[] args) {
File file = new File("f:" + File.separator + "701_03_支付宝沙箱使用.avi");
if(file.exists() && file.isFile()){
System.out.println("1文件大小为:" + MyMath.round(file.length() / (double) 1024 / 1024, 2));
System.out.println("最后一次修改日期:"+ new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date(file.lastModified())));
System.out.println("2文件大小为:" + new BigDecimal(file.length() / (double)1024 /1024).divide(new BigDecimal(1), 2, BigDecimal.ROUND_HALF_UP).doubleValue());
}
}
}

列出目录内容:public File [] listFiles(),此方法将目录中的所有文件以File对象数组的方式返回;

列出目录的所有结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.day14.demo;

import java.io.File;
import java.util.Arrays;

public class FileDemo3 {
public static void main(String[] args) {
File file = new File("f:" + File.separator);
if(file.exists() && file.isDirectory()){
File[] listFiles = file.listFiles();
System.out.println(Arrays.toString(listFiles));
}
}
}

综合案例:目录列表

现在希望把一个目录下的全部文件都列出来,那么这种情况下只能采用递归:因为列出一个目录下的全部文件或着文件夹之后,如果发现列出的内容是文件夹,则应该向后继续列出。

列出指定目录下的所有文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码package com.day14.demo;

import java.io.File;

public class FileListDemo {
public static void main(String[] args) {
File file = new File("f:" + File.separator);
list(file);
}
public static void list(File file){
if(file.isDirectory()){
File[] files = file.listFiles();
if(files != null){
for (int i = 0; i < files.length; i++) {
list(files[i]);//继续列出
}
}
}
System.out.println(file);
}
}

线程阻塞问题

现在代码都在main线程下执行,如果该程序执行完成后才能继续执行下一条语句,要想解决这种耗时的问题。最好产生一个新的线程进行列出。

1
java复制代码package com.day14.demo;import java.io.File;public class FileListDemo {	public static void main(String[] args) {		new Thread(()->{			File file = new File("f:" + File.separator);		list(file);		},"列出线程").start();				System.out.println("开始进行文件信息列出。。。。");	}	public static void list(File file){		if(file.isDirectory()){			File[] files = file.listFiles();			if(files != null){				for (int i = 0; i < files.length; i++) {				list(files[i]);//继续列出				}			}			}		System.out.println(file);	}}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《Kubernetes权威指南》笔记1 Hello K8s

发表于 2021-11-17

Hello K8s

  1. 创建集群 kind create cluster
  2. kubectl apply -f mysql-deploy.yaml
  3. k create -f mysql-svc.yaml
  4. k get svc mysql 获取 CLUSTER-IP
  5. k apply -f myweb-deploy.yaml 连接 CLUSTER-IP
  6. k create -f myweb-svc.yaml 指定 nodePort
  7. k get nodes -o wide 获取 node 的 INTERNAL-IP,简称 nodeIP
  8. curl -vL http://<nodeIP>:<nodePort>/demo/

其他命令

1
2
3
4
sql复制代码k get pods
k get nodes
k get scv
k describe node <name>

集群 Cluster

Cluster 包含一个 Master 和 N 个 Node(N>=0)。

Master 上的关键进程

  • kube-apiserver:提供 HTTP RESTful API接口的主要服务,是Kubernetes里对所有资源进行增、删、改、查等操作的唯一入口,也是集群控制的入口进程。
  • kube-controller-manager:Kubernetes 里所有资源对象的自动化控制中心
  • kube-scheduler:负责资源调度(Pod 调度)的进程
  • etcd

Node 上的关键进程

  • kubelet:负责 Pod 对应容器的创建、启停等任务,同时与 Master 密切协作,实现集群管理的基本功能。
  • kube-proxy:实现 Kubernetes Service 的通信与负载均衡机制的服务。
  • 容器运行时(如 Docker):负责本机的容器创建和管理。

Service

一般说来,Service指的是无状态服务,通常由多个程序副本提供服务,在特殊情况下也可以是有状态的单实例服务,比如MySQL这种数据存储类的服务。

系统最终由多个提供不同业务能力而又彼此独立的微服务单元组成,服务之间通过TCP/IP进行通信,从而形成强大又灵活的弹性网格。

Pod

Pod里的多个业务容器共享Pause容器的IP,共享Pause容器挂接的Volume,这样既简化了密切关联的业务容器之间的通信问题,也很好地解决了它们之间的文件共享问题。

Kubernetes为每个Pod都分配了唯一的IP地址,称之为Pod IP,一个Pod里的多个容器共享Pod IP地址。Kubernetes要求底层网络支持集群内任意两个Pod之间的TCP/IP直接通信,因此我们需要牢记一点:在Kubernetes里,一个Pod里的容器与另外主机上的Pod容器能够直接通信。

普通的Pod(不普通的Pod是指静态Pod)一旦被创建,就会被放入etcd中存储,随后被Kubernetes Master调度到某个具体的Node上并绑定(Binding),该Pod被对应的Node上的kubelet进程实例化成一组相关的Docker容器并启动。

图1.6 Pod、容器与Node的关系

Kubernetes 内部在每个 Node 上都运行了一套全局的虚拟负载均衡器,自动注入并自动实时更新集群中所有 Service 的路由表,通过 iptables 或者 IPVS 机制,把对 Service 的请求转发到其后端对应的某个 Pod 实例上,并在内部实现服务的负载均衡与会话保持机制。

Label

一个 Label 是一个 key=value 的键值对,可以被附加到各种资源对象上,例如 Node、Pod、Service、Deployment 等。

一些常用的Label示例如下:

版本标签:release:stable和release:canary。

  • 环境标签:environment:dev | qa | production。
  • 架构标签:tier:frontend | backend | middleware。
  • 分区标签:partition:customerA | customerB。
  • 质量管控标签:track:daily | weekly。

可以通过 Label Selector(标签选择器)查询和筛选拥有某些Label的资源对象,如

  • name=redis-slave
  • env!=production
  • name in (redis-master,redis-slave)
  • name not in (php-frontend)
  • name=redis-slave, env!=production

我们几乎见不到没有 Label 的 Pod。

Deployment

Deployment 资源根据我们指定的模板自动创建指定数量的Pod实例。

ClusterIP

Pod 的 Endpoint(即 IP + 端口)地址会随着 Pod 的销毁和重新创建而发生改变,因为新 Pod 的 IP 地址与旧 Pod 的不同。
而 Service 一旦被创建,Kubernetes 就会自动为它分配一个全局唯一的虚拟IP——ClusterIP,而且在 Service 的整个生命周期内,其 ClusterIP 地址不会发生改变,这样一来,每个服务就变成了具备唯一 IP 的通信节点,远程服务之间的通信问题就变成了基础的 TCP 网络通信问题。

因为没有一个“实体网络对象”来响应,所以 ClusterIP 无法被 ping 通。ClusterIP 只能与Service Port 组成一个具体的服务访问端点,单独的 ClusterIP 不具备 TCP/IP 通信的基础。

只要在 Service 的定义中设置了clusterIP:None,就定义了一个 Headless Service,它与普通 Service 的关键区别在于它没有 ClusterIP,如果解析 Headless Service 的 DNS 域名,则返回的是该 Service 对应的全部 Pod 的 Endpoint 列表,这意味着客户端是直接与后端的Pod建立TCP/IP连接进行通信的,没有通过虚拟 ClusterIP 地址进行转发,因此通信性能最高,等同于“原生网络通信”。

三种 IP

Node IP、Pod IP 和 Service IP。

Node IP 是集群中每个节点的物理网卡的 IP,是一个真实存在的物理网络 IP。这也表明集群之外的节点访问集群内的某个节点或者服务时,都必须通过 Node IP 通信。

Pod IP 是每个 Pod 的 IP,在使用 Docker 作为容器支持引擎的情况下,它是 Docker Engine 根据 docker0 网桥的 IP 段进行分配的,通常是一个虚拟二层网络。Kubernetes 要求位于不同 Node 上的 Pod 都能够彼此直接通信,所以 Kubernetes 中一个 Pod 里的容器访问另外一个 Pod 里的容器时,就是通过 Pod IP 所在的虚拟二层网络进行通信的。

Service 的 ClusterIP 地址属于集群内的地址,无法在集群外直接使用。为了解决这个问题,Kubernetes 引入了NodePort。

但 NodePort 还没有完全解决外部访问 Service 的所有问题,比如负载均衡问题。于是Kubernetes 提供了自动化的解决方案,如果我们的集群运行在谷歌的公有云 GCE 上,那么只要把Service的 “type=NodePort” 改为 “type=LoadBalancer”,Kubernetes 就会自动创建一个对应的负载均衡器实例并返回它的 IP 供外部客户端使用。

端口是有限的物理资源,那能不能让多个 Service 共用一个对外端口呢?这就是后来增加的 Ingress资源对象所要解决的问题。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Java学习笔记】equal与==

发表于 2021-11-17

==

对于基本数据类型来说,主要是匹配值是否相同。

1
2
3
4
5
6
7
8
ini复制代码public class Equals {
   public static void main(String[] args) {
       int a = 1;
       int b = 1;
       System.out.println(a == b);
       System.out.println(b == a);
  }
}

输出结果如下:

1
2
arduino复制代码true
true

对于引用类型来说,==匹配的是两个是否指向了同一内存区域。

1
2
3
4
5
6
7
8
9
10
java复制代码public class Equals {
​
   static Person person1 = new Person();
​
   static Person person2 = new Person();
​
   public static void main(String[] args) {
       System.out.println(person1 == person2);
  }
}

上述通过new关键字分别创建了2个对象,此时我们用==进行比较,结果如下

1
arduino复制代码false

这里涉及到对象创建的只是了,后续在虚拟机相关笔记中体现。

equal

equals 是 Java 中所有对象的父类,即 Object 类定义的一个方法。它只能比较对象,它表示的是引用双方的值是否相等。

1
2
3
4
5
6
7
8
9
csharp复制代码public class Equals {
   public static void main(String[] args) {
       Person person1 = new Person();
       Person person2 = new Person();
       person1.setName("1");
       person2.setName("1");
       System.out.println(person1.getName().equals(person2.getName()));
  }
}

equals 用作对象之间的比较具有如下特性

  • 自反性:对于任何非空引用 x 来说,x.equals(x) 应该返回 true。
  • 对称性:对于任何非空引用 x 和 y 来说,若x.equals(y)为 true,则y.equals(x)也为 true。
  • 传递性:对于任何非空引用的值来说,有三个值,x、y 和 z,如果x.equals(y) 返回true,y.equals(z) 返回true,那么x.equals(z) 也应该返回true。
  • 一致性:对于任何非空引用 x 和 y 来说,如果 x.equals(y) 相等的话,那么它们必须始终相等。
  • 非空性:对于任何非空引用的值 x 来说,x.equals(null) 必须返回 false。

String的equal实现

String也是在日常编程中经常使用的修饰符之一,它和其它基本数据类型不同,String本身是一个常量(final),其不能被其他对象所继承(至于原因,据说是开发者觉得String实现已经非常完美了,不想开发者任意扩展来破坏它),其一经赋值便无法再改变,继承自Object,所以Object具有的能力在String中都可以找到相对于的。

此处我们重点分析,String中的equal实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
arduino复制代码public boolean equals(Object anObject) {
   // 首先满足自反性,自己传入自己确保返回为true
   if (this == anObject) {
       return true;
  }
   // 确保传入的Object为String类型
   if (anObject instanceof String) {
       String anotherString = (String)anObject;
       int n = value.length;
       // 判断长度是否相等,如果不等一定为false
       if (n == anotherString.value.length) {
           char v1[] = value;
           char v2[] = anotherString.value;
           int i = 0;
           // 开始逐个字符比较,如果发现不相等的字符,则返回false
           while (n-- != 0) {
               if (v1[i] != v2[i])
                   return false;
               i++;
          }
           return true;
      }
  }
   return false;
}

String中equals的实现总结来说可以是以下几点:

1.判断是否传入的是自身,如果是返回true。

2.判断是否是String对象,如果不是返回true。

2.1 如果是String对象 则比较传入的len是否一致,如果一致,则继续比较。

2.2 比较字符,如果字符不一致,则返回false,反之继续比较直到比较完全(这里是一个循环全量匹配)

这里再提示一下,你可能有疑惑什么时候是

1
2
3
kotlin复制代码if (this == anObject) {
 return true;
}

这个判断语句如何才能返回 true?因为都是字符串啊,字符串比较的不都是堆空间吗,猛然一看发现好像永远也不会走,但是你忘记了 String.intern() 方法,它表示的概念在不同的 JDK 版本有不同的区分

在 JDK1.7 及以后调用 intern 方法是判断运行时常量池中是否有指定的字符串,如果没有的话,就把字符串的引用添加到常量池中,并不是复制一份对象放入其中了。

重写HashCode

equals 方法和 hashCode 都是 Object 中定义的方法,它们经常被一起重写。

equals 方法是用来比较对象大小是否相等的方法,hashcode 方法是用来判断每个对象 hash 值的一种方法。如果只重写 equals 方法而不重写 hashcode 方法,很可能会造成两个不同的对象,它们的 hashcode 也相等,造成冲突。比如

1
2
3
4
ini复制代码String str1 = "通话";
String str2 = "重地";
System.out.println(str1.hashCode());
System.out.println(str2.hashCode());

上述两个字符串所代表的hashcode就是相同的。

1
2
复制代码1179395
1179395

它们两个的 hashcode 相等,但是 equals 可不相等。

关于equals总结如下

  • 如果在 Java 运行期间对同一个对象调用 hashCode 方法后,无论调用多少次,都应该返回相同的 hashCode,但是在不同的 Java 程序中,执行 hashCode 方法返回的值可能不一致。
  • 如果两个对象的 equals 相等,那么 hashCode 必须相同
  • 如果两个对象 equals 不相等,那么 hashCode 也有可能相同,所以需要重写 hashCode 方法,因为你不知道 hashCode 的底层构造(后续可以解析jvm源码深入认识hashcode的构造),所以你需要重写 hashCode 方法,来为不同的对象生成不同的 hashCode 值,这样能够提高不同对象的访问速度。
  • hashCode 通常是将地址转换为整数来实现的。

参考文献:github.com/crisxuan/be…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Cloud / Alibaba 微服务架构

发表于 2021-11-17

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战。

上篇文章对比基于Token(即JWT)与基于服务器的身份认证做了总结。本篇文章将对授权鉴权中心代码方面做个总结。

其实代码也是很简单的,核心功能点是JWTService里的三个授权相关功能方法实现,主要是用于生成Token以及用户注册。鉴权不在我们这个微服务里,而是放到通用模块下以工具类的形式存在,原因之前也说过了,主要是为了实现解耦和高性能,这里就不再赘述了。

最后再介绍一下e-commerce-authority-center模块中pom文件里的两个依赖。

image.png

freemarker和screw其实是由开源社区提供的两个工具,能够生成数据库的html文档或者是markdown文件,方便我们去查看数据库里有哪些数据表、数据表的字段各个类型各个注释等信息。经过验证,可以用于MySQL和PostgreSQL数据库。接下来介绍一下编写测试用例来生成数据库表文档。

1、我们在e-commerce-authority-center——>test——>java——>com.sheep.ecommerce下new一个类 DBDocTest。注入应用上下文,我们需要这个容器去获取数据源。

2、配置想要忽略的数据表,忽略表前缀如(a、b开头的数据库表),忽略表后缀,然后根据名称指定表生成,如下图所示。

image.png

3、编写生成文档的测试用例,如下图。

image.png

运行之后我们会发现文件路径下多了一个.html文件,打开如下图所示:

image.png

image.png

这就是我们通过这个测试用例生成的数据库设计文档,非常清晰且方便阅读,所以将这两个依赖推荐给大家。可以看到文档里包含了所有的表,每个数据表都包含它的表名及所有字段,字段包括字段名称、数据类型、长度、是否允许空值、是否为主键、默认值等等,内容非常详细,如果后续有改动数据表内容,则只需要将我们编写的这份测试用例重新执行就可以获取到一份更新后的数据库设计文档了,非常简单方便且无脑。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

架构知识实践与总结-分层 典型分层架构 无处不在的分层架构

发表于 2021-11-17

分层是架构设计的常用方法,也是指导我们做架构设计、功能设计的重要思想。 运用好分层能帮我们解决工作中许多难题,下面分三部分来介绍分层:典型分层架构、无处不在的分层思想、如何分层。

典型分层架构

我们主要介绍三种经典分层架构。

微服务分层架构

上图是一个典型的微服务架构分层示意图。客户端代表用户的App。

  1. 网关层作为系统真正入口,接收客户端发来的请求,并做预处理,以保证其转发到业务逻辑层都是有意义的。网关层主要实现安全策略、用户鉴权、流量控制、产生Session、生成请求ID等功能。
  2. 业务逻辑层的功能就是实现具体业务逻辑,比如订单服务逻辑、共享文档合并算法、红包算法等。
  3. 数据访问层提供数据读写功能,隔离业务逻辑和数据库。这样做的好处是方便对数据做统一处理,比如给热点数据加缓存、分库分表、更换数据库等操作都不需要业务逻辑层关心。
  4. 数据库层负责数据的存储。

我们考虑一个问题,微服务架构一定要分网关层、业务逻辑层、数据访问层、数据库这四层么?答案是否定的。四层架构只是一个相对优化的方案,对不同的业务规模不同的业务场景会有很多变化。

比如,我们在项目初期,数据结构可能比较单一,没必要把业务逻辑层和数据访问层过早的分离,这样只会徒增服务维护的成本。一种比较好的方式是把对数据库的访问封装成公共的jar包,在业务逻辑层大家都调用相同的jar包去访问数据库就可以了。随着业务发展,以后要抽离数据访问层为单独服务时改造成本也比较低。 再比如随着业务发展,业务逻辑层出现一些需要其他服务调用的模块,我们可以把业务逻辑层再分为两层,公共业务逻辑层和个性化业务逻辑层,以增加代码逻辑的复用性。

微服务分层架构还有一个调用原则,只允许上层调用下层的服务,不允许同层和反向的调用,以此来避免产生循环依赖。微服务通过分层解决服务的耦合、理清调用关系、加速业务开发。

网络分层

上图是典型的网络五层架构分层模型图。

网络是为了完成计算机之间的数据通信。

  1. 两台计算机之间互相通讯,需要解决数据和物理信号之间的转换,这就是物理层的主要作用。
  2. 通讯的双方需要互相找到对方,以正确发送信息。如何确认消息发送给正确的机器了呢?这就需要用到MAC地址和广播的功能。数据链路层就是通过MAC和广播等技术手段找到机器之间能通讯的物理链路。
  3. 全世界有无数台计算机,每一台计算机都有自己的MAC地址,如果发送一条消息需要全世界广播那成本太高了。通过IP地址进行网络划分,以区分不同的网络区域,这是网络层的主要工作。
  4. 每一台计算机都有多个端口号用来实现并行通讯,同时还要保证通讯的可靠性,这就是传输层解决的主要问题。
  5. 每台计算机上都有不同的应用程序来提供网页服务、FTP服务、邮件服务等等,应用层主要用来解决如何为用户提供服务的问题。

网络的五层架构分别负责不同的功能,最终完成数据从一台计算机应用程序到达另一台计算机应用程序的传输。分层之后物理层和数据链路层主要由硬件厂商来负责,网络层和传输层主要由操作系统负责,应用层由应用程序开发者来负责。

通过分层把复杂的问题简单化,大家的分工也更加明确了。

思考:网络可以不是五层架构么?

MVC分层

MVC是非常典型的应用程序开发架构,刚毕业接触Java开发时,Spring框架的MVC模式深入我心。

M:Model业务模型。

V:View视图,用户看到的交互页面。

C:Controller控制器,接收用户的请求,调用View和Model输出用户需要的数据。

通过业务模型、视图、控制器三层划分,在开发web应用时,可以在不同文件夹中聚焦业务代码。不同的控制器可以组合不同的业务模型和视图,也增加了代码复用性。

无处不在的分层架构

前面介绍的都是一些经典分层架构,其实分层架构和思想,可以体现在我们工作的方方面面。

web页面开发

这是一个web前端项目的分层架构示意图。

  1. 前端路由层负责统一处理浏览器地址及路由变化的公共逻辑,根据不同的路由选择不同的页面。
  2. 页面展现层负责本页面的绘制,并请求相应的数据填充页面。
  3. 数据逻辑层负责组装数据,并给页面展现层提供支撑。
  4. 接口层负责调用服务端接口,拉取数据。

通过分层可以方便我们组织代码结构,也方便代码逻辑的复用,比如多个页面用到了相同的数据,那就可以在多个页面调用相同的数据逻辑层函数。

接口服务开发

这是一个web接口服务的分层架构示意图。

  1. 服务端路由统一接收请求并负责转发请求。
  2. 路由中间件负责在转发前后添加一些通用逻辑,比如验证用户Token,统计请求次数等。
  3. Controller负责接收具体的请求,并调用不同的Service。
  4. Service主要实现请求的具体业务逻辑。
  5. Model层提供数据的读写能力给Service调用。

具体实例

之前在360人工智能研究院做智能外呼项目时,我们也是利用分层的思想设计服务的整体架构。

首先我们把业务分成分成线上服务和支撑服务两层,把数据存储抽象出存储层。线上服务直接对外提供语音交互功能,支持服务负责配置数据、采集日志等。

线上服务我们又分成接入层、中控、算法三层。其中接入层类似于网关,负责用户鉴权、流量转发、生成Session等,中控负责根据策略调度算法服务,算法层实现具体的算法逻辑。

通过两次分层,理清了整个业务各模块之间的关系,划分好了各模块的主要功能,既减少模块之间功能的耦合也方便服务的开发、部署、升级。

如何分层

既然分层思想如此重要,我们应该怎么更好使用呢?我总结了一些使用规则。

  1. 数据逻辑和展现逻辑分层。
  2. 公共服务下沉、私有服务上浮分层。
  3. 公共方法下沉,个性化方法上浮分层。
  4. 网络请求统一收口分层。
  5. 对外API和业务逻辑分层。
  6. 视图和行为分层。

总结

分层架构是我们做系统架构设计不可缺少的思想。大到系统总体架构,小到一个函数库封装都能体现出分层的设计思想。

分层起到的作用有:功能内聚,解耦,增强代码复用性,明确调用关系避免循环依赖,方便代码结构组织,方便业务扩展等。

以上就是我对分层的理解,欢迎私信交流。

诚邀关注公众号:一行舟

每周更新技术文章,和大家一起进步。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go 方法 声明方法 基本用法 总结

发表于 2021-11-17

Hi,我是行舟,今天和大家一起学习Go语言的方法。

在Go语言中方法是属于某个类型的函数,方法和函数相似,都是通过对一段代码逻辑的封装,达到重复调用的目的;但二者又有所不同:

  1. 函数和方法声明的方式不同。
  2. 函数可以被当作参数传递,方法则不行。
  3. 函数可以匿名,方法则不行。

接下来我们具体看下如何使用方法。

声明方法

1
2
3
go复制代码func (t Type) 方法名(传入参数) (返回值) {
    方法体
}

和声明函数相比,声明方法需要在func后面添加Type类型的t,t可以在方法中被访问到。

基本用法

初始化

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// 声明animal结构体
type animal struct {
   name string
   age int
   class string
   weight float32
}

// 声明animal类型的方法 getName
func (a animal) getName() {
   fmt.Printf("I am %s. \n" , a.name)
}

func main()  {
   a1 := animal{name:"Tom",age:3,weight:11.5,class:"猫"}
   a1.getName() // 调用animal结构体实例的方法
}

如上示例,我们声明了animal类型的结构体,并声明了animal类型的方法getName。然后声明了animal结构体的实例a1,a1就具有了animal的属性和方法。

方法不仅仅可以隶属于结构体类型,还可以隶属于非接口、非指针类型的其它任何自定义类型。

例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码// 声明自定义类型test为go语言的基本类型int
type test int

// 声明 test类型的方法printTest
func (t test) printTest()  {
   fmt.Println("I am t",  t)
}

func main()  {
   var t test
   t = 10
   t.printTest() // print I am t 10
}

上面的例子中,我们使用关键词type定义了test类型,属于go语言的基本类型int;然后声明了test类型的方法printTest

自定义类型是使用 type关键词声明的数据类型,可以是结构体(struct)、基本数据类型、函数。 如:type i int,type f func。

同一个类型的方法名称是不允许重复的,方法名和字段名之间也不允许重复,如果重复定义在编译期会报错。

函数代替方法

如上面的例1,我们也可以使用函数达到相同的效果。

例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// 声明animal结构体
type animal struct {
   name string
   age int
   class string
   weight float32
}

// 声明函数getName
func getName(a animal) {
   fmt.Printf("I am %s. \n" , a.name)
}

func main()  {
   a1 := animal{name:"Tom",age:3,weight:11.5,class:"猫"}
   getName(a1) // 调用getName函数
}

我们定义getName函数,函数的接收参数是animal类型,调用getName函数,传入a1,也达到了和例1相同的目的。

既然函数能达到和方法相同的目的,那为什么还要有方法呢?我认为主要有以下两个原因:

  1. Go语言不是传统的面向对象的语言,它没有类的概念。通过结构体和方法可以加强Go语言面向对象的特性,模拟类的作用。golang.org/doc/faq#Is_…
  2. 隶属于不同类型的方法可以重名,而函数不可以重名。

嵌套类型的方法

在结构体一节我们说到过,当结构体本身字段不存在时,会往被嵌套结构体的“深层”寻找。Go语言由浅入深逐层查找,找到了对应的字段就返回其值,并停止查找。对于方法也是相同的逻辑,Go语言会基于嵌套结构,由浅入深逐层查找,根据方法名调用对应的方法。

例4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
go复制代码// 声明animalName结构体
type animalName struct {
   firstName string
   lastName string
}

// 声明animal结构体
type animal struct {
   animalName
   age int
   class string
   weight float32
}

func (an animalName) getAnimalName()  {
   fmt.Printf("My name is %s %s", an.firstName, an.lastName)
}

func main()  {
   a1 := animal{
      animalName: animalName{
         firstName:"tom",
         lastName:"steven",
      },
      age: 3,
      class:"猫",
      weight:12.5,
   }
   a1.getAnimalName() // print My name is tom steven
}

在上面的例子中,我们定义了animal结构体类型和嵌套的animalName结构体类型。给animalName类型定义了getAnimalName方法,在执行a1.getAnimalName()方法时,Go语言逐层查找到animalName类型的getAnimalName方法并调用。

值类型和指针类型

前面例子中我们声明的方法都属于值类型,方法还可以属于指针类型。

和函数的参数类型相似,值类型是值的副本,当我们在方法内修改副本的值时,如果是非引用类型就不会修改原值,如果是引用类型会修改原值;指针类型是指针地址的副本,所以我们在方法内的修改都会修改原值。

例5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
go复制代码// 声明animalName结构体
type animalName struct {
   firstName string
   lastName string
}

// 声明animal结构体
type animal struct {
   animalName
   age int
   class string
   weight float32
}

func (an animalName) setAnimalName(firstName,lastName string)  {
   an.firstName =  firstName
   an.lastName = lastName
}

func (an *animalName) setAnimalNamePoint(firstName,lastName string)  {
   an.firstName =  firstName
   an.lastName = lastName
}

func main()  {
   a1 := animal{
      animalName: animalName{
         firstName:"tom",
         lastName:"steven",
      },
      age: 3,
      class:"猫",
      weight:12.5,
   }
   // print 修改前:a1.firstName=tom  a1.lastName=steven
   fmt.Printf("修改前:a1.firstName=%s  a1.lastName=%s \n", a1.firstName,a1.lastName)
   a1.setAnimalName("jerry","williams") // 修改a1的firstName和lastName
   // print 修改后:a1.firstName=tom  a1.lastName=steven
   fmt.Printf("修改后:a1.firstName=%s  a1.lastName=%s \n", a1.firstName,a1.lastName) 

   a2 := animal{
      animalName: animalName{
         firstName:"tom",
         lastName:"steven",
      },
      age: 3,
      class:"猫",
      weight:12.5,
   }

   // print 修改前:a2.firstName=tom  a2.lastName=steven
   fmt.Printf("修改前:a2.firstName=%s  a2.lastName=%s \n", a2.firstName,a2.lastName)
   a2.setAnimalNamePoint("jerry","williams") // 修改a2的firstName和lastName
   // print 修改后:a2.firstName=jerry  a2.lastName=williams 
   fmt.Printf("修改后:a2.firstName=%s  a2.lastName=%s \n", a2.firstName,a2.lastName)
}

如上示例,我们声明了animal结构体及其嵌套结构体animalName,然后声明了animalName结构体类型的两个方法:setAnimalName(值类型),setAnimalNamePoint(指针类型)。

调用setAnimalName方法修改a1的firstName和lastName,通过打印信息可以看出a1的firstName、lastName两个字段的值并没有被修改。

调用setAnimalNamePoint方法修改a2的firstName和lastName,通过打印信息可以看出a2的firstName、lastName两个字段的值被成功修改。

我们的例子中firstName和lastName都是string类型,如果是引用类型的话两种情况下值都会被修改。大家可以自行动手测试下。

细心的读者可能已经发现了,a2是值类型但是setAnimalNamePoint属于指针类型的方法,怎么还能调用成功呢?

这是因为Go语言帮我们做了自动转译,让我们通过值也可以调用指针类型的方法。上面例子中的a2.setAnimalNamePoint("jerry","williams") 就等价于(&a2).setAnimalNamePoint("jerry","williams")

我们何时使用值类型的方法何时使用指针类型的方法呢?

  1. 如果我们希望调用方法的对象本身也需要被改变时,我们可以考虑使用指针方法。
  2. 当类型特别复杂时我们为了防止过大的值拷贝,也可以使用指针方法。

其它情况可以使用值方法。

值类型和指针类型的自动转换

自定义数据类型的值仅仅包含了它所有的值方法,但是自定义数据类型的指针类型既包括了值方法,又包括了指针方法。因为调用指针方法时Go语言对值类型做了自动转译,所以这里不好举例验证,后面讲到接口时我们再举例证明。

总结

本文我们主要介绍了如何声明方法,方法的基本用法,方法和函数的区别和联系,值方法和指针方法的关系等内容。结构体和方法在Go语言中起到了类似其它语言类的概念,所以我们可以说Go语言是支持面向对象思维的编程语言。

诚邀关注公众号:一行舟

每周更新技术文章,和大家一起进步。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何在SpringBoot启动时执行初始化操作,两个简单接口

发表于 2021-11-17

听说微信搜索《Java鱼仔》会变更强哦!

本文收录于github和gitee ,里面有我完整的Java系列文章,学习或面试都可以看看哦

(一)概述

最近遇到一个功能点,数据库中一张很简单的表有一千多条数据,这里的数据主要做到了值域映射的作用,简单来讲就是我可以通过中文名拿到数据库中对应的code值。原本的实现方式是每次用到之后去查一次sql,虽然不会有什么问题,但是只要是走了网络io,都会消耗时间。所以这个方案需要想办法优化。

优化的方式其实很简单,数据量不多,一千多条数据放在内存里也占不了多少空间。因此完全可以把一次性把数据加载到内存中,后面只需要每次去内存里调用就可以了。

(二)实现方案

方案想好了就要想实现方式了,想个最直接的方案,在Spring容器初始化时就把这些数据从数据库拿到内存中,后面就直接调用。

SpringBoot中有两个接口能实现该功能:CommandLineRunner和ApplicationRunner。

2.1 CommandLineRunner

首先了解一下CommandLineRunner的基本用法,CommandLineRunner可以在系统启动后执行里面的run方法

1
2
3
4
5
6
7
java复制代码@Component
public class DataPrepare implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("CommandLineRunner执行数据初始化");
}
}

如果有多个类的话也可以通过@Order注解指定每个类的执行顺序。

接着就可以写代码的实现了,首先定义一个类用来将Mysql的数据存到内存里,通过静态的Map存储

1
2
3
4
5
6
7
8
9
java复制代码public class DataMap {
public static Map<String, String> map = new HashMap<>();
public static void putData(String key, String value) {
map.put(key, value);
}
public static String getDataByKey(String key) {
return map.get(key);
}
}

接着在DataPrepare类中将数据都存入到静态到Map中。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Component
public class DataPrepare implements CommandLineRunner {
@Autowired
private DataMapper dataMapper;
@Override
public void run(String... args) throws Exception {
//从数据库中取数据
List<DataDO> dataDOS = dataMapper.selectList(Wrappers.emptyWrapper());
//写入到DataMap中
dataDOS.forEach(item -> DataMap.putData(item.getName(), item.getCode()));
}
}

要使用到时候,只需要调用DataMap.getDataByKey()方法就可以直接使用了。

2.2 ApplicationRunner

ApplicationRunner和CommandLineRunner的功能十分相似,实现方式也基本相同。同样继承接口,并实现接口的run方法。

1
2
3
4
5
6
7
java复制代码@Component
public class ApplicationDataPrepare implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("ApplicationRunner执行数据初始化");
}
}

在不指定@Order注解的情况下,ApplicationRunner会优先于CommandLineRunner执行。

两者的区别:

CommandLineRunner和ApplicationRunner的功能几乎是相同的,最大的区别在于两者run方法中的入参有所不同,CommandLineRunner通过String数组
来接收启动参数,而ApplicationRunner通过一个ApplicationArguments对象来接收。

在使用时,不管是String数组还是ApplicationArguments都可以拿到JVM的启动参数。

(三)源码分析

为什么通过实现一个接口,重写run方法就能达到启动程序后就自动执行代码的功能呢?我们可以通过SpringBoot的源码去看:

点进SpringApplication.run()方法,一直进入到public ConfigurableApplicationContext run(String… args)方法中,在执行完一系列初始化方法之后,执行了this.callRunners(context, applicationArguments)方法

callRunners的方法比较简单,首先定义了一个runners集合,并将需要执行的Bean放进去。可以看到ApplicationRunner和CommandLineRunner在这里被放入了runners中,接着对Order注解进行排序,最后遍历执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
Iterator var4 = (new LinkedHashSet(runners)).iterator();

while(var4.hasNext()) {
Object runner = var4.next();
if (runner instanceof ApplicationRunner) {
this.callRunner((ApplicationRunner)runner, args);
}

if (runner instanceof CommandLineRunner) {
this.callRunner((CommandLineRunner)runner, args);
}
}

}

(四)总结

一个小小的细节可以节约多次的Sql调用。本章主要通过一个简单的例子引出ApplicationRunner和CommandLineRunner,实际在使用时也可以通过懒加载,在第一次使用时将数据塞到静态的Map里,也能实现类似缓存的效果。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spark streaming 输出数据到redis

发表于 2021-11-16

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」。

上次使用了spark streaming读取redis中的数据并进行处理。这次解决一下输出的问题。最简单的输出方式是使用计算的结果对象上自带的print函数,输出到运行的屏幕上。但是我运行的时候使用的是远程的分布式环境。然后用程序推送运行的。因此需要一个比较方便查看运行状况的方式。

尽管spark streaming 额外还提供了一些像saveAsTextFiles这种保存数据为文件或者其他东西的方式,但没有想象中方便。

除了这些之外,spark streaming提供了一个可以用来自定义输出方式的函数foreachRDD。

出于使用简便的目的考虑。我决定使用redis的发布订阅机制,把数据推送到redis,由redis对外广播,后续用其他程序读取这个数据。也就是类似总线的机制。如果这里使用的是kafka这种重量级选手的话,它对数据做持久化之后可以让数据被随时拿出,不像redis的发布订阅只能即时性的使用。不过只看自定义输出这一部分的话,操作原理是一致的。

foreachRDD是基于rdd的一个操作,行为表现与我们在计算操作的部分基本是一致的,这也意味着一个很经典的情况:rdd是分布式的运算。它需要在在不同的机器上运行相同的程序。假设我们给这个函数传递的函数里直接创建redis连接的话,会出现一个序列化错误,因为它需要把这个连接对象序列化之后发送给计算程序,这点无法实现。

同样值得注意的是,如果我们在对rdd中的每个计算结果创建redis连接对象也是不推荐的。虽然这样不会产生先前说的那种序列化的问题,但是每一个记录都建立连接对象这会产生大量的冗余的开销。

合适的做法是利用rdd的分布式特性,rdd使用的也是经典的分区计算模型,每个节点计算的部分数据处于分区中,一般来讲,对于这一个分区的数据,我们建立一个连接对象已经是可以接受的了。我们可以像下面这样完成到redis的数据发送。

1
2
3
4
5
6
7
8
9
scala复制代码behaviorCount.foreachRDD { (rdd) =>
rdd.foreachPartition { partitionOfRecords =>
val jedis = new Jedis("master", 47777)
partitionOfRecords.foreach(it => {
jedis.publish("channel", data string of it)
})
jedis.close()
}
}

如果追求更近一步效率,还可以使用连接池技术,从连接池中获取连接,进一步降低开销。

还有一个问题则是如何整合数据,分辨数据的计算时间点。这里也可以利用foreachRDD函数。因为它实际上可以有两个参数,在rdd之后还可以接一个表示时间戳的变量,然后同样发送到redis,让其他程序来处理。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

带你了解下SynchronousQueue(并发队列专题)

发表于 2021-11-16

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

写在前面

前面我们写了延时队列,今天我们来唠唠SynchronousQueue,Sync,也叫同步队列。我还给他起了另外一个名字叫“配对”队列,具体为什么叫“配对”队列,下面我们具体说明

多说一句,欢迎大家点击头像查看专题,设计模式专题已完结,目前正在进行的是队列专题和Security专题。

回顾队列

阻塞队列可分为以下几种:

image.png

阻塞队列(Blocking Queue)提供了可阻塞的 put 和 take 方法,它们与可定时的 offer 和 poll 是等价的。如果队列满了 put 方法会被阻塞等到有空间可用再将元素插入;如果队列是空的,那么take 方法也会阻塞,直到有元素可用。当队列永远不会被充满时,put 方法和 take 方法就永远不会阻塞。

理解SynchronousQueue

SynchronousQueue,实际上它不是一个真正的队列,因为SynchronousQueue没有容量。与其他BlockingQueue(阻塞队列)不同,SynchronousQueue是一个不存储元素的BlockingQueue。只是它维护一组线程,这些线程在等待着把元素加入或移出队列。

我们简单分为以下几种特点:

  • 内部没有存储(容量为0)
  • 阻塞队列(也是blockingqueue的一个实现)
  • 发送或者消费线程会阻塞,只有有一对消费和发送线程匹配上,才同时退出。
  • 配对有公平模式和非公平模式(默认)

为了演示我们上面所分析的特么 下面根据实例代码演示下:

代码案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码/**
* SynchronousQueue队列演示demo
* @Date 2021/11/16 11:10 下午
* @Author yn
*/
public class SynchronousQueueDemo {

public static void main(String[] args) {
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t 入队列 1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName() + "\t 入队列 2");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName() + "\t 入队列 3");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "AAAAA").start();

new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "\t 出队列 " + synchronousQueue.take());
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "\t 出队列 " + synchronousQueue.take());
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "\t 出队列 " + synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "BBBBB").start();
}
}

看下控制台打印

1
2
3
4
5
6
7
8
java复制代码AAAAA	 入队列 1
BBBBB 出队列 1

AAAAA 入队列 2
BBBBB 出队列 2

AAAAA 入队列 3
BBBBB 出队列 3

看到没,这个就是开头所说“配对”队列,也就是验证了入和出必须是配对的。否则阻塞。

所以说这是一个很有意思的阻塞队列,其中每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此此队列内部其 实没有任何一个元素,因此不能调用peek操作,因为只有移除元素时才有元素。

我们接下来看下它的应用场景

应用场景

我们拿newCachedThreadPool线程池来了解下

1
2
3
4
5
6
java复制代码public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}

SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

由于ThreadPoolExecutor内部实现任务提交的时候调用的是工作队列(BlockingQueue接口的实现类)的非阻塞式入队列方法(offer方法),因此,在使用SynchronousQueue作为工作队列的前提下,客户端代码向线程池提交任务时,而线程池中又没有空闲的线程能够从SynchronousQueue队列实例中取一个任务,那么相应的offer方法调用就会失败(即任务没有被存入工作队列)。

此时,ThreadPoolExecutor会新建一个新的工作者线程用于对这个入队列失败的任务进行处理(假设此时线程池的大小还未达到其最大线程池大小)。

是不是设计的贼精彩。我们可以去里面跟一下代码

简单看下源码

开头说过,SynchronousQueue 与其他BlockingQueue一样,SynchronousQueue同样继承AbstractQueue和实现BlockingQueue接口:

image.png

SynchronousQueue提供了两个构造函数:

image.png

1
2
3
4
5
java复制代码public SynchronousQueue(boolean fair) {
// 通过 fair 值来决定公平性和非公平性
// 公平性使用TransferQueue,非公平性采用TransferStack
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

TransferQueue、TransferStack继承Transferer,Transferer为SynchronousQueue的内部类,它提供了一个方法transfer(),该方法定义了转移数据的规范,如下:

1
2
3
java复制代码abstract static class Transferer<E> {
abstract E transfer(E e, boolean timed, long nanos);
}

transfer()方法主要用来完成转移数据的,如果e != null,相当于将一个数据交给消费者,如果e == null,则相当于从一个生产者接收一个消费者交出的数据。

好的 今天的同步队列SynchronousQueue 就先讲到这里,我们简单做下总结

总结

使用SynchronousQueue的目的就是保证“对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务”。

SynchronousQueue也是blockingqueue的一个实现,内部采用的就是ArrayBlockingQueue的阻塞原语,所以在功能上完全可以用ArrayBlockingQueue替换之,但是SynchronousQueue 是轻量级的,SynchronousQueue 不具有任何内部容量,甚至不具有一的容量,我们可以用来在线程间安全的交换单一元素。所以功能比较单一,优势应该就在于轻量吧~

好的 关于队列的学习我们下期再见,加油!!!

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

也欢迎点击下面队列专题进行关注。我们一起学习

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python面向对象编程02:深度认识类class 类中的其

发表于 2021-11-16

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」

正式的Python专栏第37篇,同学站住,别错过这个从0开始的文章!

前篇学委展示分享了面向对象编程的概念,相信读者们对这个类和对象比较熟悉了。

我们在深入看一下类的结构。

类中的其他内置函数/属性

前文代码展示了‘__init__‘函数,这个是类的内置函数,默认不写就没有执行多余操作。

在Python中类这种结构还包含了下面的一些内置函数属性:

  • __name__ 类名字
  • __dict__ 类的命名空间字典(这里先不做多解释,其实这个解释虽然抽象但是准确的,下面看代码就能秒懂)
  • __doc__ 类的文档说明
  • __module__ 类的文档说明
  • __bases__ 类的文档说明

看起来比较枯燥无味的样子,除了类的名字,我们看起来好像应该是类的名字(好像废话)

我们还是看一下代码感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/15 11:58 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : __init__.py.py
# @Project : hello

"""
下面是一个学生类定义
"""


class student(object):
"""这里是一个学生类定义"""

def __init__(self, name):
self.name = name

def get_name(self):
return self.name

def set_name(self, name):
self.name = name

def study(self):
print(f"{self.name} : 好好学习,天天向上!")


stu01 = student("学委粉丝【小白】")
print(stu01)
print(stu01.get_name())
print(stu01.study())

print("类名:", student.__name__)
print("类文档:", student.__doc__)
print("类的命名空间:", student.__dict__)
print("类的基类:", student.__bases__)
print("类所在的模块:", student.__module__)

下面是运行效果:

屏幕快照 2021-11-16 下午11.54.59.png

好了,我们看到类的命名空间打印出来dict字典,内放置的内容的key都是类的成员(函数,属性),这下子清楚了:Python用一个dict来说描述类的结构!(这就是学委前篇提到的一张网,它把单一的函数,数据属性,挂到这个网上,组成了类的结构)。

其他几个内置类函数非常好理解。

另一个地方是’__bases__‘函数,它告诉我们student类是在‘object’这个类的基础上创建的。(确实如此!)

内置函数获取其对象属性

python提供了下面的一些函数:

  • getattr(对象,属性名,[可选默认值]) : 获取对象的某个属性,如果没有找到返回默认值
  • setattr(对象,属性,属性值): 设置对象的,添加/覆盖某个属性为属性值。
  • hasattr(对象,属性): 判断对象是否包含某个属性

这个非常直白。

Java同学都知道反射,比较繁琐的。但是在Python中这块做的非常简洁,这个跟__dict__的设计很精巧.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/15 11:58 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : student4_1.py
# @Project : hello

"""
下面是一个学生类定义
"""


class Student(object):
"""这里是一个学生类定义"""

def __init__(self, name):
self.name = name

def get_name(self):
return self.name

def set_name(self, name):
self.name = name

def study(self):
print(f"{self.name} : 好好学习,天天向上!")


stu01 = Student("学委粉丝【小白】")
print("student :", stu01)
print("student name:", stu01.get_name())
print("student study:", stu01.study())

stu02 = Student("学委粉丝【小花】")
print("student2 name:", stu02.get_name())

print("-" * 16)
print("getattr student name: ", getattr(stu01, 'name'))
# print("getattr: ", getattr(stu01, 'name2')) # AttributeError: 'Student' object has no attribute 'name2'
if hasattr(stu01, 'name2'):
print("student %s has attribute name1 " % stu01.name)
else:
print("student %s do not have attribute name2 " % stu01.name)
print("getattr name2: ", getattr(stu01, 'name2', 'defaultName')) # 没有但是我们可以给一个default
# print(stu01.name1) # getattr并没有办法把属性设置到stu01对象

setattr(stu01, 'name2', '小白别名')
print("student name2", stu01.name2)

# 学委温馨提示,setattr很强大可以覆盖原来的对象属性
setattr(stu01, 'study', '小白躺平了,今天休息')
print("student study:", stu01.study)

上面的代码学委展示了获取修改属性。以及根据对象是否包含属性,进行setattr。

读者多看一看,特别是代码附加的注释,加深对类和对象的理解。

屏幕快照 2021-12-03 上午9.30.39.png

既然说到了base类,再谈谈类的继承

上面的bases函数返回值的解释,换一句话说:

除了object类,其他所有类默认的都是直接或者间接的继承于object类的;
也就是说,object类是所有的类的根,源头类。

A类继承于B类,那么,我们会使用下面的描述来表示它们:

A类是B类的子类

B类是A类的父类

这个就跟家族族谱一样,最顶上那个第一代元祖父,这就是类和object的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
python复制代码#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/11/15 11:58 下午
# @Author : LeiXueWei
# @CSDN/Juejin/Wechat: 雷学委
# @XueWeiTag: CodingDemo
# @File : __init__.py.py
# @Project : hello

"""
下面是一个学生类定义
"""


class Programmer():
pass


print("programmer类的命名空间:", Programmer.__dict__)
print("programmer类的命名空间:", Programmer.__bases__)


class Student(object):
"""这里是一个学生类定义"""

def __init__(self, name):
self.name = name

def get_name(self):
return self.name

def set_name(self, name):
self.name = name

def study(self):
print(f"{self.name} : 好好学习,天天向上!")


s1 = Student("学委粉丝【小白】")
print(s1)
print(s1.get_name())
print(s1.study())

print("类名:", Student.__name__)
print("类文档:", Student.__doc__)
print("类的命名空间:", Student.__dict__)
print("类的基类:", Student.__bases__)
print("类所在的模块:", Student.__module__)




class PrimarySchoolStudent(Student):
pass


print("*" * 16)
print("PrimarySchoolStudent类的命名空间:", PrimarySchoolStudent.__dict__)
print("PrimarySchoolStudent类的基类:", PrimarySchoolStudent.__bases__)
print("PrimarySchoolStudent类所在的模块:", PrimarySchoolStudent.__module__)
PrimarySchoolStudent("主要学生").study()

这段代码想要展示的信息很多。

首先是在前面第一段代码后多加了一个Developer类(没有写继承)。

输出我们看到他说继承object类的(所以写跟不写没有差别,这里要注意python版本,学委用的是3.8)

其次,我们看到PirimarySchoolStudent的bases函数的输出是Student,没错,继承某个类之后的base值就为该类,也就是Student

再次,继承让子类直接拥有了父类的函数和数据属性,可以直接访问拿来用。

好了,请仔细看看下面的运行结果:

屏幕快照 2021-11-17 上午12.17.35.png

总结

这次把类和对象的知识补充了一些(但是还有Python支持多继承,这跟Java是很大的区别!),有过半不是常见的。

但是在写框架或者深度分析的时候,非常有用,务必过一过熟悉一下。

可以看看下图,回想一下上面的代码,这就是类命名空间,数据属性,函数属性,模块之间的关系:

屏幕快照 2021-12-03 下午10.58.22.png

我们可以通过命名空间找到函数属性,它们有关联。

此外,获取数据属性只能通过对象应用getattr来获取。

学委写了十几年的Java了,但是写的这套Python教程非常务实,对基础编程有任何问题请查看相关文章。

喜欢Python的朋友,请关注学委的 Python基础专栏 or Python入门到精通大专栏

持续学习持续开发,我是雷学委!

编程很有趣,关键是把技术搞透彻讲明白。

欢迎关注微信,点赞支持收藏!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…317318319…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%