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

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


  • 首页

  • 归档

  • 搜索

微服务设计学习(一)关于微服务和如何建模服务

发表于 2019-10-07

undefined

前言

随着互联网在21世纪初被大规模接入,互联网由基于流量点击赢利的单方面信息发布的Web 1.0业务模式,转变为由用户主导而生成内容的Web 2.0业务模式。因此,互联网应用系统所需处理的访问量和数据量均疾速增长,后端技术架构也因此面临着巨大的挑战。

Web 2.0阶段的互联网后端架构大多经历了由All in One的单体式应用架构渐渐转为更加灵活的分布式应用架构的过程,互联网开发架构开始追求更高的质量和效率。

随着智能手机的出现以及4G标准的普及,互联网应用由PC端迅速转向更加自由的移动端。移动设备由于携带方便且便于定位,因此在出行、网络购物、支付等方面彻底改变了现代人的生活方式。在技术方面,为了应对更加庞大的集群规模,单纯的分布式系统已经难于驾驭,因此技术圈开启了一个概念爆发的时代——SOA、DevOps、容器、CI/CD、微服务、Service Mesh等概念层出不穷,而Docker、Kubernetes、Mesos、Spring Cloud、gRPC、Istio等一系列产品的出现,标志着云时代已真正到来。

本文(或者说本系列文章),是本人在阅读完 Sam Newman 的《微服务设计》一书之后,与其他的微服务设计相关文章、《从服务化到云原生》等书籍进行关联阅读后做的笔记总结。

目的是构建分布式、微服务、云原生方面的体系化的知识结构树。

希望巩固学习的同时能够帮助到你。

一、关于微服务

1.1 什么是微服务

微服务就是一些协同工作的小而自治的服务。

关键词: 小而自治

“小”

“小”这个概念,一方面体现在微服务的内聚性上。

  • 内聚性也可以称之为单一职责原则:“把因相同原因而变化的东西聚合到一起,而把因不同原因而变化的东西分离开来。”
  • 也就是说,微服务应该专注于做好一件事情。
  • 由业务边界来确定服务的边界

另一方面体现在代码库的大小,这里有几个参考的标准或者说原则

  • 代码库小到团队结构相匹配
  • 代码库小到易于迅速重写
  • 辩证的看待。服务越小,微服务架构的优点和缺点也就越明显

自治

“自治”这个概念,强调的是,一个微服务就是一个独立的实体。体现在服务之间的松耦合上。

  • 黄金法则:你是否能够修改一个服务并对其进行部署,而不影响其他任何服务?

关键点:要学会正确的建模服务,正确的设计服务API,才能做到上述两点。

1.2 微服务的好处

微服务的大多好处都适用于分布式系统架构,只不过微服务会将这些好处推向极致

  • 技术异构性
    • 不同服务根据业务场景、性能要求、功能需求采用不同的技术架构
    • 新技术的快速实践,技术团队的快速成长
  • 弹性/反脆弱性(anti-fragility)
    • 服务降级
    • 服务容灾、服务熔断
  • 扩展
    • 传统单体系统,无法做到对局部功能进行扩展
    • 根据具体的业务需求,对特定的微服务进行扩展
    • 通过架构的手段,节省成本
  • 简化部署
    • 传统单体应用,即使是一行代码修改,也需要整体重新部署。风险太大。
    • 微服务架构,各个服务部署相互独立
      • 灵活的发版方式
      • 快速回滚
      • 风险小
  • 架构与组织结构相匹配
    • 关联知识点:康威定律
  • 服务的可重用、可组合
  • 服务的可替代性,快速重写

1.3 面向服务的架构

SOA(Service-Oriented Architecture,面向服务的架构)是一种设计方法,其中包含多个服务,而服务之间通过配合最终会提供一系列功能。一个服务通常以独立的形式存在于操作系统进程中。服务之间通过网络调用,而非采用进程内调用的方式进行通信。

就像认为 XP 或者 Scrum 是敏捷软件开发的一种特定方法一样,微服务架构是 SOA 的一种特定方法。

实施SOA会遇到的问题:

  • 通信协议(SOAP or REST)
  • 第三方中间件选型
  • 服务粒度划分
  • ……

思考与小结

微服务确实有许多优点:“反脆弱性(anti-fragility)”、容错、独立部署与扩展、架构抽象、技术隔离。但并不是说采用了微服务就自然地具备了这些特性。

比如,要具备反脆弱性,需要充分考虑分布式系统的不确定性,清楚异步、网络划分、节点故障、平衡可用性与数据一致性等问题。

同样地,要具备可维护性和可扩展性,首先要有恰当的基础设施和组织结构。

理论上讲,微服务可以提高开发速度,但在创建组织依赖时,“微服务佣金(MicroservicePremium)”可能会降低开发速度。

所以,采用微服务架构需要具备一些先决条件,包括恰当的持续发布管道、能胜任的DevOps 和Ops 团队、审慎的服务边界等等。此外,周密的测试和集成模式也很重要。

就书中这章最后一讲说的:微服务不是银弹,你需要在部署、测试和监控等方面做很多的工作。你还需要考虑如何扩展系统、并且保证他们的弹性,甚至还需要处理类似分布式事务、CAP相关的问题。

我觉得,这也是为什么服务化到云原生是大势所趋,因为只有结合“容器 +编排调度”的云平台,微服务才能将自己的优点发挥到最大。至于云原生的知识,又是后面的内容了。

引申阅读 Martin Fowler大神的文章

微服务佣金 www.martinfowler.com/bliki/Micro…

二、如何建模服务

还是要重申两个重要概念,高内聚和松耦合,对应前面的关键词:小而自治。

2.1 几个概念

限界上下文

此处作者引用了 Eric Evans 的著作《领域驱动设计》中的概念(这本书强烈建议阅读):限界上下文。

任何一个给定的领域都包含多个限界上下文,每个限界上下文中的东西(Eric 更常使用模型这个词,应该比“东西”好得多)分成两部分,一部分不需要与外部通信,另一部分则需要。每个上下文都有明确的接口,该接口决定了它会暴露哪些模型给其他的上下文。
使用细胞作为比喻:“细胞之所以会存在,是因为细胞膜定义了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。”

共享模型和隐藏模型

同时又提到共享模型和隐藏模型的概念。

共享模型就是上述比喻中上下文之间交互的模型,隐藏模型就是不需要与外部进行交互的模型。

image.png


关于如何开始微服务,作者在书中的观点和同在TW的Martin Fowler是一个观点:“MonolithFirst” - 单体应用先行。

undefined

主要出于以下考虑:

  • Yagni 原则——确定软件思路是否有用,最好的方法是创建一个简化版本,然后看它的使用效果。在这个阶段,最重要的是速度,而该原则可缩短反馈周期,避免微服务佣金。
  • 明确的有界上下文集合——只有服务存在良好且稳定的边界,微服务才能有效地发挥作用。但是,任何微服务间的功能重构都比在单体架构中难度大,即使是经验丰富的架构师在自己熟悉的领域里,也很难在一开始就恰当地定义出边界,而首先构建一个单体应用有利于明确功能边界。

引申阅读 www.martinfowler.com/bliki/Monol…

当然这和他们所处的公司业务环境肯定有很大的关系,ThoughtWorks 是一家帮助其他公司解决问题的顾问公司,也就是说,他们会在某个公司的单体应用出现问题时提供帮助,将其重构为微服务架构。得出这样的结论也无可厚非。业界关于这个也有其他的声音,比如直接就从微服务开始。

我本人是比较倾向于这种方式,所以对这种方式进行了摘录和学习。

2.2 建模要点

  • 切忌过早划分

过早将一个系统划分成为微服务的代价非常高,尤其是在面对新领域时。很多时候,将一个已有的代码库划分成微服务,要比从头开始构建微服务简单得多。

  • 确定限界上下文

使用模块对限界上下文进行建模,同时使用共享和隐藏模型。

  • 从业务功能入手

思考限界上下文的时候,应该从业务功能入手,首先要问自己“这个上下文是做什么用的”,然后再考虑“它需要什么样的数据”。

  • 逐步划分上下文

当考虑微服务的边界时,首先考虑比较大的、粗粒度的那些上下文,然后当发现合适的缝隙后,再进一步划分出那些嵌套的上下文。

+ 嵌套上下文

image.png

  • 顶层上下文

image.png

思考与总结

如何在问题空间中寻找能达到高内聚低耦合的接缝。限界上下文是寻找这些接缝的一个非常重要的工具,通过将微服务与这些边界相匹配,可以保证最终的系统能够得到微服务提供的所有好处。

这一部分可以关联学习DDD理念相关书籍。

本文转载自: 掘金

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

死磕 java线程系列之创建线程的8种方式

发表于 2019-10-07

(手机横屏看源码更方便)

问题

(1)创建线程有哪几种方式?

(2)它们分别有什么运用场景?

简介

创建线程,是多线程编程中最基本的操作,彤哥总结了一下,大概有8种创建线程的方式,你知道吗?

继承Thread类并重写run()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
haxe复制代码public class CreatingThread01 extends Thread {

@Override
public void run() {
System.out.println(getName() + " is running");
}

public static void main(String[] args) {
new CreatingThread01().start();
new CreatingThread01().start();
new CreatingThread01().start();
new CreatingThread01().start();
}
}

继承Thread类并重写run()方法,这种方式的弊端是一个类只能继承一个父类,如果这个类本身已经继承了其它类,就不能使用这种方式了。

实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
haxe复制代码public class CreatingThread02 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}

public static void main(String[] args) {
new Thread(new CreatingThread02()).start();
new Thread(new CreatingThread02()).start();
new Thread(new CreatingThread02()).start();
new Thread(new CreatingThread02()).start();
}
}

实现Runnable接口,这种方式的好处是一个类可以实现多个接口,不影响其继承体系。

匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typescript复制代码public class CreatingThread03 {
public static void main(String[] args) {
// Thread匿名类,重写Thread的run()方法
new Thread() {
@Override
public void run() {
System.out.println(getName() + " is running");
}
}.start();

// Runnable匿名类,实现其run()方法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}).start();

// 同上,使用lambda表达式函数式编程
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " is running");
}).start();
}
}

使用匿名类的方式,一是重写Thread的run()方法,二是传入Runnable的匿名类,三是使用lambda方式,现在一般使用第三种(java8+),简单快捷。

实现Callabe接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gradle复制代码public class CreatingThread04 implements Callable<Long> {
@Override
public Long call() throws Exception {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId() + " is running");
return Thread.currentThread().getId();
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Long> task = new FutureTask<>(new CreatingThread04());
new Thread(task).start();
System.out.println("等待完成任务");
Long result = task.get();
System.out.println("任务结果:" + result);
}
}

实现Callabe接口,可以获取线程执行的结果,FutureTask实际上实现了Runnable接口。

定时器(java.util.Timer)

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public class CreatingThread05 {
public static void main(String[] args) {
Timer timer = new Timer();
// 每隔1秒执行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}, 0 , 1000);
}
}

使用定时器java.util.Timer可以快速地实现定时任务,TimerTask实际上实现了Runnable接口。

线程池

1
2
3
4
5
6
7
8
livescript复制代码public class CreatingThread06 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
threadPool.execute(()-> System.out.println(Thread.currentThread().getName() + " is running"));
}
}
}

使用线程池的方式,可以复用线程,节约系统资源。

并行计算(Java8+)

1
2
3
4
5
6
7
8
9
10
11
reasonml复制代码public class CreatingThread07 {

public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 串行,打印结果为12345
list.stream().forEach(System.out::print);
System.out.println();
// 并行,打印结果随机,比如35214
list.parallelStream().forEach(System.out::print);
}
}

使用并行计算的方式,可以提高程序运行的效率,多线程并行执行。

Spring异步方法

首先,springboot启动类加上@EnableAsync注解(@EnableAsync是spring支持的,这里方便举例使用springboot)。

1
2
3
4
5
6
7
less复制代码@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

其次,方法加上@Async注解。

1
2
3
4
5
6
7
8
typescript复制代码@Service
public class CreatingThread08Service {

@Async
public void call() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}

然后,测试用例直接跟使用一般的Service方法一模一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class CreatingThread08Test {

@Autowired
private CreatingThread08Service creatingThread08Service;

@Test
public void test() {
creatingThread08Service.call();
creatingThread08Service.call();
creatingThread08Service.call();
creatingThread08Service.call();
}
}

运行结果如下:

1
2
3
4
ada复制代码task-3 is running
task-2 is running
task-1 is running
task-4 is running

可以看到每次执行方法时使用的线程都不一样。

使用Spring异步方法的方式,可以说是相当地方便,适用于前后逻辑不相关联的适合用异步调用的一些方法,比如发送短信的功能。

总结

(1)继承Thread类并重写run()方法;

(2)实现Runnable接口;

(3)匿名内部类;

(4)实现Callabe接口;

(5)定时器(java.util.Timer);

(6)线程池;

(7)并行计算(Java8+);

(8)Spring异步方法;

彩蛋

上面介绍了那么多创建线程的方式,其实本质上就两种,一种是继承Thread类并重写其run()方法,一种是实现Runnable接口的run()方法,那么它们之间到底有什么联系呢?

请看下面的例子,同时继承Thread并实现Runnable接口,应该输出什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
livescript复制代码public class CreatingThread09 {

public static void main(String[] args) {
new Thread(()-> {
System.out.println("Runnable: " + Thread.currentThread().getName());
}) {
@Override
public void run() {
System.out.println("Thread: " + getName());
}
}.start();
}
}

说到这里,我们有必要看一下Thread类的源码:

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
aspectj复制代码public class Thread implements Runnable {
// Thread维护了一个Runnable的实例
private Runnable target;

public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ...
// 构造方法传进来的Runnable会赋值给target
this.target = target;
// ...
}

@Override
public void run() {
// Thread默认的run()方法,如果target不为空,会执行target的run()方法
if (target != null) {
target.run();
}
}
}

看到这里是不是豁然开朗呢?既然上面的例子同时继承Thread并实现了Runnable接口,根据源码,实际上相当于重写了Thread的run()方法,在Thread的run()方法时实际上跟target都没有关系了。

所以,上面的例子输出结果为Thread: Thread-0,只输出重写Thread的run()方法中的内容。

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

qrcode

本文转载自: 掘金

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

Java SPI 机制及其实现 前言 ServiceLoad

发表于 2019-10-06
  • 前言
  • ServiceLoader
  • JDBC 中的使用
  • 更多的使用场景
  • 实现原理
  • SpringFactoriesLoader
  • 结语

前言

第一次接触 SPI 是在看《Java 核心计算卷》中 JDBC 相关的章节的时候,当时看到说在高版本的 JDBC 中可以省略通过 Class.forName 加载驱动这一步, 因为高版本的 JDBC 可以通过 SPI 机制自动加载注册驱动。

当时看到的时候感觉很惊喜,终于不用写那又臭又长的 try-catch 了。

后来在阅读源码的过程中又发现 Spring 中也实现了类似于 Java SPI 机制的功能,研究了一下后发现 SPI 机制无论是在使用上还是在实现上,都是很简单的。

所以,我觉得,可以整一篇博客总结一下。

隐藏内容
上一次写博客还是 6 月 22 号,断更了 100 多天,感觉有点手生 @_@

ServiceLoader

SPI 的全称为 (Service Provider Interface),是 JDK 内置的一种服务提供发现机制。主要由工具类 java.util.ServiceLoader 提供相应的支持。

其中的两个主要角色为:

  • Service - 服务,通常为一个接口或一个抽象类,具体类虽然也可以,但是一般不建议那样做
  • Service Provider - 服务提供者,服务的具体实现类

使用时,需要在 META-INF/services 下创建和服务的 全限定名 相同的文件,然后在该文件中写入 服务提供者 的全限定名,可以用 # 作为注释。比如说, 我们可以在文件 mysql-connector-java/META-INF/services/java.sql.Driver 中发现如下内容:

1
2
复制代码com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

然后,就可以通过 ServiceLoader 来获取这些服务提供者。由于 ServiceLoader 并没有提供直接获取服务提供者的方法,因此,只能通过迭代的方式获取:

1
2
3
4
5
复制代码ServiceLoader<Service> loader = ServiceLoader.load(Service.class);

for (Service service : loader) {
// ...
}

可以看到,ServiceLoader 的使用还是很简单的,更多的和 ServiceLoader 相关的内容可以看一下官方文档:ServiceLoader (Java Platform SE 8 )

JDBC 中的使用

如果要找一个使用了 SPI 机制的例子的话,最直接的就是 JDBC 中通过 SPI 的方式加载驱动了,这里可以看一下 JDBC 的使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码public class DriverManager {
static {
loadInitialDrivers();
}

private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
}
}

通过上面的简化过后的代码可以发现,在加载 DriverManager 这个类的时候就会通过静态初始化代码块调用执行 loadInitialDrivers 方法,而这个方法会通过 ServiceLoader 加载所有的 Driver 提供者。

而在相应的 Driver 提供类中,比如类 com.mysql.jdbc.Driver 中就存在如下形式的代码:

1
2
3
4
5
6
7
复制代码static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

是不是很简单?加载 DriverManager 的时候通过 SPI 机制加载各个 Driver,然后各个 Driver 又在它们自己的静态初始化代码块中将自己注册到 DriverManager。

更多的使用场景

通过 JDBC 中 SPI 机制的使用可以发现,要使用 SPI 的话还是很简单的,那么,我们可以在什么地方使用 SPI 呢?

由于 SPI 机制的限制,单个 ServiceLoader 只能加载单个类型的 Service,同时还必须创建相应的文件放到 META-INF/services 目录下,因此,使用场景最好就是类似 JDBC 中这种, 可以通过单个对象来访问其他服务提供者的场景,即:可以使用 门面模式 的场景。

比如说,现在 Java 中存在不少常用的 JSON 库,比如 Gson、FastJSON、Jackson 等,这些库在使用时都可以通过简单的封装来满足大部分的需求,那么, 我们就可以考虑通过 SPI 机制来实现一个这些 JSON 库的门面,将 JSON 的处理下放到 Service Provider 来完成,而我们通过门面来使用这些服务。

这样一来,我们一方面可以提供自己的默认实现,也可以留出扩展的接口,也就不需要自己手动去加载那些实现了。

实现原理

SPI 不仅在使用上很简单,它的实现原理也很简单,关键就在 ClassLoader.getResources 这个方法上,SPI 加载服务的方式就是通过 ClassLoader.getResources 方法找到 META-INF/services 目录下的相应文件, 然后解析文件得到服务提供者的类名。

最后通过 Class.forName() -> clazz.newInstance() 得到实例返回。

非常简单且直白的实现方式,比较值得注意的就是 ClassLoader.getResources 方法的使用了,比如,你可以在一个 Spring 项目下执行如下代码:

1
2
3
4
5
6
7
8
复制代码public class Test {
public static void main(String[] args) throws Exception {
Enumeration<URL> urls = Test.class.getClassLoader().getResources("META-INF/spring.factories");
while (urls.hasMoreElements()) {
System.out.println(urls.nextElement());
}
}
}

这个就是 Spring 中通过 SpringFactoriesLoader 来加载相关的类的起点。

SpringFactoriesLoader

SpringFactoriesLoader 是 Spring 中十分重要的一个扩展机制之一,它的使用方式和实现原理和 SPI 十分相似,只不过,提供了更加强大的功能。

和 SPI 不同,由于 SpringFactoriesLoader 中的配置文件格式是 properties 文件,因此,不需要要像 SPI 中那样为每个服务都创建一个文件, 而是选择直接把所有服务都扔到 META-INF/spring.factories 文件中。

比如,spring-boot-autoconfigure 中的部分内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnClassCondition

# ...

更多的使用可以参考:SpringFactoriesLoader (Spring Framework 5.2.0.RELEASE API)

结语

总的来说,无论是 ServiceLoader 还是 SpringFactoriesLoader,它们的基本原理都是一样的,都是通过 ClassLoader.getResources 方法找到相应的配置文件, 然后解析文件得到服务提供者的全限定名。

得益于 Java 强大的反射机制,拿到全限定名后基本上就可以为所欲为了 @_@

隐藏内容
简陋的 JSON 门面:DefaultJsonProviderFactory.java

本文转载自: 掘金

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

【SpringBoot2x】-自定义Spring boot

发表于 2019-10-02

github:github.com/Ccww-lx/Spr…

模块:spring-boot-starter-base-service
   SpringBoot的方便快捷主要体现之一starter pom,Spring Boot为我们提供了简化企业级开发绝大多数场景的
starter pom, 只要使用了应用场景所需要的starter pom,只需要引入对应的starter即可,即可以得到Spring Boot为我们提供的自动配置的Bean。

  然而,可能在很多情况下,我们需要自定义stater,这样可以方便公司内部系统调用共同的配置模块的时候可以自动进行装载配置。比如,很多公司将生产数据库的密码托管在公司的另外一个专门管理生产密码的系统上,公司每个系统需要使用的时候都需要调用其方法进行使用,现在可以通过starter自动配置的形式进行配置。

  1. SpringBoot Starter源码分析

Q:@SpringBootApplication 注解中核心注解@EnableAutoConfiguration注解在starter起什么作用呢?

@EnableAutoConfiguration源码分析:

1
2
3
4
5
6
7
8
9
10
11
复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}

  可以从源码看出关键功能是@import注解导入自动配置功能类AutoConfigurationImportSelector类,主要方法getCandidateConfigurations()使用了SpringFactoriesLoader.loadFactoryNames()方法加载META-INF/spring.factories的文件(spring.factories声明具体自动配置)。

1
2
3
4
5
6
7
8
9
复制代码protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}

Q:通常情况下,starter会根据条件进行操作处理,比如根据不同条件创建不同Bean。在SpringBoot有哪些注解可用呢?

可使用org.springframwork.boot.autoconfigure.condition的条件注解,具体如下所示:

注解 解析
@ConditionalOnBean 当容器里有指定的Bean的条件下。
@ConditionalOnClass 当类路径下有指定的类的条件下。
@ConditionalOnExpression 基于SpEL表达式作为判断条件。
@ConditionalOnJava 基于JVM版本作为判断条件。
@ConditionalOnJndi 在JNDI存在的条件下查找指定的位置。
@ConditionalOnMissingBean 当容器里没有指定Bean的情况下。
@ConditionalOnMissingClass 当类路径下没有指定的类的条件下。
@ConditionalOnNotWebApplication 当前项目不是Web项目的条件下。
@ConditionalOnProperty 指定的属性是否有指定的值。
@ConditionalOnResource 类路径是否有指定的值。
@ConditionalOnSingleCandidate 当指定Bean在容器中只有一个, 或者虽然有多个但是指定首选的Bean。
@ConditionalOnWebApplicatio 当前项目是Web项目的条件下。
  1. 自定starter

在此将模拟公司获取生产密码模块进行自定义starter demo

2.1 核心依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>

2.2 服务类service以及属性配置注入

PasswordService服务类:

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
复制代码public class PasswordService {
//第三方系统获取密码所需的key
private String objectKey;
@Autowired
//模拟的第三方系统service
private ThirdPartySystemService thirdPartySystemService;

public String getSystemPassword(String objectKey,String originalPassord){

if(StringUtils.isEmpty(objectKey)){
return originalPassord;
}
//从第三方系统获取密码
String password= thirdPartySystemService.getPassword(objectKey);
//返回密码
return password!=null?password:originalPassord;

}
}

//模拟第三方系统service
public class ThirdPartySystemService {
public String getPassword(String objectKey){
//返回一个32位随机数
return UUID.randomUUID().toString();
}
}

属性配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码//通过@ConfigurationProperties注解获取属性值
@ConfigurationProperties(prefix = "project.starter")
public class BaseServiceProperties {
private String serviceName;
private String serviceVersion;

public String getServiceName() {
return serviceName;
}

public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}

public String getServiceVersion() {
return serviceVersion;
}

public void setServiceVersion(String serviceVersion) {
this.serviceVersion = serviceVersion;
}
}

配置属性使用类:

1
2
3
4
5
6
复制代码public class BaseStarterService {

public void addServiceName(BaseServiceProperties baseServiceProperties){
System.out.println("serviceName:"+baseServiceProperties.getServiceName()+"----"+"serviceVersion"+baseServiceProperties.getServiceVersion());
}
}

其他类:

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
复制代码//判断是否windows系统
public class WindowsCondition implements Condition {

private final static String WINDOWS="Windows";

/**
* ConditionContext:判断条件能使用的上下文(环境)
* AnnotatedTypeMetadata:注释信息
*/
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
//获取当前环境变量
Environment environment=conditionContext.getEnvironment();
//获取bean注册器
BeanDefinitionRegistry registry = conditionContext.getRegistry();
//能获取到ioc使用的beanfactory
ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory();
//获取环境变量中操作系统
String property = environment.getProperty("os.name");
//判断操作系统是否为windows
if(property.contains(WINDOWS)){
//判断是否存在baseWindowsSevice类,不存在则进行bean注册
boolean isWindowsSevice = registry.containsBeanDefinition("baseStarterService");
if(!isWindowsSevice){
//指定Bean定义信息;(Bean的类型,Bean的一系列信息)
RootBeanDefinition beanDefinition = new RootBeanDefinition(BaseStarterService.class);
//注册一个Bean,指定bean名
registry.registerBeanDefinition("baseStarterService", beanDefinition);
BaseStarterService windowsSevice = (BaseStarterService)beanFactory.getBean("baseStarterService");
}
return true;
}
return false;
}
}

2.3自动配置类

代码解读:

  • @EnableConfigurationProperties:读取配置文件的属性
  • @Import:导入其他配置类或者自定义类
  • @Conditional:判断当前环境是否为windows,是则注册该类
  • @ConditionalOnProperty:判断属性spring.project.ThirdPartySystemService.isPassword是否等于true,不为true则不注册该类
  • @ConditionalOnClass:判断IOC容器中是否存在ThirdPartySystemService类,存在则创建PasswordService bean
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
复制代码@Configuration
//自动加载配置文件属性值
@EnableConfigurationProperties(BaseServiceProperties.class)
@Import(BeanConfiguration.class)
//判断当前环境是否为windows
@Conditional(WindowsCondition.class):
//判断属性spring.project.ThirdPartySystemService.isPassword是否等于true
@ConditionalOnProperty(prefix = "spring.project.ThirdPartySystemService",value = "enablePassword", havingValue = "true",matchIfMissing = true)
public class AutoConfigurationPassoword {
@Autowired
private BaseServiceProperties baseServiceProperties;
@Autowired
private BaseStarterService baseWindowsService;

//加载第三方系统service
@Bean("thirdPartySystemService")
public ThirdPartySystemService thirdPartySystemService(){
baseWindowsService.addServiceName(baseServiceProperties);
return new ThirdPartySystemService();
}
@Bean
//判断IOC容器中是否存在ThirdPartySystemService类,存在则创建PasswordService bean
@ConditionalOnClass(ThirdPartySystemService.class)
public PasswordService passwordService(){
baseWindowsService.addServiceName(baseServiceProperties);
return new PasswordService();
}
}

2.4 注册配置

  想自动配置生效, 需要注册自动配置类,即在src/main/resources下新建METAINF/spring.factories。在spring.factorie配置如下:

1
2
复制代码org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.cn.ccww.configuration.AutoConfigurationPassoword

若有多个自动配置, 则用“,”隔开, 此处“\”是为了换行后还能够读取到属性。

  1. 测试自定义starter

3.1 import 依赖

1
2
3
4
5
6
7
复制代码 <dependencies>
<dependency>
<artifactId>spring-boot-starter-base-service</artifactId>
<groupId>com.cn.ccww</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

3.2 application.properties属性

application.properties文件有对应的字段是否启动自定义starter,还可以设置starter所需的属性。如下所示:

1
2
3
4
5
复制代码//自定义Starter配置
//当该属性的值不为true时,才不会启动自定义starter
spring.project.ThirdPartySystemService.enablePassword=true
project.starter.serviceName=ccww
project.starter.serviceVersion=1.0

4.总结

由上所述, starter的大体的工作流程:

  • SpringBoot启动时会自动搜索包含spring.factories文件的JAR包;
  • 根据spring.factories文件加载自动配置类AutoConfiguration;
  • 通过AutoConfiguration类,加载满足条件(@ConditionalOnXxx)的bean到Spring IOC容器中;
  • 使用者可以直接使用自动加载到IOC的bean。

各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!

欢迎关注公众号【**Ccww技术博客**】,原创技术文章第一时间推出

本文转载自: 掘金

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

Spring、SpringMVC、SpringBoot、Sp

发表于 2019-09-30

前言

Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。Spring使你能够编写更干净、更可管理、并且更易于测试的代码。Spring MVC是Spring的一个模块,一个web框架。通过Dispatcher Servlet, ModelAndView 和 View Resolver,开发web应用变得很容易。主要针对的是网站应用程序或者服务开发——URL路由、Session、模板引擎、静态Web资源等等。Spring配置复杂,繁琐,所以推出了Spring boot,约定优于配置,简化了spring的配置流程。Spring Cloud构建于Spring Boot之上,是一个关注全局的服务治理框架。Spring VS SpringMVC:

Spring是一个一站式的轻量级的java开发框架,核心是控制反转(IOC)和面向切面(AOP),针对于开发的WEB层(springMvc)、业务层(Ioc)、持久层(jdbcTemplate)等都提供了多种配置解决方案;SpringMVC是Spring基础之上的一个MVC框架,主要处理web开发的路径映射和视图渲染,属于Spring框架中WEB层开发的一部分;SpringMVC VS SpringBoot:

SpringMVC属于一个企业WEB开发的MVC框架,涵盖面包括前端视图开发、文件配置、后台接口逻辑开发等,XML、config等配置相对比较繁琐复杂;SpringBoot框架相对于SpringMVC框架来说,更专注于开发微服务后台接口,不开发前端视图;SpringBoot和SpringCloud:

SpringBoot使用了默认大于配置的理念,集成了快速开发的Spring多个插件,同时自动过滤不需要配置的多余的插件,简化了项目的开发配置流程,一定程度上取消xml配置,是一套快速配置开发的脚手架,能快速开发单个微服务;SpringCloud大部分的功能插件都是基于SpringBoot去实现的,SpringCloud关注于全局的微服务整合和管理,将多个SpringBoot单体微服务进行整合以及管理;SpringCloud依赖于SpringBoot开发,而SpringBoot可以独立开发;总结

  • Spring是核心,提供了基础功能;
  • Spring MVC 是基于Spring的一个 MVC 框架 ;
  • Spring Boot 是为简化Spring配置的快速开发整合包;
  • Spring Cloud是构建在Spring Boot之上的服务治理框架。

最后

欢迎大家关注我的公众号【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。

本文转载自: 掘金

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

【译】 axios 是如何封装 HTTP 请求的

发表于 2019-09-29

原载于 TutorialDocs 网站的文章《How to Implement an HTTP Request Library with Axios》。

概述

前端开发中,经常会遇到发送异步请求的场景。一个功能齐全的 HTTP 请求库可以大大降低我们的开发成本,提高开发效率。

axios 就是这样一个 HTTP 请求库,近年来非常热门。目前,它在 GitHub 上拥有超过 40,000 的 Star,许多权威人士都推荐使用它。

因此,我们有必要了解下 axios 是如何设计,以及如何实现 HTTP 请求库封装的。撰写本文时,axios 当前版本为 0.18.0,我们以该版本为例,来阅读和分析部分核心源代码。axios 的所有源文件都位于 lib 文件夹中,下文中提到的路径都是相对于 lib 来说的。

本文我们主要讨论:

  • 怎样使用 axios。
  • axios 的核心模块(请求、拦截器、撤销)是如何设计和实现的?
  • axios 的设计优点是什么?

如何使用 axios

要理解 axios 的设计,首先需要看一下如何使用 axios。我们举一个简单的例子来说明下 axios API 的使用。

发送请求

1
2
3
4
5
6
7
8
复制代码axios({
method:'get',
url:'http://bit.ly/2mTM3nY',
responseType:'stream'
})
.then(function(response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});

这是一个官方示例。从上面的代码中可以看到,axios 的用法与 jQuery 的 ajax 方法非常类似,两者都返回一个 Promise 对象(在这里也可以使用成功回调函数,但还是更推荐使用 Promise 或 await),然后再进行后续操作。

这个实例很简单,不需要我解释了。我们再来看看如何添加一个拦截器函数。

添加拦截器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码// 添加一个请求拦截器。注意,这里面有 2 个函数——分别是成功和失败时的回调函数,这样设计的原因会在之后介绍
axios.interceptors.request.use(function (config) {
// 发起请求前执行一些处理任务
return config; // 返回配置信息
}, function (error) {
// 请求错误时的处理
return Promise.reject(error);
});

// 添加一个响应拦截器
axios.interceptors.response.use(function (response) {
// 处理响应数据
return response; // 返回响应数据
}, function (error) {
// 响应出错后所做的处理工作
return Promise.reject(error);
});

从上面的代码,我们可以知道:发送请求之前,我们可以对请求的配置参数(config)做处理;在请求得到响应之后,我们可以对返回数据做处理。当请求或响应失败时,我们还能指定对应的错误处理函数。

撤销 HTTP 请求

在开发与搜索相关的模块时,我们经常要频繁地发送数据查询请求。一般来说,当我们发送下一个请求时,需要撤销上个请求。因此,能撤销相关请求功能非常有用。axios 撤销请求的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 例子一
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('请求撤销了', thrown.message);
} else {
// 处理错误
}
});

// 例子二
axios.post('/user/12345', {
name: '新名字'
}, {
cancelToken: source.token
}).

// 撤销请求 (信息参数是可选的)
source.cancel('用户撤销了请求');

从上例中可以看到,在 axios 中,使用基于 CancelToken 的撤销请求方案。然而,该提案现已撤回,详情如 点这里。具体的撤销请求的实现方法,将在后面的源代码分析的中解释。

axios 核心模块的设计和实现

通过上面的例子,我相信每个人都对 axios 的使用有一个大致的了解了。下面,我们将根据模块分析 axios 的设计和实现。下面的图片,是我在本文中会介绍到的源代码文件。如果您感兴趣,最好在阅读时克隆相关的代码,这能加深你对相关模块的理解。

HTTP 请求模块

请求模块的代码放在了 core/dispatchRequest.js 文件中,这里我只展示了一些关键代码来简单说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);

// 其他源码

// 默认适配器是一个模块,可以根据当前环境选择使用 Node 或者 XHR 发送请求。
var adapter = config.adapter || defaults.adapter;

return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);

// 其他源码

return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);

// 其他源码

return Promise.reject(reason);
});
};

上面的代码中,我们能够知道 dispatchRequest 方法是通过 config.adapter ,获得发送请求模块的。我们还可以通过传递,符合规范的适配器函数来替代原来的模块(一般来说,我们不会这样做,但它是一个松散耦合的扩展点)。

在 defaults.js 文件中,我们可以看到相关适配器的选择逻辑——根据当前容器的一些独特属性和构造函数,来确定使用哪个适配器。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码function getDefaultAdapter() {
var adapter;
// 只有在 Node.js 中包含 process 类型对象时,才使用它的请求模块
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// Node.js 请求模块
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// 浏览器请求模块
adapter = require('./adapters/xhr');
}
return adapter;
}

axios 中的 XHR 模块相对简单,它是对 XMLHTTPRequest 对象的封装,这里我就不再解释了。有兴趣的同学,可以自己阅读源源码看看,源码位于 adapters/xhr.js 文件中。

拦截器模块

现在让我们看看 axios 是如何处理,请求和响应拦截器函数的。这就涉及到了 axios 中的统一接口 ——request 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码Axios.prototype.request = function request(config) {

// 其他源码

var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

return promise;
};

这个函数是 axios 发送请求的接口。因为函数实现代码相当长,这里我会简单地讨论相关设计思想:

  1. chain 是一个执行队列。队列的初始值是一个携带配置(config)参数的 Promise 对象。
  2. 在执行队列中,初始函数 dispatchRequest 用来发送请求,为了与 dispatchRequest对应,我们添加了一个 undefined。添加 undefined 的原因是需要给 Promise 提供成功和失败的回调函数,从下面代码里的 promise = promise.then(chain.shift(), chain.shift()); 我们就能看出来。因此,函数 dispatchRequest 和 undefiend 可以看成是一对函数。
  3. 在执行队列 chain 中,发送请求的 dispatchReqeust 函数处于中间位置。它前面是请求拦截器,使用 unshift 方法插入;它后面是响应拦截器,使用 push 方法插入,在 dispatchRequest 之后。需要注意的是,这些函数都是成对的,也就是一次会插入两个。

浏览上面的 request 函数代码,我们大致知道了怎样使用拦截器。下一步,来看看怎样撤销一个 HTTP 请求。

撤销请求模块

与撤销请求相关的模块位于 Cancel/ 文件夹下,现在我们来看下相关核心代码。

首先,我们来看下基础 Cancel 类。它是一个用来记录撤销状态的类,具体代码如下:

1
2
3
4
5
6
7
8
9
复制代码function Cancel(message) {
this.message = message;
}

Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};

Cancel.prototype.__CANCEL__ = true;

使用 CancelToken 类时,需要向它传递一个 Promise 方法,用来实现 HTTP 请求的撤销,具体代码如下:

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
复制代码function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}

var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});

var token = this;
executor(function cancel(message) {
if (token.reason) {
// 已经被撤销了
return;
}

token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}

CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};

adapters/xhr.js 文件中,撤销请求的地方是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码if (config.cancelToken) {
// 等待撤销
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}

request.abort();
reject(cancel);
// 重置请求
request = null;
});
}

通过上面的撤销 HTTP请求的例子,让我们简要地讨论一下相关的实现逻辑:

  1. 在需要撤销的请求中,调用 CancelToken 类的 source 方法类进行初始化,会得到一个包含 CancelToken 类实例 A 和 cancel 方法的对象。
  2. 当 source 方法正在返回实例 A 的时候,一个处于 pending 状态的 promise 对象初始化完成。在将实例 A 传递给 axios 之后,promise 就可以作为撤销请求的触发器使用了。
  3. 当调用通过 source 方法返回的 cancel 方法后,实例 A 中 promise 状态从 pending 变成 fulfilled,然后立即触发 then 回调函数。于是 axios 的撤销方法——request.abort() 被触发了。

axios 这样设计的好处是什么?

发送请求函数的处理逻辑

如前几章所述,axios 不将用来发送请求的 dispatchRequest 函数看做一个特殊函数。实际上,dispatchRequest 会被放在队列的中间位置,以便保证队列处理的一致性和代码的可读性。

适配器的处理逻辑

在适配器的处理逻辑上,http 和 xhr 模块(一个是在 Node.js 中用来发送请求的,一个是在浏览器里用来发送请求的)并没有在 dispatchRequest 函数中使用,而是各自作为单独的模块,默认通过 defaults.js 文件中的配置方法引入的。因此,它不仅确保了两个模块之间的低耦合,而且还为将来的用户提供了定制请求发送模块的空间。

撤销 HTTP 请求的逻辑

在撤销 HTTP 请求的逻辑中,axios 设计使用 Promise 来作为触发器,将 resolve 函数暴露在外面,并在回调函数里使用。它不仅确保了内部逻辑的一致性,而且还确保了在需要撤销请求时,不需要直接更改相关类的样例数据,以避免在很大程度上入侵其他模块。

总结

本文详细介绍了 axios 的用法、设计思想和实现方法。在阅读之后,您可以了解 axios 的设计,并了解模块的封装和交互。

本文只介绍了 axios 的核心模块,如果你对其他模块代码感兴趣,可以到 GitHub 上查看。

(完)

本文转载自: 掘金

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

Spring Boot入门(四)全局异常处理

发表于 2019-09-29

banner窄.png

铿然架构 | 作者 / 铿然一叶
这是铿然架构的第 30 篇原创文章


相关阅读:

萌新快速成长之路

如何编写软件设计文档

JAVA编程思想(一)通过依赖注入增加扩展性

JAVA编程思想(二)如何面向接口编程

JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则

JAVA编程思想(四)Builder模式经典范式以及和工厂模式如何选?

Java编程思想(七)使用组合和继承的场景

JAVA基础(一)简单、透彻理解内部类和静态内部类

JAVA基础(二)内存优化-使用Java引用做缓存

JAVA基础(三)ClassLoader实现热加载

JAVA基础(四)枚举(enum)和常量定义,工厂类使用对比

JAVA基础(五)函数式接口-复用,解耦之利刃

Seata源码(一)初始化

Seata源码(二)事务基础对象

Seata源码(三)事务处理类结构和流程

Seata源码(四)全局锁GlobalLock

Seata源码(五)Seata数据库操作

Seata源码(六)Seata的undo日志操作

Seata源码(七)Seata事务故障处理

Seata源码(八)Seata事务生命周期hook

Seata源码(九)TCC核心类和处理逻辑

Seata源码(十)RM接收到请求后的调用过程

Seata源码(十一)TC接收到请求后的处理过程\


一、前后端调用关系

通常情况下,前后端调用关系如下:

后端服务的入口为Controller,然后调用Service,如涉及到数据库访问则调用DAO。在每一层都有可能抛出异常,有的是系统异常,有的是封装后的业务异常,例如用户密码错误,用户号码不存在都属于业务异常,需要封装后返回。为了方便异常处理,可以从底层一直向上抛,在返回客户端前再统一处理。

二、统一异常处理

当所有的客户端调用的入口都必须经过Controller时,可以拦截Controller进行处理,类结构如下:

代码参考如下:

1.GlobalExceptionHandler.java

拦截返回给客户端的异常进行处理。

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
java复制代码import com.javashizhan.domain.Response;
import com.javashizhan.exception.ThrowableHandler;
import org.springframework.http.HttpStatus;
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.bind.annotation.ResponseStatus;

/**
* @ClassName GlobalExceptionHandler
* @Description 全局异常处理,捕捉所有Controller抛出的异常, @ResponseBody注解必须加上,否则会跳到默认的错误页
* @Author 铿燃一叶
* @Date 2019/9/28 23:24
* @Version 1.0
* javashizhan.com
**/
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

/**
* 默认返回HttpStatus.OK, 这里不是必须的,如果返回其他状态码才有必要加上
* @param e
* @return 封装后的应答对象
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler({Throwable.class})
public Response handle(Throwable e) {
return ThrowableHandler.handle(e);
}
}

2.ThrowableHandler.java

对异常进行处理,和GlobalExceptionHandler类解耦,这样当非Controller作为调用入口类时也可以复用。

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
java复制代码import java.io.PrintWriter;
import java.io.StringWriter;
import com.javashizhan.domain.Response;

/**
* @ClassName ThrowableHandler
* @Description 异常处理类,将异常转换为统一应答对象
* @Author 铿然一叶
* @Date 2019/9/28 23:30
* @Version 1.0
* javashizhan.com
**/
public class ThrowableHandler {

private ThrowableHandler() {}

public static Response handle(Throwable t) {
Response response = new Response();
response.setStatus(Response.STAUTS_FAILED);
response.setErrorMsg(t.getMessage());
response.setErrorStack(getErrorStack(t));
return response;
}

private static String getErrorStack(Throwable t) {
if (null != t) {
PrintWriter pw = null;
StringWriter sw = new StringWriter();
pw = new PrintWriter(sw);
t.printStackTrace(pw);
return sw.toString();
}
return "";
}
}

3.Response.java

异常转换结果,不管是否有异常,此类都可以使用。

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
62
63
64
65
66
67
68
69
70
71
72
73
74
java复制代码/**
* @ClassName A
* @Description TODO
* @Author 铿然一叶
* @Date 2019/9/28 23:24
* @Version 1.0
* javashizhan.com
**/
public final class Response<T> {

/** 应答状态-成功 */
public static final int STAUTS_OK = 0;

/** 应答状态-失败 */
public static final int STAUTS_FAILED = 1;

/** 默认错误码 */
public static final String DEFAULT_ERROR_CODE = "UNKNOWN-000";

/** 应答状态 */
private int status = STAUTS_OK;

/** 应答错误码,有错误时返回 */
private String errorCode = DEFAULT_ERROR_CODE;

/** 应答错误消息,有错误时返回 */
private String errorMsg;

/** 应答错误码,有错误时返回 */
private String errorStack;

/** 应答结果,存放业务应答内容 */
private T result;

public int getStatus() {
return status;
}

public void setStatus(int status) {
this.status = status;
}

public String getErrorCode() {
return errorCode;
}

public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}

public String getErrorMsg() {
return errorMsg;
}

public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}

public String getErrorStack() {
return errorStack;
}

public void setErrorStack(String errorStack) {
this.errorStack = errorStack;
}

public T getResult() {
return result;
}

public void setResult(T result) {
this.result = result;
}
}

4.TestGlobalExceptionController.java

验证类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @ClassName TestGlobalExceptionController
* @Description TODO
* @Author 铿然一叶
* @Date 2019/9/29 1:15
* @Version 1.0
* javashizhan.com
**/
@RestController
public class TestGlobalExceptionController {

@GetMapping("testGlobalException")
public void exec() throws Exception {
throw new Exception("Error occurred");
}
}

end.

本文转载自: 掘金

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

面试必问的4种单点登录的实现方式,你知道几个?

发表于 2019-09-27

前言

单点登录在现在的系统架构中广泛存在,他将多个子系统的认证体系打通,实现了一个入口多处使用,而在架构单点登录时,也会遇到一些小问题,在不同的应用环境中可以采用不同的单点登录实现方案来满足需求。一、共享Session

共享Session可谓是实现单点登录最直接、最简单的方式。将用户认证信息保存于Session中,即以Session内存储的值为用户凭证,这在单个站点内使用是很正常也很容易实现的,而在用户验证、用户信息管理与业务应用分离的场景下即会遇到单点登录的问题,在应用体系简单,子系统很少的情况下,可以考虑采用Session共享的方法来处理这个问题。
这个架构我使用了基于Redis的Session共享方案。将Session存储于Redis上,然后将整个系统的全局Cookie Domain设置于顶级域名上,这样SessionID就能在各个子系统间共享。分布式Session共享解决方案,这篇推荐大家看下。这个方案存在着严重的扩展性问题,首先,ASP.NET的Session存储必须为SessionStateItemCollection对象,而存储的结构是经过序列化后经过加密存储的。并且当用户访问应用时,他首先做的就是将存储容器里的所有内容全部取出,并且反序列化为SessionStateItemCollection对象。这就决定了他具有以下约束:1.Session中所涉及的类型必须是子系统中共同拥有的(即程序集、类型都需要一致),这导致Session的使用受到诸多限制;2.跨顶级域名的情况完全无法处理;
二、基于OpenId的单点登录


这种单点登录将用户的身份标识信息简化为OpenId存放于客户端,当用户登录某个子系统时,将OpenId传送到服务端,服务端根据OpenId构造用户验证信息,多用于C/S与B/S相结合的系统,流程如下:由上图可以看到,这套单点登录依赖于OpenId的传递,其验证的基础在于OpenId的存储以及发送。1.当用户第一次登录时,将用户名密码发送给验证服务;2.验证服务将用户标识OpenId返回到客户端;3.客户端进行存储;4.访问子系统时,将OpenId发送到子系统;5.子系统将OpenId转发到验证服务;6.验证服务将用户认证信息返回给子系统;7.子系统构建用户验证信息后将授权后的内容返回给客户端。这套单点登录验证机制的主要问题在于他基于C/S架构下将用户的OpenId存储于客户端,在子系统之间发送OpenId,而B/S模式下要做到这一点就显得较为困难。为了处理这个问题我们将引出下一种方式,这种方式将解决B/S模式下的OpenId的存储、传递问题。
三、基于Cookie的OpenId存储方案


我们知道,Cookie的作用在于充当一个信息载体在Server端和Browser端进行信息传递,而Cookie一般是以域名为分割的,例如a.xxx.com与b.xxx.com的Cookie是不能互相访问的,但是子域名是可以访问上级域名的Cookie的。即a.xxx.com和b.xxx.com是可以访问xxx.com下的Cookie的,于是就能将顶级域名的Cookie作为OpenId的载体。验证步骤和上第二个方法非常相似:1、 在提供验证服务的站点里登录;2、 将OpenId写入顶级域名Cookie里;3、 访问子系统(Cookie里带有OpenId)4、 子系统取出OpenId通过并向验证服务发送OpenId5、 返回用户认证信息6、 返回授权后的内容在以上两种方法中我们都可以看到通过OpenId解耦了Session共享方案中的类型等问题,并且构造用户验证信息将更灵活,子系统间的验证是相互独立的,但是在第三种方案里,我们基于所有子系统都是同一个顶级域名的假设,而在实际生产环境里有多个域名是很正常的事情,那么就不得不考虑跨域问题究竟如何解决。
四、B/S多域名环境下的单点登录处理


在多个顶级域名的情况下,我们将无法让各个子系统的OpenId共享。处理B/S环境下的跨域问题,我们首先就应该想到JSONP的方案。验证步骤如下:1、 用户通过登录子系统进行用户登录;2、 用户登录子系统记录了用户的登录状态、OpenId等信息;3、 用户使用业务子系统;4、 若用户未登录业务子系统则将用户跳转至用户登录子系统;5、 用户子系统通过JSONP接口将用户OpenId传给业务子系统;6、 业务子系统通过OpenId调用验证服务;7、 验证服务返回认证信息、业务子系统构造用户登录凭证;(此时用户客户端已经与子业务系统的验证信息已经一一对应)8、 将用户登录结果返回用户登录子系统,若成功登录则将用户跳转回业务子系统;9、 将授权后的内容返回客户端;五、安全问题

经过以上步骤,跨域情况下的单点登录问题已经可以得到解决。而在整个开发过程初期,我们采用用户表中纪录一个OpenId字段来保存用户OpenId,而这个机制下很明显存在一些安全性、扩展性问题。这个扩展性问题主要体现在一个方面:OpenId的安全性和用户体验的矛盾。整个单点登录的机制决定了OpenId是会出现在客户端的,所以OpenId需要有过期机制,假如用户在一个终端登录的话可以选择在用户每次登录或者每次退出时刷新OpenId,而在多终端登录的情况下就会出现矛盾:当一个终端刷新了OpenId之后其他终端将无法正常授权。而最终,我采用了单用户多OpenId的解决方案。每次用户通过用户名/密码登录时,产生一个OpenId保存在Redis里,并且设定过期时间,这样多个终端登录就会有多个OpenId与之对应,不再会存在一个OpenId失效所有终端验证都失效的情况。最后

欢迎大家关注我的公众号【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。

本文转载自: 掘金

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

通俗讲解 RESTful

发表于 2019-09-26

1 什么是 RESTful

百度一下 RESTful,查到的资料很多都讲得不清楚,看完了都不知道说的是啥,导致很多人对 RESTful 不甚了解。来看一下常见的解释:

(1)神一样的描述
REST 并不是“休息”的意思,而是 Representational State Transfer 的简称,即 表现层状态转移。

“表现层状态转移”是什么鬼?

(2)云里雾里的描述

  • REST 指的是 一组架构约束条件和原则,如果一个架构符合 REST 的约束条件和原则,就称之为 RESTful 架构。
  • RESTful 是一种软件架构风格,而不是标准。

这能看懂一点了,但还是云里雾里的。

(3)大神的总结
再看知乎大神 Ivony 的一句话精辟概括:

用 URL 定位资源,用 HTTP 动词(GET,POST,DELETE,PUT)描述操作。

RESTful 是一种 web 服务设计风格,风格意思就是大家默认的但不是强制的。

2 RESTful 详解

2.1 用 URL 定位资源

REST 的主体是资源,所谓“资源”,就是网络上的一个具体信息,例如:一张图片,一段文字、一种服务。总之就是一个实际存在的东西,而 URL 就是用来指向这个资源的。

例如:

1
复制代码https://api.example.com/users

这个 URL 一看就知道是对 user 资源的操作。URL 中只使用名词来指定资源,不包含操作。为什么呢?

如果要包含操作,那至少有增删改查四种,那么上例中的一个接口至少要变成四个:

1
2
3
4
复制代码https://api.example.com/add_user
https://api.example.com/delete_user
https://api.example.com/update_use
https://api.example.com/get_user

太多了,不够简洁。

2.2 用 HTTP 动词描述操作

那怎么描述操作呢?答案就是用 HTTP 动词。

HTTP 动词,可能很多人第一眼看到的时候有点蒙,不知道是啥,其实就是我们请求网页时用的 GET、POST 等操作。我们平时用的最多的就是 GET 和 POST(例如写爬虫的时候,基本都是这两种),常用的还有 PUT、PATCH、DELETE 。

对资源的操作,无外乎 CRUD(增删改查),RESTful 中,每个 HTTP 动词对应一个 CRUD 操作。

  • GET:对应 Retrieve 操作(查询操作)
  • POST:对应 Create 操作
  • DELETE:对应 Delete 操作
  • PUT:对应 Update 操作
  • PATCH:对应 Update 操作

2.3 POST 和 PUT 的区别

一般说到 HTTP 动词对应 CRUD 的时候,PUT 都是对应 Update 操作的。但其实,PUT 也可以做 Create 操作。二者的区别在于:

  • URL:POST 不需要指定到个体,例如新增 user 的接口 POST /api/users。 PUT 的 URL 需要指定到具体的个体,例如 PUT /api/users/1,如果 1 这个 user 存在,则 Update,否则 Create。这个很好理解,POST 确定是新增,insert 的时候是不需要 where 条件的;PUT 则不行,update 的时候不加 where,干过的小伙伴请举手。另外,PUT 的时候,也不是每个 user 就要建一个接口的,这里需要用到的就是路由,一般是写成 PUT /api/users/{id},这样就具有一般性了。路由在这里就不展开讲了。
  • 幂等性:PUT 是幂等的,而 POST 是非幂等的。关于幂等性,见下文。

2.4 PATCH 和 PUT 的区别

PATCH 是 2010 后成为的正式 http 方法,它是对 PUT 的补充。在没有 PATCH 之前,都是用 PUT 进行更新操作,这时候我们的接口中通常会有一个逻辑规则,如:如果对象的一个属性值为null,那么就不更新该属性(字段)值,通过这种方式来避免全部覆盖的操作。现在有了 PATCH 就解决了这种判断,在 PUT 操作中不管属性是不是 null,都进行更新,在 PATCH 接口中就对非 null 的进行更新。另外,PATCH 是非幂等的。

2.5 变通的 POST

按照 REST 建议,查询操作要使用 GET 方法,但是实际情况中处理起来比较麻烦,如:报表统计查询,需要传递的参数很多,如果采用 GET 方法,那么接口接收的参数非常多,接口很难看,通常会封装为 java 对象,但 GET 方法又不支持对象传参,所以很蛋疼;

对于这种情况,最简单的方式就是改成 POST 方式,而且很多公司都是这么干的。可见 REST 只是建议,并非强制约束。

补充:幂等性

幂等(Idempotence)本来是一个数学上的概念,定义就不说了,看了头晕。

后来拓展到计算机领域,描述为:

一个操作、方法或者服务,其任意多次执行所产生的影响均与一次执行的影响相同。

一个幂等的方法,使用同样的参数,对它进行多次调用和一次调用,对系统产生的影响是一样的。所以,对于幂等的方法,不用担心重复执行会对系统造成任何改变。

举个例子,用户 X 的手机话费余额为 2 元,他用支付宝给手机充了 100 元话费,如果将这个操作描述为“给 X 的账户余额增加 100 元”那就是非幂等的,重复操作几次运营商就亏大了。但是,如果将这个操作描述为“将 X 的账户余额设置为 102 元”,那这个操作就是幂等的。简单来说:

  • 幂等操作:将账户 X 的余额设置为 102 元;
  • 非幂等操作:将账户 X 的余额增加 100 元。

注意:这里的幂等性的例子并不严谨,本文主要不是讲幂等性的,所以只是举个简单的例子,不做深入探讨。

3 RESTful 的其他细节

3.1 命名规则

  • (1)全部小写,用 _ 或 - 线连接。

例如我在上面给出的例子 :

1
复制代码https://api.example.com/add_user

之所以不用驼峰命名法,是因为早期的 URI 一般都是表示服务器上的文件路径,而不同服务器对大小写的敏感性是不同的,为了兼容不同服务器所以才规定不能混用大小写字母。

  • (2)URL 中只用名词指定资源,因为 REST 的核心是资源,而表示资源的词语天然就是名词。
  • (3)资源用复数表示。

3.2 版本

一种方法是在 URL 中添加版本号,例如:

1
复制代码https://api.example.com/v1/users

另一种方法是将版本号加在 HTTP 请求头信息的 Accept 字段中,例如:

1
复制代码Accept: version=1.0

虽然有很多博客里推荐里说是推荐在 header 里添加版本信息,因为不同的版本表示的资源依然是同一个,所以不应该用不同的 URL。但是以我目前了解到的情况来看,绝大多数公司都是将版本号放在 URL 中的,并且推荐这么做,简单直观。

网上能找到的版本号加在 URL 中的例子,都是如我上例所示的写法。但是 Jack_Zeng 指出,这样写容易有歧义,会让人误以为 v1 也是资源的一部分,一般都是这么写:

1
复制代码https://api.example.com/users?api-version=1

3.3 HTTP 状态码

知乎上另一大神对 RESTful 的解释,相比于 Ivony 多了一句话,他用了三句话来描述:

  • 看 Url 就知道要什么
  • 看 http method 就知道干什么
  • 看 http status code 就知道结果如何

前两句和 Ivony 的是一个意思。这第三句我觉得总结得也很经典。

http 状态码有 100 多种,我们并不需要全部用到,只需要了解其中常用的就可以了

  • 200 – OK – 一切正常
  • 201 – OK – 新资源已经被创建
  • 204 – OK – 资源删除成功
  • 304 – 没有变化,客户端可以使用缓存数据
  • 400 – Bad Request – 调用不合法,确切的错误应该在 error payload 中描述
  • 401 – 未认证,调用需要用户通过认证
  • 403 – 不允许的,服务端正常解析和请求,但是调用被回绝或者不被允许
  • 404 – 未找到,指定的资源不存在
  • 422 – 不可指定的请求体 – 只有服务器不能处理实体时使用,比如图像不能被格式化,或者重要字段丢失
  • 500 – Internal Server Error – 标准服务端错误,开发人员应该尽量避开这种错误

参考资料:

  • www.zhihu.com/question/28…
  • blog.csdn.net/mingjia1987…

本文转载自: 掘金

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

Java基础笔试练习(四)

发表于 2019-09-25

Java基础笔试练习(四)

1.编译Java Application 源程序文件将产生相应的字节码文件,这些字节码文件的扩展名为( )。

A.java
B.class
C.html
D.exe

答案:
B

解析:
Java源程序的后缀名为“.java”,经过编译之后生成的字节码文件后缀名为“.class”

  1. 问以下哪个说法是正确的?()

A 派生出子类 B, B 派生出子类 C,并且在 java 源代码有如下声明:

  1. A a0=new A();
  2. A a1=new B();
  3. A a2=new C();

A.只有第一行能通过编译
B.第1、2行能通过编译,但第3行编译出错
C.第1、2、3行能通过编译,但第2、3行运行时出错
D.第1行,第2行和第3行的声明都是正确的

答案:
D

解析:
这个题考了两个点:

  1. 继承的传递性。
  2. 多态,父类引用指向子类对象。

3.以下哪个接口的定义是正确的?( )

A.interface B{ void print() { } ;}
B.interface B{ static void print() ;}
C.abstract interface B extends A1, A2 //A1、A2为已定义的接口{ abstract void print(){ };}
D.interface B{ void print();}

答案:
D

解析:
A,接口中方法的默认修饰符时public abstract,抽象方法可是没有方法体的,没有大括号{}
B,JDK8中,接口中的方法可以被default和static修饰,但是!!!被修饰的方法必须有方法体。
C,注意一下,接口是可以多继承的。整个没毛病,和A选项一样,抽象方法不能有方法体

4.以下会产生信息丢失的类型转换是( )

A.float a=10
B.int a=(int)8846.0
C.byte a=10; int b=-a
D.double d=100

答案:
B

解析:
会产生信息丢失不如说丢失精度,这样可能更容易明白,而精度丢失只会发生在从大范围到小范围的转换。上面四个选项,只有 B 是从 double 到 int ,也就是从大范围到小范围。

5.以下代码的输出的正确结果是

1
2
3
4
5
6
复制代码public class Test {
public static void main(String args[]) {
String s = "祝你考出好成绩!";
System.out.println(s.length());
}
}

A.24
B.16
C.15
D.8

答案:
D

解析:
java的String底层是char数组,它的length()返回数组大小,而unicode中一个汉字是可以用一个char表示的。 一个汉字等于一个字符字符是char,一个汉字也等于二个字节,字节是byte。

6.java中,StringBuilder和StringBuffer的区别,下面说法错误的是?

A.StringBuffer是线程安全的
B.StringBuilder是非线程安全的
C.StringBuffer对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象。
D.效率比较String<StringBuffer<StringBuilder,但是在 String S1 = “This is only a” + “ simple” + “ test”时,String效率最高。

答案:
C

解析:
效率:String(大姐,出生于JDK1.0时代) 不可变字符序列<StringBuffer(二姐,出生于JDK1.0时代) 线程安全的可变字符序列<StringBuilder(小妹,出生于JDK1.5时代) 非线程安全的可变字符序列。Java中的String是一个类,而并非基本数据类型。string是值传入,不是引用传入。StringBuffer和StringBuilder可以算是双胞胎了,这两者的方法没有很大区别。但在线程安全性方面,StringBuffer允许多线程进行字符操作。这是因为在源代码中StringBuffer的很多方法都被关键字synchronized修饰了,而StringBuilder没有。StringBuilder的效率比StringBuffer稍高,如果不考虑线程安全,StringBuilder应该是首选。另外,JVM运行程序主要的时间耗费是在创建对象和回收对象上。
关于D:因为进行字符串加运算时实际上是在堆中开辟大量空间,为了避免资源耗尽java编译器自动将String变量变为StringBulider而后进行append处理。

7.下列关于修饰符混用的说法,错误的是( )

A.abstract不能与final并列修饰同一个类
B.abstract类中不应该有private的成员
C.abstract方法必须在abstract类或接口中
D.static方法中能直接调用类里的非static的属性

答案:
D

解析:
1、abstract不能与final并列修饰同一个类 对的。
2、abstract类中不应该有private的成员 对的 :abstract类中可以有private 但是不应该有。
3、abstract方法必须在abstract类或接口中 对的 : 若类中方法有abstract修饰的,该类必须abstract修改。接口方法默认public abstract。
4、static方法中能处理非static的属性 错误 :在JVM中static方法在静态区,静态区无法调用非静态区属性。

8.如下语句通过算术运算和逻辑运算之后i和 j的结果是( )

1
2
3
4
5
复制代码int i=0;
int j=0;
if((++i>0)||(++j>0)){
//打印出i和j的值。
}

A.i=0;j=0
B.i=1;j=1
C.i=0;j=1
D.i=1;j=0

答案:
D

解析:
考察的是短路逻辑的用法,即:当第一个逻辑表达式可以确定结果时,将不再进行剩余表达式的运算。

9.以下是java concurrent包下的4个类,选出差别最大的一个

A.Semaphore
B.ReentrantLock
C.Future
D.CountDownLatch

答案:
C

解析:
A、Semaphore:类,控制某个资源可被同时访问的个数;
B、ReentrantLock:类,具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大;
C、Future:接口,表示异步计算的结果;
D、CountDownLatch: 类,可以用来在一个线程中等待多个线程完成任务的类。

10.下面有关java实例变量,局部变量,类变量和final变量的说法,错误的是?

A.实例变量指的是类中定义的变量,即成员变量,如果没有初始化,会有默认值。
B.局部变量指的是在方法中定义的变量,如果没有初始化,会有默认值
C.类变量指的是用static修饰的属性
D.final变量指的是用final 修饰的变量

答案:
B

解析:
B.定义在类中的变量是类的成员变量,可以不进行初始化,Java会自动进行初始化,如果是引用类型默认初始化为null,如果是基本类型例如int则会默认初始化为0
局部变量是定义在方法中的变量,必须要进行初始化。
被static关键字修饰的变量是静态的,静态变量随着类的加载而加载,所以也被称为类变量
被final修饰的变量是常量

11.关于JAVA的垃圾回收机制,下面哪些结论是正确?

A.程序可以任意指定释放内存的时间
B.JAVA程序不能依赖于垃圾回收的时间或者顺序
C.程序可明确地标识某个局部变量的引用不再被使用
D.程序可以显式地立即释放对象占有的内存

答案:
B

解析:
java提供了一个系统级的线程,即垃圾回收器线程。用来对每一个分配出去的内存空间进行跟踪。当JVM空闲时,自动回收每块可能被回收的内存,GC是完全自动的,不能被强制执行。程序员最多只能用System.gc()来建议执行垃圾回收器回收内存,但是具体的回收时间,是不可知的。当对象的引用变量被赋值为null,可能被当成垃圾。

12.JSP分页代码中,哪个步骤次序是正确的?

A.先取总记录数,得到总页数,最后显示本页的数据。
B.先取所有的记录,得到总页数,再取总记录数,最后显示本页的数据。
C.先取总页数,得到总记录数,再取所有的记录,最后显示本页的数据。
D.先取本页的数据,得到总页数,再取总记录数,最后显示所有的记录。

答案:
A

解析:
1、count(*)得到总记录数
2、计算总页数
3、获取所有记录(个人感觉这一步不需要,可以直接获取指定页数数据)
4、过滤显示本页数据

13.关于访问权限说法正确的是 ? ( )

A.外部类前面可以修饰public,protected和private
B.成员内部类前面可以修饰public,protected和private
C.局部内部类前面可以修饰public,protected和private
D.以上说法都不正确

答案:
B

解析:
(1)对于外部类而言,它也可以使用访问控制符修饰,但外部类只能有两种访问控制级别:public和默认。因为外部类没有处于任何类的内部,也就没有其所在类的内部、所在类的子类两个范围,因此 private 和 protected 访问控制符对外部类没有意义。
(2)内部类的上一级程序单元是外部类,它具有 4 个作用域:同一个类( private )、同一个包( protected )和任何位置( public )。
(3)因为局部成员的作用域是所在方法,其他程序单元永远不可能访问另一个方法中的局部变量,所以所有的局部成员都不能使用访问控制修饰符修饰。

14.以下代码执行的结果显示是多少( )?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码public class Demo { 
class Super{
int flag=1;
Super(){
test();
}
void test(){
System.out.println("Super.test() flag="+flag);
}
}
class Sub extends Super{
Sub(int i){
flag=i;
System.out.println("Sub.Sub()flag="+flag);
}
void test(){
System.out.println("Sub.test()flag="+flag);
}
}
public static void main(String[] args) {
new Demo().new Sub(5);
}
}

A.Sub.test() flag=1,Sub.Sub() flag=5
B.Sub.Sub() flag=5,Sub.test() flag=5
C.Sub.test() flag=0,Sub.Sub() flag=5
D.Super.test() flag=1,Sub.Sub() flag=5

答案:
A

解析:

在继承中代码的执行顺序为:
1.父类静态对象,父类静态代码块
2.子类静态对象,子类静态代码块
3.父类非静态对象,父类非静态代码块
4.父类构造函数
5.子类非静态对象,子类非静态代码块
6.子类构造函数
对于本题来说:在只想new Sub(5)的时候,父类先初始化了int flag=1,然后执行父类的构造函数Super(),父类构造函数中执行的test()方法,因子类是重写了test()方法的,因此父类构造函数中的test()方法实际执行的是子类的test()方法,所以输出为Sub.test() flag=1,接着执行子类构造函数Sub(5) 将flag赋值为5,因此输出结果Sub.Sub() flag=5。最终选择了A。

15.Java多线程有几种实现方法?

A.继承Thread类
B.实现Runnable接口
C.实现Thread接口
D.以上都不正确

答案:
AB

解析:
AB.
多线程一共有三种实现方式
方式1:继承Thread类,并重写run()方法
方式2:实现Runnable接口,实现run()方法
方式3:实现Callable接口,线程结束后可以有返回值,但是该方式是依赖于线程池的。

16.在Jdk1.7中,下述说法中抽象类与接口的区别与联系正确的有哪些?

A.抽象类中可以有普通成员变量,接口中没有普通成员变量。
B.抽象类和接口中都可以包含静态成员常量。
C.一个类可以实现多个接口,但只能继承一个抽象类
D.抽象类中可以包含非抽象的普通方法,接口中的方法必须是抽象的,不能有非抽象的普通方法。

答案:
A B C D

解析:
总结一下

  1. 一个子类只能继承一个抽象类,但能实现多个接口
  2. 抽象类可以有构造方法,接口没有构造方法
  3. 抽象类可以有普通成员变量,接口没有普通成员变量
  4. 抽象类和接口都可有静态成员变量,抽象类中静态成员变量访问类型任意,接口只能public static final(默认)
  5. 抽象类可以没有抽象方法,抽象类可以有普通方法,接口中都是抽象方法
  6. 抽象类可以有静态方法,接口不能有静态方法
  7. 抽象类中的方法可以是public、protected;接口方法只有public

17.根据下面这个程序的内容,判断哪些描述是正确的:( )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public class Test {
public static void main(String args[]) {
String s = "tommy";
Object o = s;
sayHello(o); //语句1
sayHello(s); //语句2
}
public static void sayHello(String to) {
System.out.println(String.format("Hello, %s", to));
}
public static void sayHello(Object to) {
System.out.println(String.format("Welcome, %s", to));
}
}

A.这段程序有编译错误
B.语句1输出为:Hello, tommy
C.语句2输出为:Hello, tommy
D.语句1输出为:Welcome, tommy
E.语句2输出为:Welcome, tommy
F.根据选用的Java编译器不同,这段程序的输出可能不同

答案:
CD

解析:
略

18.Java特性中,abstract class和interface有什么区别()

A.抽象类可以有构造方法,接口中不能有构造方法
B.抽象类中可以有普通成员变量,接口中没有普通成员变量
C.抽象类中不可以包含静态方法,接口中可以包含静态方法
D.一个类可以实现多个接口,但只能继承一个抽象类。

答案:
A B D

解析:
接口中的成员变量被隐式的声明为public static final ,jdk1.8后接口中可以有静态方法而且存在方法体

19.关于下面代码片段叙述正确的是()

代码片段:

1
2
3
4
5
复制代码byte b1=1,b2=2,b3,b6; 
final byte b4=4,b5=6;
b6=b4+b5;
b3=(b1+b2);
System.out.println(b3+b6);

A.输出结果:13
B.语句:b6=b4+b5编译出错
C.语句:b3=b1+b2编译出错
D.运行期抛出异常

答案:
C

解析:
C.被final修饰的变量是常量,这里的b6=b4+b5可以看成是b6=10;在编译时就已经变为b6=10了。而b1和b2是byte类型,java中进行计算时候将他们提升为int类型,再进行计算,b1+b2计算后已经是int类型,赋值给b3,b3是byte类型,类型不匹配,编译不会通过,需要进行强制转换。Java中的byte,short,char进行计算时都会提升为int类型。

20.下列那些方法是线程安全的(所调用的方法都存在)

A.

1
2
3
4
5
6
复制代码public class MyServlet implements Servlet {
public void service (ServletRequest req, ServletResponse resp) {
BigInteger I = extractFromRequest(req);
encodeIntoResponse(resp,factors);
}
}

B.

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public class MyServlet implements Servlet {
private long count =0;
public long getCount() {
return count;
}
public void service (ServletRequest req, ServletResponse resp) {
BigInteger I = extractFromRequest(req);
BigInteger[] factors = factor(i);
count ++;
encodeIntoResponse(resp,factors);
}
}

C.

1
2
3
4
5
6
7
8
9
复制代码public class MyClass {
private int value;
public synchronized int get() {
return value;
}
public synchronized void set (int value) {
this.value = value;
}
}

D.

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public class Factorizer implements Servlet {
private volatile MyCache cache = new MyCache(null,null);

public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new MyCache(i,factors);
}
encodeIntoResponse(resp,factors);
}

答案:
A C D

解析:
A:没有成员(没有共享资源),线程安全;
B:假设存在线程1和线程2,count初始值为0,当线程1执行count++中count+1(此时未写回最终计算值),这时线程2执行count++中读取count,发生数据错误,导致线程1线程2的结果都为1,而不是线程1的结果为1,线程2的结果为2,线程不安全;
C:成员私有,对成员的set get方法都加重量级锁,线程安全; D:volatile有两个作用:可见性(volatile变量的改变能使其他线程立即可见,但它不是线程安全的,参考B)和禁止重排序;这里是可见性的应用,类中方法对volatile修饰的变量只有赋值,线程安全; 欢迎指正。

推荐

笔试内容集合 持续更新中….

文末

欢迎关注个人微信公众号:Coder编程
获取最新原创技术文章和免费学习资料,更有大量精品思维导图、面试资料、PMP备考资料等你来领,方便你随时随地学习技术知识!

文章收录至
Github: github.com/CoderMerlin…
Gitee: gitee.com/573059382/c…
欢迎关注并star~

微信公众号

本文转载自: 掘金

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

1…855856857…956

开发者博客

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