Coding & Life

求知若饥,虚心若愚

什么是拆箱和装箱

Java中为每种基本类型都提供了对应的包装器类型。拆箱就是把包装器类型转换为基本数据类型的过程;装箱就是把基本数据类型转换为包装器类型的过程。

1
2
3
Integer i = 10; // 装箱

int b = i; // 拆箱

Java中拆箱和装箱的实现过程是:

以Double类型举例

拆箱:调用doubleValue方法;

装箱:调用Double.valueOf方法

面试相关问题

Integer比较

1
2
3
4
5
6
7
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;

assertThat(i1 == i2).isEqualTo(true);
assertThat(i3 == i4).isEqualTo(false);

比较过程:

  1. 调用Integer.valueOf()方法将各个变量装箱;

  2. 装箱后比较各个变量指向的内存地址是否相同。

但是为什么以上两个结果不同呢?看一下Integer的valueOf方法的实现便知究竟

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
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
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

通过分析源码,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。

Double类型比较

那么Double类型是否相同呢?

1
2
3
4
5
6
7
Double d1 = 100.0;
Double d2 = 100.0;
Double d3 = 200.0;
Double d4 = 200.0;

assertThat(d1 == d2).isEqualTo(false);
assertThat(d3 == d4).isEqualTo(false);

原因同样我们去看Double.valueOf的源码实现

1
2
3
public static Double valueOf(String s) throws NumberFormatException {
return new Double(parseDouble(s));
}

通过分析源码,double类型在每一次装箱时,都会新建一个对象。

Boolean类型比较

1
2
3
4
5
6
7
Boolean i1 = false;
Boolean i2 = false;
Boolean i3 = true;
Boolean i4 = true;

assertThat(i1 == i2).isEqualTo(true);
assertThat(i3 == i4).isEqualTo(true);

同样看Boolean.valueOf的源码实现

1
2
3
4
5
6
7
8

public static final Boolean TRUE = new Boolean(true);

public static final Boolean FALSE = new Boolean(false);

public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

关于equals和==

==

如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;

如果作用于引用类型的变量,则比较的是所指向的内存地址

equals

如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;

诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。

介绍

使用Spring Boot开发时,如果配置文件的敏感信息以明文方式展示,将加大系统的安全隐患,使用jasypt加密,十分简单的解决了这个问题

使用方法

  1. 在项目的启动类中(Application.java)增加注解@EnableEncryptableProperties;
  2. 增加配置文件jasypt.encryptor.password = xxxx,这是加密的密钥;
  3. 将配置文件中所有的敏感信息替换为ENC(加密字符串);
  4. 引入maven依赖;
1
2
3
4
5
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>1.9</version>
</dependency>

生成密钥的方法

1
java -cp /Users/wangweiye/.m2/repository/org/jasypt/jasypt/1.9.2/jasypt-1.9.2.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="root" password=xxxx algorithm=PBEWithMD5AndDES
  • input为需要加密的字符串;
  • password为配置文件中设置的加密密钥;

Java项目内使用

加密

1
2
3
4
5
6
7
8
9
10
11
12
13
public void encrypt() {
// 创建加密器
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
// 配置
EnvironmentStringPBEConfig config = new EnvironmentStringPBEConfig();
config.setAlgorithm("PBEWithMD5AndDES");// 加密算法
config.setPassword("xxxx");// 密码
encryptor.setConfig(config);

String plaintext = "root"; //明文
String ciphertext = encryptor.encrypt(plaintext); // 加密
System.out.println(plaintext + " : " + ciphertext);// 运行结果:root : root : zLdyNB+Dj3iw+J+TXZiv5g==
}

解密

1
2
3
4
5
6
7
8
9
10
11
12
public void decrypt() {
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
EnvironmentStringPBEConfig config = new EnvironmentStringPBEConfig();
config.setAlgorithm("PBEWithMD5AndDES");
config.setPassword("xxxx");
encryptor.setConfig(config);
String ciphertext = "zLdyNB+Dj3iw+J+TXZiv5g==";// 密文

//解密
String plaintext = encryptor.decrypt(ciphertext); // 解密
assertThat(plaintext).isEqualTo("root");
}

什么是模板方法模式

定义一个操作中算法的骨架,而将这些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤

好抽象的概念啊,文绉绉的东西就是不讨人喜欢,下面我用一个生活中常见的例子来举例说明吧

饮料自动售卖机,生产饮料的过程大致分为四个步骤

  1. 烧水
  2. 冲泡饮料
  3. 把饮料倒入杯中
  4. 加入调料

例如:
咖啡:烧开开水->加入咖啡粉冲泡->把饮料倒入杯中->加入少许糖
奶茶:烧开开水->加入奶茶粉冲泡->把饮料倒入杯中->加入珍珠

不难发现,饮料制作过程中的步骤中的1烧水、3把饮料倒入杯中是重复工作,制泡哪种饮料都一样,那么也就是重复工作,我们可以把它设定为通用性操作

我们只需要去关心步骤2和步骤4即可

由于制泡饮料的步骤就是这4步,所以我们可以把它抽象成一个”制作饮料模板”出来,下面就以上面这个例子,我用代码来说明

DrinkTemplate.java(模板类)

由于避免继承它的子类去修改整体制作架构,所以这个方法用了final修饰符来修饰

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
public abstract class DrinkTemplate {
public final void drinkTemplate() {
// 烧水
boilWater();
// 冲泡饮料
brew();
// 把饮料倒入杯中
pourInCup();
// 加调料
addCondiments();
}

// 加调料
protected abstract void addCondiments();

protected void pourInCup() {
System.out.println("把饮料倒入杯中");
}

// 冲泡饮料
protected abstract void brew();

protected void boilWater() {
System.out.println("烧水中...");
}
}

抽象类中的抽象方法必须重写,抽象类中的非抽象方法可以选择重写

MakeCoffee.java(制作咖啡)

1
2
3
4
5
6
7
8
9
10
11
public class MakeCoffee extends DrinkTemplate{
@Override
protected void addCondiments() {
System.out.println("加糖...");
}

@Override
protected void brew() {
System.out.println("加入咖啡粉冲泡...");
}
}

MakeOrange.java(制作橙汁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MakeOrange extends DrinkTemplate{
@Override
protected void addCondiments() {
System.out.println("加柠檬...");
}

@Override
protected void brew() {
System.out.println("加入橘子粉冲泡...");
}

protected void pourInCup() {
System.out.println("慢慢倒入");
}
}

这样只需要复写必须复写的方法,选择复写方法即可,大大提高了代码的复用性`

定义

是一种常用的软件设计模式,在它的核心结构中只包含一个被称为单例的特殊类。一个类只有一个实例,即一个类只有一个对象的实例

形式

单例模式可以分为懒汉式和饿汉式

懒汉式:在类加载时不初始化

饿汉式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快

懒汉式

下面试最标准也是最原始的单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static Singleton singleton;

private Singleton() {}

public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}

return singleton;
}
}

不考虑并发情况下,以上方法主要靠以下来限制实例的单一性

  1. 静态实例,带有static关键字的属性在每一个类中都是唯一的

  2. 私有化构造函数,限制了随意创造实例的可能性

  3. 公有获取实例的静态方法。为什么是静态?这个方法是供消费者获取对象实例的,如果是非静态的话,需要使用对象来调用,这就形成了矛盾

线程安全的单例

上面的方法在高并发情况下,肯定会出现有多个实例的情况-当一个线程判断为空,但是又没创建实例时,另一线程仍判断为空,这时就会出现单例模式非单例的情况

解决以上问题首先可能想到的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BadSynchronizedSingleton {

//一个静态的实例
private static BadSynchronizedSingleton synchronizedSingleton;
//私有化构造函数
private BadSynchronizedSingleton(){}
//给出一个公共的静态方法返回一个单一实例
public synchronized static BadSynchronizedSingleton getInstance(){
if (synchronizedSingleton == null) {
synchronizedSingleton = new BadSynchronizedSingleton();
}
return synchronizedSingleton;
}
}

上面的做法很简单,就是将整个获取实例的方法同步,这样在一个线程访问这个方法时,其它所有的线程都要处于挂起等待状态,倒是避免了刚才同步访问创造出多个实例的危险,但是我只想说,这样的设计实在是糟糕透了,这样会造成很多无谓的等待

其实我们同步的地方只是需要发生在单例的实例还未创建的时候,在实例创建以后,获取实例的方法就没必要再进行同步控制了。下面我们将上面的示例修改一下,变成标准版的单例模式,也称为双重加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SynchronizedSingleton {

//一个静态的实例
private static SynchronizedSingleton synchronizedSingleton;
//私有化构造函数
private SynchronizedSingleton(){}
//给出一个公共的静态方法返回一个单一实例
public static SynchronizedSingleton getInstance(){
if (synchronizedSingleton == null) {
synchronized (SynchronizedSingleton.class) {
if (synchronizedSingleton == null) {
synchronizedSingleton = new SynchronizedSingleton();
}
}
}
return synchronizedSingleton;
}
}

这种做法与上面那种最无脑的同步做法相比就要好很多了,因为我们只是在当前实例为null,也就是实例还未创建时才进行同步,否则就直接返回,这样就节省了很多无谓的线程等待时间,值得注意的是在同步块中,我们再次判断了synchronizedSingleton是否为null,解释下为什么要这样做。

假设我们去掉同步块中的是否为null的判断,有这样一种情况,假设A线程和B线程都在同步块外面判断了synchronizedSingleton为null,结果A线程首先获得了线程锁,进入了同步块,然后A线程会创造一个实例,此时synchronizedSingleton已经被赋予了实例,A线程退出同步块,直接返回了第一个创造的实例,此时B线程获得线程锁,也进入同步块,此时A线程其实已经创造好了实例,B线程正常情况应该直接返回的,但是因为同步块里没有判断是否为null,直接就是一条创建实例的语句,所以B线程也会创造一个实例返回,此时就造成创造了多个实例的情况。

饿汉式

1
2
3
4
5
6
7
8
9
public class SingletonDemo {
private static SingletonDemo instance = new SingletonDemo();

private SingletonDemo () {}

public static SingletonDemo getInstance() {
return instance;
}
}

这种方式基于类加载机制避免了多线程同步问题

定义

观察者模式(有时又被称为发布-订阅模式、模型-视图模式、源-收听者模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。

上面的定义当中,主要有这样几个意思,首先是有一个目标的物件,通俗点讲就是一个类,它管理了所有依赖于它的观察者物件,或者通俗点说是观察者类,并在它自己状态发生变化时,主动发出通知。

简单点概括成通俗的话来说,就是一个类管理着所有依赖于它的观察者类,并且它状态变化时会主动给这些依赖它的类发出通知。

应用

通常意义上如果一个对象状态的改变需要通知很多对这个对象关注的一系列对象,就可以使用观察者模式。

类图

可以看到,我们的被观察者类Observable只关联了一个Observer的列表,然后在自己状态变化时,使用notifyObservers方法通知这些Observer,具体这些Observer都是什么,被观察者是不关心也不需要知道的。

上面就将观察者和被观察者二者的耦合度降到很低了,而我们具体的观察者是必须要知道自己观察的是谁,所以它依赖于被观察者。

实现

实现一个简单的观察者模式,使用JAVA简单诠释一下上面的类图

观察者接口:

1
2
3
4
 // 这个接口时为了提供一个统一的观察者做出相应行为的方法
public interface Observer {
void update(Observable o);
}

具体观察者(观察者接口的实现)

1
2
3
4
5
6
7
public class ConcreteObserver1 implements Observer {
@Override
public void update(Observable o) {
System.out.println("观察者1观察到" + o.getClass().getSimpleName() + "发生变化");
System.out.println("观察者1做出相应变化");
}
}
1
2
3
4
5
6
7
public class ConcreteObserver2 implements Observer {
@Override
public void update(Observable o) {
System.out.println("观察者2观察到" + o.getClass().getSimpleName() + "发生变化");
System.out.println("观察者2做出相应变化");
}
}

下面是被观察者,它有一个观察者的列表,并且有一个通知所有观察者的方法,通知的方式就是调用观察者通用的接口行为update方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Observable {
List<Observer> observers = new ArrayList<>();

public void addObserver(Observer o) {
observers.add(o);
}

public void changed() {
System.out.println("==我是被观察者,我发生了变化==");

// 通知观察我的所有观察者
notifyObservers();
}

public void notifyObservers() {
for (Observer observer : observers) {
observer.update(this);
}
}
}

这里面很简单,新增两个方法,一个是为了改变自己的同时通知观察者们,一个是为了给客户端一个添加观察者的公共接口

下面测试一下

1
2
3
4
5
6
7
public void testObserver() {
Observable observable = new Observable();
observable.addObserver(new ConcreteObserver1());
observable.addObserver(new ConcreteObserver2());

observable.changed();
}

运行结果如下

可以看到我们在操作被观察者时,只要调用changed方法,观察者们就会做出相应的动作,而添加观察者这个行为算是准备阶段,将具体的观察者关联到被观察者上面去

下面给出一个有实际意义的例子,比如我们经常看的小说网站,都有这样的功能,就是读者可以订阅作者,这当中就有明显的观察者模式案例,就是作者和读者。他们的关系是一旦读者关注了一个作者,那么这个作者一旦有什么新书,就都要通知读者们,这明显是一个观察者模式的案例,所以我们可以使用观察者模式解决。

读者类,要实现观察者接口

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
import java.util.Observable;
import java.util.Observer;

public class Reader implements Observer {
private String name;

public Reader(String name) {
super();
this.name = name;
}

public String getName() {
return name;
}

// 当关注的作者发布新小说时,会通知读者去看
@Override
public void update(Observable o, Object arg) {
if (o instanceof Writer) {
Writer writer = (Writer) o;
System.out.println(name + "知道" + writer.getName() + "发布了新书《" + writer.getLastNovel() + "》非要去看");
}
}

//读者可以关注某一位作者,关注则代表把自己加到作者的观察者列表里
public void subscribe(String writerName) {
WriterManager.getInstance().getWriter(writerName).addObserver(this);
}

//读者可以取消关注某一位作者,取消关注则代表把自己从作者的观察者列表里删除
public void unsubscribe(String writerName) {
WriterManager.getInstance().getWriter(writerName).deleteObserver(this);
}
}

作者类

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
import java.util.Observable;

public class Writer extends Observable {
private String name; // 作者名称

private String lastNovel; //作者最新发布的小说

public String getName() {
return this.name;
}

public Writer(String name) {
super();
this.name = name;
WriterManager.getInstance().add(this);
}

// 作者发布新小说,要通知所有关注自己的读者
public void addNovel(String novel) {
System.out.println(name + "发布了新书《" + novel + "》");

lastNovel = novel;

setChanged();

notifyObservers();
}

public String getLastNovel() {
return this.lastNovel;
}
}

然后我们还需要一个管理器帮我们管理这些作者

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
import java.util.HashMap;
import java.util.Map;

public class WriterManager {
private static WriterManager writerManager;

private Map<String, Writer> writerMap = new HashMap<>();

// 添加作者
public void add(Writer writer) {
writerMap.put(writer.getName(), writer);
}

// 根据作者名获得作者
public Writer getWriter(String name) {
return writerMap.get(name);
}

// 单例模式(私有构造函数)
private WriterManager() {

}

public static WriterManager getInstance() {
if (writerManager == null) {
synchronized (WriterManager.class) {
if (writerManager == null) {
writerManager = new WriterManager();
}
}
}

return writerManager;
}
}

好了,这下我们的观察者模式就做好了,这个简单的DEMO可以支持读者关注作者,当作者发布新书时,读者会观察到这个事情,会产生相应的动作。下面我们写个测试用例测试一下

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 void testJdkObserver() {
//假设四个读者,两个作者
Reader r1 = new Reader("谢广坤");
Reader r2 = new Reader("赵四");
Reader r3 = new Reader("七哥");
Reader r4 = new Reader("刘能");
Writer w1 = new Writer("谢大脚");
Writer w2 = new Writer("王小蒙");
//四人关注了谢大脚
r1.subscribe("谢大脚");
r2.subscribe("谢大脚");
r3.subscribe("谢大脚");
r4.subscribe("谢大脚");
//七哥和刘能还关注了王小蒙
r3.subscribe("王小蒙");
r4.subscribe("王小蒙");

//作者发布新书就会通知关注的读者
//谢大脚写了设计模式
w1.addNovel("设计模式");
//王小蒙写了JAVA编程思想
w2.addNovel("JAVA编程思想");
//谢广坤取消关注谢大脚
r1.unsubscribe("谢大脚");
//谢大脚再写书将不会通知谢广坤
w1.addNovel("观察者模式");
}

我们使用观察者模式的用意是为了作者不再需要关心他发布新书时都要去通知谁,更重要的是他不需要关心他通知的是读者还是其它什么人,他只知道这个人是实现了观察者接口的,即我们的被观察者依赖的只是一个抽象的接口观察者接口,而不关心具体的观察者都有谁都是什么,比如以后要是游客也可以关注作者了,那么只要游客类实现观察者接口,那么一样可以将游客列入到作者的观察者列表中

另外,我们让读者自己来选择自己关注的对象,这相当于被观察者将维护通知对象的职能转化给了观察者,这样做的好处是由于一个被观察者可能有N多观察者,所以让被观察者自己维护这个列表会很艰难,这就像一个老师被许多学生认识,那么是所有的学生都记住老师的名字简单,还是让老师记住N多学生的名字简单?答案显而易见,让学生们都记住一个老师的名字是最简单的

观察者模式分离了观察者和被观察者二者的责任,这样让类之间各自维护自己的功能,专注于自己的功能,会提高系统的可维护性和可重用性


作者:zuoxiaolong(左潇龙)

出处:博客园左潇龙的技术博客–http://www.cnblogs.com/zuoxiaolong

您的支持是对博主最大的鼓励,感谢您的认真阅读。

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

什么是Docker

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口 –百度百科

Docker的思想来自于集装箱,集装箱解决了什么问题?在一艘大船上,可以把货物规整的摆放起来。并且各种各样的货物被集装箱标准化了,集装箱和集装箱之间不会互相影响。那么我就不需要专门运送水果的船和专门运送化学品的船了。只要这些货物在集装箱里封装的好好的,那我就可以用一艘大船把他们都运走。

docker就是类似的理念。现在都流行云计算了,云计算就好比大货轮。docker就是集装箱 –知乎

镜像

Docker 镜像是用于创建 Docker 容器的只读模板

容器

容器是独立运行的一个或一组应用
Docker容器通过Docker镜像来创建

Docker在不同操作系统下的安装

Mac安装

Mac下安装Docker很简单,可视化操作即可 dmg包下载地址

查看docker版本

1
docker -v

CentOS安装

1
yum install docker

安装完成后,查看docker版本

1
docker version

Docker镜像中心

动手构建Spring Boot+Docker应用

通过Spring Initializr创建普通Spring Boot应用

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication
@RestController
public class DockerApplication {
public static void main(String[] args) {
SpringApplication.run(DockerApplication.class, args);
}

@RequestMapping("/")
public String home() {
return "Hello Spring Boot, Docker and CloudComb!";
}
}

打包并运行jar包

1
mvn clean package

1
java -jar docker-0.0.1-SNAPSHOT.jar

浏览器访问http://127.0.0.1:8080输出正常则证明成功

容器化构建及运行

编写Dockerfile文件

在项目根目录下创建Dockerfile文件,

1
2
3
4
5
6
7
FROM hub.c.163.com/bingohuang/jdk8:latest

MAINTAINER wangweiye wwyknight@163.com

COPY target/docker-0.0.1-SNAPSHOT.jar app.jar

ENTRYPOINT ["java","-jar","/app.jar"]

此Dockerfile核心功能就是将可执行文件拷贝到镜像中,并在容器启动时默认执行启动命令java -jar /app.jar

docker创建

1
docker build -t spring-boot:latest .

-t为docker设置镜像名称和版本

详情查看docker build

docker运行

1
docker run -d -p 9999:8080 spring-boot

-d Run container in background and print container ID(在后台运行容器并打印容器ID)

-p 设置本机端口和容器端口的映射关系

详情查看docker run

访问

浏览器访问http://127.0.0.1:8080输出正常则证明成功

推送镜像到网易蜂巢

首先需要一个网易蜂巢账号,注册地址

在命令行中登录蜂巢仓库

1
2
3
4
docker login hub.c.163.com
Username: wwyknight@163.com
Password:
Login Succeeded

推送本地镜像

1
docker tag {镜像名或ID} hub.c.163.com/{你的用户名}/{标签名}

你的网易云镜像仓库推送地址为hub.c.163.com/{你的用户名}/{标签名}
Attention: 此处为你的用户名,不是你的邮箱帐号或者手机号码 登录网易云控制台,页面右上角头像右侧即为「用户名」
推送至不存在的镜像仓库时,自动创建镜像仓库并保存新推送的镜像版本;
推送至已存在的镜像仓库时,在该镜像仓库中保存新推送的版本,当版本号相同时覆盖原有镜像。

1
docker push hub.c.163.com/{你的用户名}/{标签名}

默认为私有镜像仓库,推送成功后即可在控制台的「镜像仓库」查看。

Spring Boot集成logback可以解决大部分日志需求,但是缺少访问日志,这在生产环境也是一种隐患。配合Tomcat Access Log可以完美解决这个问题

配置

在Spring boot中使用了内嵌的tomcat,可以通过server.tomcat.accesslog配置tomcat 的access日志

默认日志如下:

1
2
3
4
5
6
7
8
9
10
server.tomcat.accesslog.buffered=true # 缓存日志定期刷新输出(建议设置为true,否则当有请求立即打印日志对服务的响应会有影响)
server.tomcat.accesslog.directory=logs # 日志文件路径,可以是相对于tomcat的路径也可是绝对路径
server.tomcat.accesslog.enabled=false # 是否开启访问日志
server.tomcat.accesslog.file-date-format=.yyyy-MM-dd # 放在日志文件名中的日期格式
server.tomcat.accesslog.pattern=common # 日志格式,在下面详解
server.tomcat.accesslog.prefix=access_log # 日志文件名前缀
server.tomcat.accesslog.rename-on-rotate=false # 推迟在文件名中加入日期标记,直到日志分割时
server.tomcat.accesslog.request-attributes-enabled=false # 为请求使用的IP地址,主机名,协议和端口设置请求属性
server.tomcat.accesslog.rotate=true # 是否启用访问日志分割
server.tomcat.accesslog.suffix=.log # 日志名后缀

pattern的配置:

  • %a - 远程ip地址,注意不一定是原始ip地址,中间可能经过nginx等的转发

  • %A - 本地ip

  • %b - 发送的字节数,不包括HTTP标头,或者如果没有字节发送则使用’ - ‘

  • %B - 发送的字节数,不包括HTTP标头

  • %h - 远程主机名(或IP地址,如果连接器的enableLookups为false)

  • %H - 请求协议

  • %l - Remote logical username from identd (always returns ‘-‘)

  • %m - 请求方法(GET,POST)

  • %p - 接受请求的本地端口

  • %q - 查询字符串(如果存在则用’?’作为前缀,否则为空字符串)

  • %r - HTTP请求的第一行(包括请求方法,请求的URI)

  • %s - HTTP的响应代码,如:200,404

  • %S - User session ID

  • %t - 日期和时间,Common Log Format格式

  • %u - 被认证的远程用户

  • %U - Requested URL path

  • %v - Local server name

  • %D - Time taken to process the request, in millis

  • %T - Time taken to process the request, in seconds

  • %I - 当前请求的线程名,可以和打印的log对比查找问题

Access log 也支持将cookie、header、session或者其他在ServletRequest中的对象信息打印到日志中,其配置遵循Apache配置的格式({xxx}指值的名称):

  • %{xxx}i for incoming headers,request header信息
  • %{xxx}o for outgoing response headers,response header信息
  • %{xxx}c for a specific cookie
  • %{xxx}r xxx is an attribute in the ServletRequest
  • %{xxx}s xxx is an attribute in the HttpSession
  • %{xxx}t xxx is an enhanced SimpleDateFormat pattern (see Configuration Reference document for details on supported time patterns)

内置模板

server.tomcat.accesslog.pattern中内置了两个日志格式模板,分别是common和combined

  • common: %h %l %u %t "%r" %s %b

  • combined: %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"

参考Tomcat access log配置

最近发现线上一个项目日志突然报错,最终找到解决方法记录一下

错误信息

java.io.IOException: The temporary upload location [/tmp/tomcat.948083514929465118.8889/work/Tomcat/localhost/ROOT] is not valid

原因

参考 https://github.com/spring-projects/spring-boot/issues/5009

tmpwatch – removes files which haven’t been accessed for a period of time

如上所言,删除指定的目录中一段时间未访问的文件。一般对于/tmp下的文件或日志文件

意思是tomcat的临时目录会被tmpwatch删除掉,甚至可能删除掉class文件,导致错误的发生

解决方法

方法1.启动时指定新的临时目录

1
-Djava.io.tmpdir=/var/tmp

方法2.配置文件中设置新的临时目录

1
2
3
server:
tomcat:
basedir: /var/tmp/

方法3.代码中配置tomcat 临时目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class MultipartConfig {
@Bean
MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
String location = System.getProperty("user.dir") + "/data/tmp";
File tmpFile = new File(location);
if (!tmpFile.exists()) {
tmpFile.mkdirs();
}
factory.setLocation(location);
return factory.createMultipartConfig();
}
}

参考链接

toutu

单服务器应用不用nginx代理时在Java中都是在Filter中实现的跨域设置,如何在请求到达应用服务器之前实现跨域的设置呢?使用nginx配置实现

什么是CORS

CORS是一个W3C标准,全称是跨域资源共享(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

实现方法

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
server {
listen 80;
server_name agent.bater.top;

root /var/www/html/api_agent/public;

location / {

if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,OPTIONS';
add_header Access-Control-Allow-Headers 'Authorization,X-Requested-With,Content-Type,Origin,Accept,Cookie';
add_header Access-Control-Max-Age 3600;
add_header Content-Length 0;
return 202;
}

add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,OPTIONS';
add_header Access-Control-Allow-Headers 'Authorization,X-Requested-With,Content-Type,Origin,Accept,Cookie';

proxy_pass http://localhost:8080/;
}
}

Access-Control-Allow-Origin: 它是W3C标准里用来检查该跨域请求是否可以被通过(Access Control Check)。如果需要跨域,解决方法就是在资源的头中加入Access-Control-Allow-Origin 指定你授权的域。

Access-Control-Allow-Credentials: 它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

Access-Control-Allow-Methods: 它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法

Access-Control-Allow-Headers: 它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段

Access-Control-Max-Age: 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是1小时(3600秒),即允许缓存该条回应3600秒,在此期间,不用发出另一条预检请求

Content-Length: 用于描述HTTP消息实体的传输长度

比起传统复杂的单体工程,使用Maven的多模块配置,可以帮助项目划分模块,鼓励重用,防止POM变得过于庞大,方便某个模块的构建,而不用每次都构建整个项目,并且使得针对某个模块的特殊控制更为方便

构建项目

  • 可以使用Spring Initializr先创建一个普通的spring boot项目,然后删除src目录以及其他无用文件

  • 然后修改主项目的pom文件,在其中添加<packaging>pom</packaging>

WX20201102-171326.png

  • 接着添加<dependencyManagement>用于管理项目中所有依赖的版本信息。当子moudle中需要该依赖时,不需要填写版本号,以此来保证项目中的版本一致性

WX20201102-171601.png

创建Spring Boot子模块

使用Spring Initializr创建普通的spring boot模块

  • pom中的parent信息改为主项目信息;并修改<packaging>jar

WX20201102-172112.png

  • 注意:如果该子模块仍需要被其他模块依赖,则修改打包插件如图,增加<configuration>的配置,使该模块默认打包为普通jar而非可执行jar打包后生成的两个jar包如图所示

WX20201102-172559.png

如何做到多环境配置文件合理覆盖

在子项目中有默认的配置文件,其他依赖于该子项目的项目可以选择性覆盖配置文件中的信息

  • 在子项目中创建默认的配置文件

WX20201102-173211.png

WX20201102-173338.png

  • 在主项目中使用spring.profiles.include引入子项目中的配置文件,来覆盖其中的配置信息(具有相同的配置时后面的覆盖前面的 service覆盖common,dev覆盖service)

WX20201102-173609.png

测试

在根目录下执行mvn clean package即可打包所有的子项目

代码地址

0%