環境資訊中心綜合外電;范震華 翻譯;賴慧玲 審校;稿源:Mongabay
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※回頭車貨運收費標準
※推薦評價好的iphone維修中心
※超省錢租車方案
※台中搬家遵守搬運三大原則,讓您的家具不再被破壞!
※推薦台中搬家公司優質服務,可到府估價
為什麼看源碼: 最好的學習的方式就是模仿,接下來才是創造。而源碼就是我們最好的模仿對象,因為寫源碼的人都不是一般的人,所以用心學習源碼,也就可能變成牛逼的人。其次,看源碼,是一項修練內功的重要方式,書看百遍其意自現,源碼也是一樣,前提是你不要懼怕源碼,要用心的看,看不懂了,不要懷疑自己的智商,回過頭來多看幾遍,我就是這樣做的,一遍有一遍的感受,等你那天看源碼不由的驚嘆一聲,這個代碼寫得太好了,恭喜你,你已經離優秀不遠了。最後,看源碼,能培養我們的編程思維,當然這個層次有點高了,需要時間積累,畢竟我離這個境界也有點遠。
今天就來談談java的string的源碼實現,後續我也會寫javaSe的源碼系列,歡迎圍觀和交流。
繼承三個接口的說明:
Comparable接口:
實現對象之間比較的接口,它的核心方法只有一個:
public int compareTo(T o);
CharSequence接口:
CharSequence是char值的可讀序列。該接口提供對許多不同種類的char序列的統一隻讀訪問。CharSequence是一個接口,它只包括length(), charAt(int index), subSequence(int start, int end)這幾個API接口。除了String實現了CharSequence之外,StringBuffer和StringBuilder也實現了 CharSequence接口。
那麼String為什麼實現Charsequence這個接口呢。這裏就要涉及一個java的重要特性,也就是多態。看下面的代碼
public void charSetTest(CharSequence charSequence){
System.out.println(charSequence+"實現了多態");
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
StringTest strTest = new StringTest();
strTest.charSetTest(new String("我是String"));
strTest.charSetTest(new StringBuffer("我是StringBuffer"));
strTest.charSetTest(new StringBuilder("我是StringBuilder"));
}
執行結果:
我是String實現了多態
我是StringBuffer實現了多態
我是StringBuilder實現了多態
繼承這個接口的原因就很明顯:
因為String對像是不可變的,StringBuffer和StringBuilder這兩個是可變的,所以我們在構造字符串的過程中往往要用到StringBuffer和StringBuilder。如果那些方法定義String作為參數類型,那麼就沒法對它們用那些方法,先得轉化成String才能用。但StringBuffer和StringBuilder轉換為String再轉換過來很化時間的,用它們而不是直接用String的“加法”來構造新String本來就是為了省時間。
Serializable接口:
繼承該接口,就是表明這個類是是可以別序列化的,這裏就標記了string這個類是可以被序列化的,序列化的定義以及使用時機可以移步這裏。
string總共提供了15種構造方法:
當然,這麼多的構造方法,只需搞明白四個構造方法就可以了,因為其他的構造方法就是這四個構造方法的調用:
在看構造方法之前,先看看String的屬性
private final char value[];
private int hash; // Default to 0
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
通過屬性了解到,底層聲明了一個final類型的char數組,這個char數組就是String的構造方法的核心,因為String的本質就是字符char(只針對javaSE8 以前的版本),下面來分析有代表的四個構造方法:
這兩個構造方法我們平時用的比較經常,源碼如下:
public String() { this.value = "".value; }
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
分析:
通過源碼可以看到,核心還是和String類聲明的char[]數組value屬性建立聯繫,進而來處理這個屬性的值。
在String()這個空構造的方法中,就是屬性char數組的值聲明為一個空的字符串賦值給屬性value.
而在String(String original),也是將方法參數的value的屬性賦值給聲明char[]數組的value屬性,方便String的其他方法對char[]數組處理。
記住,在java的String操作中,大多數情況下還是對char[]數組的操作,這點很重要。
一般情況下定義一個新的字符串,有下面的兩種方式:
String chen = new String("chen"); // 這個我們一般不會使用
String chen = "chen";
我們一般會選擇第二種,這又是什麼原因呢:
其實這兩種聲明的方式在JVM看來時等價的。
劃重點:
但是String password=”chen”,利用了字符串緩衝池,也就是說如果緩衝池中已經存在了相同的字符串,就不會產生新的對象,而直接返回緩衝池中的字符串對象的引用。
如:
String a = "chen";
String b = "chen";
String c = new String("chen");
String d = new String("chen");
System.out.println(a==b);//將輸出"true";因為兩個變量指向同一個對象。利用了字符串的緩衝池
System.out.println(c==d);//將輸出"flase";因為兩個變量不指向同一個對象。雖然值相同,只有用c.equals(d)才能返回true.
所以實際中,建議用第一種,可以減少系統資源消耗。
源碼如下:
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
分析:
這個構造方法的作用就是傳入指定大小的char[] 數組,指定一個起始位置,然後構造出從起始位置開始計算的指定長度個數的字符串,具體用法:
public static void main(String[] args) {
char[] chars = new char[]{'a','b','c','d'};
// 從chars數組的第二位開始,總數為3個字符的字符串
String rangeChar=new String(chars,1,3);
System.out.println(rangeChar);
}
輸出:
abc
這個構造方法的核心在於:
this.value = Arrays.copyOfRange(value, offset, offset+count);
//而這個方法在追一層,就到了Arrays這個工具類copyOfRange這個方法
public static char[] copyOfRange(char[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
char[] copy = new char[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
其實看到這裏,他的實現原理就基本清楚了,分析copyOfRange()這個方法的執行步驟:
首先是獲取原字符數組的orginal[]要構造字符串的長度,也就是這一行代碼:
int newLength = to - from;
然後異常判斷,並聲明新的數組,來存儲原數組指定長度的值
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
char[] copy = new char[newLength];
將原字符數組指定長度的值拷貝到新數組,返回這個數組:
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
最後再將數組的值賦值給String的屬性value,完成初始化:
this.value = Arrays.copyOfRange(value, offset, offset+count);
歸根結底還是和String聲明的屬性value建立聯繫,完成相關的操作。
這個構造方法的作用就是將指定長度的字節數組,構造成字符串,且還可以指定編碼值:
涉及的源碼如下:
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
}
// StringCoding中的方法:
static char[] decode(Charset cs, byte[] ba, int off, int len) {
// 1, 構造解碼器
CharsetDecoder cd = cs.newDecoder();
int en = scale(len, cd.maxCharsPerByte());
char[] ca = new char[en];
if (len == 0)
return ca;
boolean isTrusted = false;
if (System.getSecurityManager() != null) {
if (!(isTrusted = (cs.getClass().getClassLoader0() == null))) {
ba = Arrays.copyOfRange(ba, off, off + len);
off = 0;
}
}
cd.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE)
.reset();
if (cd instanceof ArrayDecoder) {
int clen = ((ArrayDecoder)cd).decode(ba, off, len, ca);
return safeTrim(ca, clen, cs, isTrusted);
} else {
ByteBuffer bb = ByteBuffer.wrap(ba, off, len);
CharBuffer cb = CharBuffer.wrap(ca);
try {
CoderResult cr = cd.decode(bb, cb, true);
if (!cr.isUnderflow())
cr.throwException();
cr = cd.flush(cb);
if (!cr.isUnderflow())
cr.throwException();
} catch (CharacterCodingException x) {
// Substitution is always enabled,
// so this shouldn't happen
throw new Error(x);
}
return safeTrim(ca, cb.position(), cs, isTrusted);
}
}
private static char[] safeTrim(char[] ca, int len,
Charset cs, boolean isTrusted) {
if (len == ca.length && (isTrusted || System.getSecurityManager() == null))
return ca;
else
return Arrays.copyOf(ca, len);
}
這個方法構造的方法的複雜之處就是在於對於指定編碼的處理,但是我們如果看完這個方法調用的整個流程最終還是落到
return Arrays.copyOf(ca, len);
返回一個指定編碼的字符數組,然後和String類的value屬性建立聯繫
字節數組構造方法的基本邏輯:就是將字節數組轉化為字符數組,再和String的value屬性建立聯繫,完成初始化。
這個構造方法就是將StringBuilder或者StringBuffer類初始化String類,源碼如下:
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
分析:
核心的代碼還是這一句:
this.value = Arrays.copyOf(builder.getValue(), builder.length());
在往下看builder.getValue(),的源碼
final char[] getValue() {
return value;
}
返回一個字符數組
這樣就能很好理解這個構造方法了: 先利用builder.getValue()將指定的類型轉化為字符數組,通過Arrays.copyOf()方法進行拷貝,將返回的數組賦值給String的屬性value,完成初始化。
String的方法大概有60多個,這裏只分析幾個常用的方法,了解其他的方法,可以移步javaSE官方文檔:
public char charAt(int index) {
//1. 判斷異常
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
// 2.返回指定位置的字符
return value[index];
}
用法示例:
String StrChar = "chen";
char getChar = StrChar.charAt(1);
System.out.println(getChar);
輸出:
h
// 源碼
public byte[] getBytes() {
return StringCoding.encode(value, 0, value.length);
}
// encode的源碼
static byte[] encode(char[] ca, int off, int len) {
String csn = Charset.defaultCharset().name();
try {
// use charset name encode() variant which provides caching.
return encode(csn, ca, off, len);
} catch (UnsupportedEncodingException x) {
warnUnsupportedCharset(csn);
}
try {
return encode("ISO-8859-1", ca, off, len);
} catch (UnsupportedEncodingException x) {
// If this code is hit during VM initialization, MessageUtils is
// the only way we will be able to get any kind of error message.
MessageUtils.err("ISO-8859-1 charset not available: "
+ x.toString());
// If we can not find ISO-8859-1 (a required encoding) then things
// are seriously wrong with the installation.
System.exit(1);
return null;
}
}
用法示例:
String strByte = "chen";
// 將string轉化為字節數組
byte[] getBytes = strByte.getBytes();
// 遍歷輸出
for (byte getByte : getBytes) {
System.out.println(getByte);
}
輸出結果:
99
104
101
110
這裏的參數為字符串:
這個方法一共涉及了四個方法,源碼如下:
public int indexOf(String str) {
return indexOf(str, 0);
}
public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
}
static int indexOf(char[] source, int sourceOffset, int sourceCount,
String target, int fromIndex) {
return indexOf(source, sourceOffset, sourceCount,
target.value, 0, target.value.length,
fromIndex);
}
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
// 1. 判斷範圍
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
// 2,判斷目標字符串是否時原子符串的子序列,並返回目標序列的第一個字符在原字符序列的索引。
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount);
for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++);
if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}
具體執行過程已在方法的註釋中進行了說明:
用法示例:
String strIndex = "chen";
int getIndex = strIndex.indexOf("he");
System.out.println(getIndex);
輸出:
1
注意:也就是輸出字符串中的第一個字符在原子符串中的索引,前提是傳入的參數必須是原子符串的子序列,以上面的列子為例,傳入的字符串序列必須是chen這個字符串的子序列,才能輸出正確的索引,比如傳入的序列不是chen的子序列,輸出為-1
String strIndex = "chen";
int getIndex = strIndex.indexOf("hew");
System.out.println(getIndex);
輸出:
-1
使用這個方法時,這點非常注意。
參數就是被替換的字符和新的字符,源碼如下:
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
// 1.遍歷字符數組,找到原子符的位置
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
//2. 聲明一個臨時的字符數組,用來存儲替換后的字符串,
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
// 3. 將原字符數組拷貝到新的字符數組中去
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
// 4.
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
// 3. 初始化一個新的字符串
return new String(buf, true);
}
}
return this;
}
具體的執行邏輯就是註釋的語句。
用法示例:
String strIndex = "chee";
String afterReplace = strIndex.replace('e','n');
System.out.println(afterReplace);
輸出:
chnn
注意:這裏的替換是字符串中的所有與舊字符的相同的字符,比如上面的這個例子,就是將原子符中的e全部替換為n。
源碼如下:
public String[] split(String regex) {
return split(regex, 0);
}
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));
off = value.length;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this};
// Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}
用法示例:
// 將一句話使用空格進行分隔
String sentence = "People who hear thumb up can get rich";
// 使用空格進行分隔
String[] subSequence = sentence.split("\\s");
for (String s : subSequence) {
System.out.println(s);
}
輸出:
People
who
hear
thumb
up
can
get
rich
注意:使用這個方法,對正則表達式有所了解,才能實現更強大的功能,正則表達式的學習,可以移步菜鳥教程
源碼如下:
public String substring(int beginIndex, int endIndex) {
// 1.判斷異常
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
// 2,確定切割的長度
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 3.使用構造方法,返回切割后字符串
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
具體的執行邏輯如註釋所示,這個方法的邏輯總體比較簡單:
具體用法:
String sentence = "hhchennn";
String subSequence = sentence.substring(2,6);
System.out.println(subSequence);
輸出:
chen
注意:從源碼了解到,這個方法的在切割的時候,一般將第一個參數包含,包含第二個參數,也就是說上面的例子中在切割後的字符串中包含2這個字符,但是不包含6這個字符。
// 1.去除字符串前後空格的方法
trim()
//2.大小寫轉換的方法
toLowerCase()
toUpperCase()
//3. 將字符串轉化為數組
toCharArray()
// 4.將基本類型轉化字符串
valueOf(boolean b)
// 5.返回對象本身的字符串形式
toString()
這些方法使用起來都比較簡單,強烈建議看看java官方文檔。
從java角度來講就是說成final的。參考Effective Java中第15條使可變性最小化中對不可變類的解釋:
不可變類只是其實例不能被修改的類。每個實例中包含的所有信息都必須在創建該實例的時候就提供,並且在對象的整個生命周期內固定不變。為了使類不可變,要遵循下面五條規則:
1. 不要提供任何會修改對象狀態的方法。
2. 保證類不會被擴展。 一般的做法是讓這個類稱為 `final` 的,防止子類化,破壞該類的不可變行為。
3. 使所有的域都是 final 的。
4. 使所有的域都成為私有的。 防止客戶端獲得訪問被域引用的可變對象的權限,並防止客戶端直接修改這些對象。
5. 確保對於任何可變性組件的互斥訪問。 如果類具有指向可變對象的域,則必須確保該類的客戶端無法獲得指向這些對象的引用。
當然在Java平台類庫中,包含許多不可變類,例如String ,基本類型的包裝類,BigInteger, BigDecimal等等。綜上所述,不可變類具有一些顯著的通用特徵:類本身是final修飾的;所有的域幾乎都是私有final的;不會對外暴露可以修改對象屬性的方法。通過查閱String的源碼,可以清晰的看到這些特徵。
String real = "chen"
real = "Wei";
下圖就很好解釋了代碼的執行過程:
執行第一行代碼時,在堆上新建一個對象實例chen,real是一個指向該實例的引用,引用包含的僅僅只是實例在堆上的內存地址而已。執行第二行代碼時,僅僅只是改變了real這個引用的地址,指向了另一個實例wei。所以,正如前面所說過的,不可變類只是其實例不能被修改的類。給real重新賦值僅僅只是改變了它的引用而已,並不會真正去改變它本來的內存地址上的值。這樣的好處也是顯而易見的,最簡單的當存在多個String的引用指向同一個內存地址時,改變其中一個引用的值並不會對其他引用的值造成影響。
那麼,String是如何保持不可變性的呢?結合Effective Java中總結的五條原則,閱讀它的源碼之後就一清二楚了。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
String類是final修飾的,滿足第二條原則:保證類不會被擴展。分析一下它的幾個域:
private final char value[] :可以看到Java還是使用字節數組來實現字符串的,並且用final修飾,保證其不可變性。這就是為什麼String實例不可變的原因。private int hash : String的哈希值緩存private static final long serialVersionUID = -6849794470754667710L : String對象的 serialVersionUIDprivate static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0] : 序列化時使用其中最主要的域就是value,代表了String對象的值。由於使用了private final修飾,正常情況下外界沒有辦法去修改它的值的。正如第三條使所有的域都是final的。和第四條使所有的域都成為私有的所描述的。難道這樣一個private加上final就可以保證萬無一失了嗎?看下面代碼示例:
final char[] value = {'a', 'b', 'c'};
value[2] = 'd';
這時候的value對像在內存中已經是a b d了。其實final修飾的僅僅只是value這個引用,你無法再將value指向其他內存地址,例如下面這段代碼就是無法通過編譯的:
final char[] value = {'a', 'b', 'c'};
value = {'a', 'b', 'c', 'd'};
所以僅僅通過一個final是無法保證其值不變的,如果類本身提供方法修改實例值,那就沒有辦法保證不變性了。Effective Java中的第一條原則不要提供任何會修改對象狀態的方法。String類也很好的做到了這一點。在String中有許多對字符串進行操作的函數,例如substring concat replace replaceAll等等,這些函數是否會修改類中的value域呢?我們看一下concat()函數的內部實現:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
注意其中的每一步實現都不會對value產生任何影響。首先使用Arrays.copyOf()方法來獲得value的拷貝,最後重新new一個String對像作為返回值。其他的方法和contact一樣,都採取類似的方法來保證不會對value造成變化。的的確確,String類中並沒有提供任何可以改變其值的方法。相比final而言,這更能保障String不可變。
Effective Java 中總結了不可變類的特點。
當然使用反射,java的反射機制可以做到我們平常做不到的很多事情:
String str = "chen";
System.out.println(str);
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = 'a';
System.out.println(str);
執行結果:
chen
caen
從以下三個方面來考慮他們之間的異同點:
1.可變和不可變性:
String: 字符串常量,在修改時,不會改變自身的值,若修改,就會重新生成新的字符串對象。
StringBuffer: 在修改時會改變對象本身,不會生成新的對象,使用場景:對字符經常改變的情況下,主要方法: append(),insert() 等。
2.線程是否安全
String: 定義之後不可改變,線程安全
String Buffer: 是線程安全的,但是執行效率比較低,適用於多線程下操作字符串緩衝區的大量數據。
StringBuilder: 線程不安全的,適用於單線程下操作字符串緩衝區的大量數據
3.共同點
StringBuilder和StringBuffer有共有的父類AbstractStringBuilder(抽象類)。
StringBuilder,StringBuffer的方法都會調用AbstractStringBuilder中的公共方法,如: super().append()…
只是StringBuffer會在方法上加上synchronized關鍵字,進行同步。
Guava工程包含了若干被Google的Java項目廣泛依賴的核心庫,例如:集合[collections] 、緩存[caching] 、原生類型支持[primitives support] 、併發庫[concurrency libraries] 、通用註解[common annotations] 、字符串處理[string processing] 、I/O 等等。所有這些工具每天都在被Google的工程師應用在產品服務中。具體的中文參考文檔,Guava中文參考文檔。
Hutool是一個Java工具包,也只是一個工具包,它幫助我們簡化每一行代碼,減少每一個方法,讓Java語言也可以“甜甜的”。Hutool最初是我項目中“util”包的一個整理,後來慢慢積累並加入更多非業務相關功能,並廣泛學習其它開源項目精髓,經過自己整理修改,最終形成豐富的開源工具集。Hutool參考文檔
參考博客:
https://juejin.im/post/59cef72b518825276f49fe40
https://www.cnblogs.com/ChrisMurphy/p/4760197.html
參考書籍: java官方文檔《深入理解JVM虛擬機》
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※回頭車貨運收費標準
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※推薦評價好的iphone維修中心
※教你寫出一流的銷售文案?
※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!
※台中搬家遵守搬運三大原則,讓您的家具不再被破壞!
摘錄自2020年5月26日中央社報導
澳洲環境衛生專家今(26日)告訴政府公共調查機構,澳洲最近野火季致命野火產生的煙霧,與近幾個月445人的死亡及逾4000人的住院治療有關。
澳洲皇家委員會(Royal Commission)是政府特設的公共調查機構,負責尋找改善因應自然災害的方法,必須在下次野火季來臨前,於8月31日報告結果。
澳洲塔斯馬尼亞大學(Universityof Tasmania)孟席斯醫學研究所(Menzies Institute for Medical Research)副教授強斯敦(Fay Johnston)指出,研究所的模型發現,有445人的死亡、3340人的住院及1373人的急診就醫可歸咎於野火。
強斯敦表示,80%的澳洲人(大約2000萬人)受到大火產生的煙霧影響,逾1000萬公頃的土地遭到焚燒。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計最專業,超強功能平台可客製化
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※回頭車貨運收費標準
※推薦評價好的iphone維修中心
※教你寫出一流的銷售文案?
※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!
※台中搬家公司費用怎麼算?
摘錄自2020年5月26日聯合報報導
正當全球各國疲於對抗新冠病毒之際,印度和巴基斯坦還多了一個頭痛問題:蝗災。成群蝗蟲侵襲印度20多個行政區,災區超過5萬公項,以拉賈斯坦、中央和古吉拉特三個邦最嚴重。
在巴基斯坦,這波蝗災是近20多年來最嚴重,巴國於2月宣布全國緊急狀態,俾路支省、信德省和旁遮普省有38%面積淪為蝗蟲繁殖的溫床。
向來敵對的印度和巴基斯坦,為了解決蝗害破天荒合作,自4月以來已開過九次視訊會議,亦有阿富汗和伊朗的專家加入。
生活環境
永續發展
土地利用
國際新聞
印度
巴基斯坦
蝗蟲
糧食
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※回頭車貨運收費標準
※推薦評價好的iphone維修中心
※超省錢租車方案
※台中搬家遵守搬運三大原則,讓您的家具不再被破壞!
※推薦台中搬家公司優質服務,可到府估價
摘錄自2020年5月26日聯合報報導
美國疾病管制中心(CDC)上星期對全美發出了「鼠患警報」,指在連續多月的防疫封鎖下,全國各地——特別是都會區的城中地段——紛紛出現了鼠患通報數量異常增加的情況。因為都會鼠群倚賴的餐飲業垃圾與商業區廚餘系統,已因防疫歇業而「斷糧多時」,因此除了鼠群大舉遷移、大膽深入民宅區外,部分鼠群更因生存壓力而出現互食、噬崽、以及鼠群格鬥的「暴力傾向」。
長期以來,以紐約為首的大型城市,都一直苦於城市鼠患的問題。但在2020年初,武漢肺炎疫情的封鎖之下,全國各地的衛生單位、防治機關,都紛紛接到了「大量新增」的居民鼠患通報。相關狀態,自3月中旬陸續爆發,但到了5月下旬,美國疾管中心才終於發出了「鼠患警報」,提醒美國民眾應該注意居家衛生與糧食囤積的安全,以避免養鼠為患、或遭遇那批從都心防疫區裡竄出求生的「侵略性鼠群」。
CDC表示,在近期的鼠患通報中,各級單位不僅發現了「鼠群侵門踏戶」不畏人類的機率大增;治鼠專家們也紛紛觀察到,都會區鼠群出現了「鼠吃鼠」、「鼠群相殺」與「嗜食幼鼠」的極端狀況。種種跡象皆顯示,被人類防疫行為嚴重影響的都會區老鼠,正因生存壓力而在劇烈地轉變集體行為。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※回頭車貨運收費標準
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※推薦評價好的iphone維修中心
※教你寫出一流的銷售文案?
※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!
※台中搬家遵守搬運三大原則,讓您的家具不再被破壞!
五菱之光V新車發布日前我們從上汽通用五菱官網獲悉,五菱之光V車型正式上市。不過這個V和凱迪拉克並沒有什麼關係,新車共推出3款車型,售價區間為3。38-3。68萬元。動力方面均搭載1。2L自然吸氣,最大功率為82馬力,峰值扭矩116牛·米,與之匹配的是5速手動變速器。
新款捷達,眾泰看到都不想拿出皮尺
就在本月7號,神車中的神車捷達再次發布新款,作為中期改款,外觀和內飾並沒有明顯改動,可能連眾泰看到了,都不屑於拿出自己的皮尺。
(車身尺寸4501*1704*1469,軸距2604mm,)而動力方面除了1.4TSI和1.4LMpI還增加了全新的1.5L MpI發動機,最大功率81Kkw,最大扭矩150牛米,匹配5速手動和6速自動變速箱,配置上稍有提升(中控台換裝了一塊尺寸更大的液晶显示屏,並配備有USB、SD卡以及AUX接口。
此外,新車還換裝了新的三輻式多功能方向盤),根據車型的不同還配備有自動空調、座椅加熱、胎壓監測、車身穩定控制以及全皮座椅等。
老捷達陪了國人二十幾年的情懷不是所有人都懂,但是現在的捷達(老款pOLO平台)每個月還2萬多的銷量,真的只能說這個車比較高級。
途觀L 15號首發 明年1月上市
目前,據最新消息稱,上汽大眾全新途觀L將於今年12月15日首發亮相,並有望於明年1月正式上市。從曝光的圖片來看,新車的前臉基本與海外版途觀保持一致,其延續了最新家族式的設計,車身側面海外版相比則有明顯的加長,後窗的三角窗面積也更大一些。
車身尺寸(長寬高4712/1839/1673mm,)相比海外版Tiguan的車身長度加長了226mm,同時軸距增長了110mm。達到了2791mm,儘管如此車內仍採用五座布局,動力方面,將搭載1.4TSI、1.8TSI、和2.0TSI的高低功率版3款發動機可選,最大功率分別為110kW(150ps)、132kW(179ps)、147kW(200ps)/162kW(220ps),與3款發動機匹配的均是7速DSG雙離合變速箱。
此外1.4TSI版途觀L則將保留手動擋車型。高配車型還將搭載四驅系統,而配置方面還有HUD抬頭显示、自動大燈、全景天窗、一鍵啟動、电子駐車、自動啟停、自動泊車等。這麼拽的配置還加長,到時候你不加價十幾萬我都不好意思提車啊。
五菱之光V新車發布
日前我們從上汽通用五菱官網獲悉,五菱之光V車型正式上市。不過這個V和凱迪拉克並沒有什麼關係,新車共推出3款車型,售價區間為3.38-3.68萬元。
動力方面均搭載1.2L自然吸氣,最大功率為82馬力,峰值扭矩116牛·米,與之匹配的是5速手動變速器。整車(新車的長寬高分別為4051/1575/1780毫米)軸距達到2600毫米。
其中最重要的是一改以往的中置后驅採用前置后驅的布局,除了發動機的噪音和散熱有所改善,新的布局也將令車頭有更長的緩衝區,這以後五菱宏光S的山路傳說估計要換主角的了。
天津港口奧迪事故車三折開賣
近日,據一汽-大眾內部員工爆料稱,公司內部發布了《天津港事件輕微受損進口奧迪車銷售公告》。內部員工只需3折的價格就可購買奧迪車。公告显示,一汽服貿汽車銷售中心將於12月6日至14日銷售一批國四排放進口奧迪車,此批車受2015年8月12日天津港事件輕微影響(在車庫中停放,未受到直接影響)。
車型包括(A1、A4allroad、A5、A7、S5、S6、S7、SQ5、Q5、Q7、TTS、R8)大部分進口車型。價格為市場指導價的3-5折,而且需要全款購車。此外,購車后需在2016年12月15日前落籍上牌,還需封存車籍四年。有網友表示奧迪真黑心,事故車還拿來賣,不過更多的網友則是更關心三折的奧迪哪裡有得賣?我先來一台R8。
奔馳C63被深圳交警認定為改裝車
一直以來深圳的各項交通法規都是走在全國最前線,像安全座椅和後排安全帶的強制性要求等,而深圳的交警更是妙招奇多,特別是治遠光狗的辦法都讓全國人民拍手稱快。
不過他們最近一條微博引起了車友的熱議,內容大致上整治改裝車行動,查獲非法改裝車7輛,最終全部查扣,而從照片和微信聊天記錄中我們可以看到其中有原裝的C63 AMG也被查扣,這TM就尷尬了,國內確實沒有合法改裝,那些非法改裝抓你的,但是連原廠車都不認識,只能對深圳交警說,要不拉你們進個猜車群?
本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※別再煩惱如何寫文案,掌握八大原則!
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※回頭車貨運收費標準
※台中搬家公司費用怎麼算?
什麼是好的hashcode,一般來說,一個hashcode,一般用int來表示,32位。
下面兩個hashcode,大家覺得怎麼樣?
0111 1111 1111 1111 1111 1111 1111 1111 ------A
1111 1111 1111 1111 1111 1111 1111 1111 ------B
只有第32位(從右到左)不一樣,好像也沒有所謂的好壞吧?
那,我們再想想,hashcode一般怎麼使用呢?在hashmap中,由數組+鏈表+紅黑樹組成,其中,數組乃重中之重,假設數組長度為2的n次方,(hashmap的數組,強制要求長度為2的n次方),這裏假設為8.
大家又知道,hashcode 對 8 取模,效果等同於 hashcode & (8 – 1)。
那麼,前面的A 和 (8 – 1)相與的結果如何呢?
0111 1111 1111 1111 1111 1111 1111 1111 ------A
0000 0000 0000 0000 0000 0000 0000 0111 ------ 8 -1
相與
0000 0000 0000 0000 0000 0000 0000 0111 ------ 7
結果為7,也就是,會放進array[7]。
大家再看B的計算過程:
1111 1111 1111 1111 1111 1111 1111 1111 ------B
0000 0000 0000 0000 0000 0000 0000 0111 ------ 8 -1
相與
0000 0000 0000 0000 0000 0000 0000 0111 ------ 7
雖然B的第32位為1,但是,奈何和我們相與的隊友,7,是個垃圾。
前面的高位,全是0。
ok,你懂了嗎,數組長度太小了,才8,導致前面有29位都是0;你可能覺得一般容量不可能這麼小,那假設容量為2的16次方,容量為65536,這下不是很小了吧,但即使如此,前面的16位也是0.
所以,問題明白了嗎,我們計算出來的hashcode,低位相同,高位不同;但是,因為和我們進行與計算的隊友太過垃圾,導致我們出現了hash衝突。
ok,我們怎麼來解決這個問題呢?
我們能不能把高位也參与計算呢?自然,是可以的。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
這裏,其實分了3個步驟:
計算hashcode,作為操作數1
h = key.hashCode()
將第一步的hashcode,右移16位,作為操作數2
h >>> 16
操作數1 和 操作數2 進行異或操作,得到最終的hashcode
還是拿前面的來算,
0111 1111 1111 1111 1111 1111 1111 1111 ------A
0000 0000 0000 0000 0111 1111 1111 1111 ----- A >>> 16
異或(相同則為0,否則為1)
0111 1111 1111 1111 1000 0000 0000 0000 --- 2147450880
這裏算出來的結果是 2147450880,再去對 7 進行與運算:
0111 1111 1111 1111 1000 0000 0000 0000 --- 2147450880
0000 0000 0000 0000 0000 0000 0000 0111 ------ 8 -1
與運算
0000 0000 0000 0000 0000 0000 0000 0000 ------ 0
這裏的A,算出來,依然在array[0]。
再拿B來算一下:
1111 1111 1111 1111 1111 1111 1111 1111 ------ B
0000 0000 0000 0000 1111 1111 1111 1111 ----- B >>> 16
異或(相同則為0,否則為1)
1111 1111 1111 1111 0000 0000 0000 0000 --- -65536
0000 0000 0000 0000 0000 0000 0000 0111 ------ 7
與運算
0000 0000 0000 0000 0000 0000 0000 0000 ------- 0
最終算出來為0,所以,應該放在array[0]。
恩?算出來兩個還是衝突了,我只能說,我挑的数字真的牛逼,是不是該去買彩票啊。。
總的來說,大家可以多試幾組數,下邊提供下源代碼:
public class BinaryTest {
public static void main(String[] args) {
int a = 0b00001111111111111111111111111011;
int b = 0b10001101111111111111110111111011;
int i = tabAt(32, a);
System.out.println("index for a:" + i);
i = tabAt(32, b);
System.out.println("index for b:" + i);
}
static final int tabAt(int arraySize, int hash) {
int h = hash;
int finalHashCode = h ^ (h >>> 16);
int i = finalHashCode & (arraySize - 1);
return i;
}
}
雖然說,我測試了幾個数字,還是有些衝突,但是,你把高16位弄進來參与計算,總比你不弄進來計算要好吧。
大家也可以看看hashmap中,hash方法的註釋:
/** * Computes key.hashCode() and spreads (XORs) higher bits of hash * to lower. Because the table uses power-of-two masking, sets of * hashes that vary only in bits above the current mask will * always collide. (Among known examples are sets of Float keys * holding consecutive whole numbers in small tables.) So we * apply a transform that spreads the impact of higher bits * downward. There is a tradeoff between speed, utility, and * quality of bit-spreading. Because many common sets of hashes * are already reasonably distributed (so don't benefit from * spreading), and because we use trees to handle large sets of * collisions in bins, we just XOR some shifted bits in the * cheapest possible way to reduce systematic lossage, as well as * to incorporate impact of the highest bits that would otherwise * never be used in index calculations because of table bounds. */
裏面提到了2點:
So we apply a transform that spreads the impact of higher bits downward.
所以,我們進行了一個轉換,把高位的作用利用起來。
we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as
to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds.
我們僅僅異或了從高位移動下來的二進制位,用最經濟的方式,削減系統性能損失,同樣,因為數組大小的限制,導致高位在索引計算中一直用不到,我們通過這種轉換將其利用起來。
在concurrentHashMap中,其主要是:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
這裏主要是使用spread方法來計算hash值:
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
大家如果要仔細觀察每一步的二進制,可以使用下面的demo:
static final int spread(int h) {
// 1
String s = Integer.toBinaryString(h);
System.out.println("h:" + s);
// 2
String lower16Bits = Integer.toBinaryString(h >>> 16);
System.out.println("lower16Bits:" + lower16Bits);
// 3
int temp = h ^ (h >>> 16);
System.out.println("h ^ (h >>> 16):" + Integer.toBinaryString(temp));
// 4
int result = (temp) & HASH_BITS;
System.out.println("final:" + Integer.toBinaryString(result));
return result;
}
這裏和HashMap相比,多了點東西,也就是多出來了:
& HASH_BITS;
這個有什麼用處呢?
因為(h ^ (h >>> 16))計算出來的hashcode,可能是負數。這裏,和 HASH_BITS進行了相與:
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
1111 1111 1111 1111 1111 1111 1111 1111 假設計算出來的hashcode為負數,因為第32位為1
0111 1111 1111 1111 1111 1111 1111 1111 0x7fffffff
進行相與
0111 ..................................
這裏,第32位,因為0x7fffffff的第32位,總為0,所以相與后的結果,第32位也總為0 ,所以,這樣的話,hashcode就總是正數了,不會是負數。
當hashcode為正數時,表示該哈希桶為正常的鏈表結構。
當hashcode為負數時,有幾種情況:
此時,其hash值為:
static final int MOVED = -1; // hash for forwarding nodes
當節點為ForwardingNode類型時(表示哈希表在擴容進行中,該哈希桶已經被遷移到了新的臨時hash表,此時,要get的話,需要去臨時hash表查找;要put的話,是不行的,會幫助擴容)
static final int TREEBIN = -2; // hash for roots of trees
表示,該哈希桶,已經轉了紅黑樹。
/**
* Returns the stamp bits for resizing a table of size n.
* Must be negative when shifted left by RESIZE_STAMP_SHIFT.
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
這裏,假設,n為4,即,hashmap中數組容量為4.
下面這句,求4的二進製表示中,前面有多少個0.
Integer.numberOfLeadingZeros(n)
表示為32位后,如下
0000 0000 0000 0000, 0000 0000 0000 0100
所以,前面有29個0,即,這裏的結果為29.
(1 << (RESIZE_STAMP_BITS – 1)
這一句呢,其中RESIZE_STAMP_BITS 是個常量,為16. 相當於,把1 向左移動15位。
二進製為:
1000 0000 0000 0000 -- 1 << 15
最終結果:
0000 0000 0000 0000 0000 0000 0001 1101 -- 29
0000 0000 0000 0000 1000 0000 0000 0000 -- 1 << 15
進行或運算
0000 0000 0000 0000 1000 0000 0001 1101 -- 相當於把29的第一位,變成了1,其他都沒變。
所以,最終結果是,
這個數,換算為10進制,為32972,是個正數。
這個數,有啥用呢?
在addCount函數中,當整個哈希表的鍵值對數量,超過sizeCtl時(一般為0.75 * 數組長度),就會觸發擴容。
java.util.concurrent.ConcurrentHashMap#addCount
int sc = sizeCtl;
boolean bSumExteedSizeControl = newBaseCount >= (long) sc;
// 1
if (bContinue) {
int rs = resizeStamp(n);
// 2
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 3
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
newBaseCount = sumCount();
} else {
break;
}
1處,如果擴容條件滿足
2處,如果sc小於0,這個sc是啥,就是前面說的sizeCtl,此時應該是等於:0.75 * 數組長度,不可能為負數
3處,將sc(此時為正數),cas修改為:
(rs << RESIZE_STAMP_SHIFT) + 2)
這個數有點意思了,rs就是前面我們的resizeStamp得到的結果。
按照前面的demo,我們拿到的結果為:
0000 0000 0000 0000 1000 0000 0001 1101 -- 相當於把29的第一位,變成了1,其他都沒變。
因為
private static int RESIZE_STAMP_BITS = 16;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
所以,RESIZE_STAMP_SHIFT 為16.
0000 0000 0000 0000 1000 0000 0001 1101 -- 相當於把29的第一位,變成了1,其他都沒變。
1000 0000 0001 1101 0000 0000 0000 0000 --- 左移16位,即 rs << RESIZE_STAMP_SHIFT
1000 0000 0001 1101 0000 0000 0000 0010 -- (rs << RESIZE_STAMP_SHIFT) + 2)
最終,這個數,第一位是 1,說明了,這個數,肯定是負數。
大家如果看過其他人寫的資料,也就知道,當sizeCtl為負數時,表示正在擴容。
所以,這裏
if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
這句話就是,如果當前線程成功地,利用cas,將sizeCtl從正數,變成負數,就可以進行擴容。
// 1
if (bContinue) {
int rs = resizeStamp(n);
// 2
if (sc < 0) {
// 2.1
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 2.2
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 3
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
newBaseCount = sumCount();
} else {
break;
}
此時,因為上面的線程觸發了擴容,sc已經變成了負數了,此時,新的線程進來,會判斷2處。
2處是滿足的,會進入2.1處判斷,這裏的部分條件看不懂,大概是:擴容已經結束,就不再執行,直接break
否則,進入2.2處,輔助擴容,同時,把sc變成sc + 1,增加擴容線程數。
時間倉促,如有問題,歡迎指出。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※別再煩惱如何寫文案,掌握八大原則!
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※超省錢租車方案
※教你寫出一流的銷售文案?
※網頁設計最專業,超強功能平台可客製化
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※台中搬家遵守搬運三大原則,讓您的家具不再被破壞!
有關SpringSceurity系列之前有寫文章
1、SpringSecurity(1)—認證+授權代碼實現
2、SpringSecurity(2)—記住我功能實現
3、SpringSceurity(3)—圖形驗證碼功能實現
在獲取短信驗證碼功能和圖形驗證碼還是有很多相似的地方,所以這裡在設計獲取短信驗證的時候,將之前開發好的的圖形驗證碼進一步整合、抽象與重構。
在獲取驗證碼的時候,它們最大的不同在於: 圖形驗證碼是通過接口返回獲取給前端。而短信驗證碼而言是通過第三方API向我們手機推送。
但是它們在登陸的時候就有很大的不同了,對於圖形驗證碼而言驗證通過之前就走 UsernamePasswordAuthenticationFilter 過濾器了開始校驗用戶名密碼了。
但對於短信登陸而言,確實也需要先現在短信驗證碼是否通過,但是一旦通過他是不走 UsernamePasswordAuthenticationFilter,而是通過其它方式查詢用戶信息來校驗
認證已經通過了。
這篇博客只寫獲取獲取短信驗證碼的功能,不寫通過短信驗證碼登陸的邏輯。
這裏才是最重要的,如何去設計和整合短信驗證碼和圖形驗證碼的代碼,是我們最應該思考的。如何將相似部分抽離出來,然後去實現不相同的部分。
整理后發現不同點主要在於
1、獲取驗證碼。因為對於圖形驗證碼需要有個畫布,而短信驗證碼並不需要,所以它們可以實現同一個接口,來完成不同的邏輯。
2、發送驗證碼。對於圖形驗證碼來講只要把驗證碼返給前端就可以,而短信驗證碼而言是通過第三方API將驗證碼發到我們的手機上。
所以這裏也可以通過實現統一接口來具體實現不同的方法。
相同部分我可以通過抽象類來完成實現,不同部分可以通過具體的實現類來實現。
AbstractValidateCodeProcessorService 抽象類是用來實現兩種驗證碼可以抽離的部分。ImageCodeProcessorServiceImpl 和
SmsCodeProcessorServiceImpl方法是來實現兩種驗證碼不同的發送方式。
在簡單看下時序圖可能會更加明白點。
一個接口只有一個方法(processor)就是處理驗證碼,它其實需要做三件事。
1、獲取驗證碼。2、將驗證碼存入session。3、將驗證碼信息通過短信或者圖形驗證碼發送出去。
首先講生成獲取驗證碼,這裡有一個公共接口和兩個實現類
對於保存驗證碼信息而言,可以在直接在 AbstractValidateCodeProcessorService抽象類來完成,都不需要去實現。
對發送驗證碼信息而言,只需要實現AbstractValidateCodeProcessorService抽象類的send發送驗證碼接口即可。
整個大致接口設計就是這樣,具體的可以通過代碼來展示。
短信驗證碼和圖形驗證后包含屬性有 code 和 expireTime,短信驗證碼只有這兩個屬性,而圖形驗證碼還多一個BufferedImage實例對象屬性,所以將共同屬性進行抽取
,抽取為ValidateCode類,代碼如下:
ValidateCode實體
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ValidateCode {
private String code;
private LocalDateTime expireTime;
public boolean isExpired() {
return LocalDateTime.now().isAfter(expireTime);
}
}
對於圖形驗證碼而言,除了需要code和過期時間還需要圖片的畫布,所以繼承ValidateCode之後再寫自己屬性
ImageCode實體
@Data
public class ImageCode extends ValidateCode {
private BufferedImage image;
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
super(code, expireTime);
this.image = image;
}
public ImageCode(BufferedImage image, String code, int expireIn) {
super(code, LocalDateTime.now().plusSeconds(expireIn));
this.image = image;
}
}
對於短信驗證碼而言,暫時不需要添加自己的屬性字段了。
SmsCode實體
public class SmsCode extends ValidateCode {
public SmsCode(String code, LocalDateTime expireTime) {
super(code, expireTime);
}
public SmsCode(String code, int expireIn) {
super(code, LocalDateTime.now().plusSeconds(expireIn));
}
}
ValidateCodeProcessor接口主要是完成 驗證碼的生成、保存與發送的完整流程,接口的主要設計如下所示:
ValidateCodeProcessorService接口
public interface ValidateCodeProcessorService {
/**
* 因為現在有兩種驗證碼,所以存放到seesion的key不能一樣,所以前綴+具體type
*/
String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";
/**
* 通過也是 type+CODE_PROCESSOR獲取對於的bean
*/
String CODE_PROCESSOR = "CodeProcessorService";
/**
* 這個接口要做三件事
* 1、獲取驗證碼。
* 2、將驗證碼存入session
* 3、將驗證碼信息通過短信或者圖形驗證碼發送出去。
* (將spring-boot-security-study-03接口裡的那三步進行里封裝)
*
*/
void processor(ServletWebRequest request) throws Exception;
由於圖片驗證碼和短信驗證碼的 生成和保存、發送等流程是固定的。所以這裏寫一個抽象類來實現ValidateCodeProcessor接口,來實現相似部分。
AbstractValidateCodeProcessorService抽象類
@Component
public abstract class AbstractValidateCodeProcessorService<C> implements ValidateCodeProcessorService {
private static final String SEPARATOR = "/code/";
/**
* 操作session的工具集
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 這是Spring的一個特性,就是在項目啟動的時候會自動收集系統中 {@link ValidateCodeGeneratorService} 接口的實現類對象
*
* key為bean name
*/
@Autowired
private Map<String, ValidateCodeGeneratorService> validateCodeGeneratorMap;
@Override
public void processor(ServletWebRequest request) throws Exception {
//第一件事
C validateCode = generate(request);
//第二件事
save(request, validateCode);
//第三件事
send(request, validateCode);
}
/**
* 生成驗證碼
*
*/
private C generate(ServletWebRequest request) {
String type = getProcessorType(request);
//這裏 image+CodeGenerator = imgCodeGenerator 對應的就是ImageCodeGeneratorServiceService
ValidateCodeGeneratorService validateCodeGenerator = validateCodeGeneratorMap.get(type.concat(ValidateCodeGeneratorService.CODE_GENERATOR));
return (C) validateCodeGenerator.generate(request);
}
/**
* 保存驗證碼到session中
*/
private void save(ServletWebRequest request, C validateCode) {
//這裏也是封裝了一下
sessionStrategy.setAttribute(request, SESSION_KEY_PREFIX.concat(getProcessorType(request).toUpperCase()), validateCode);
}
/**
* 發送驗證碼 (只有發送驗證碼是需要自己去實現的。)
*/
protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;
/**
* 獲取請求URL中具體請求的驗證碼類型
*
*/
private String getProcessorType(ServletWebRequest request) {
// 獲取URI分割后的第二個片段 (/code/image 通過/code/ 切割后就只剩下 image
return StringUtils.substringAfter(request.getRequest().getRequestURI(), SEPARATOR);
}
}
簡單說明:
1、這裏用到了Spring一個特性就是Map<String, ValidateCodeGeneratorService> validateCodeGeneratorMap 可以把ValidateCodeGeneratorService所以的實現類都放
到這個map中,key為bean的名稱。
2、抽象類中實現了 ValidateCodeProcessor接口的processor方法,它主要是完成了驗證碼的創建、保存和發送的功能。
3、generate 方法根據傳入的不同泛型而生成了特定的驗證碼。
4、save 方法是將生成的驗證碼實例對象存入到session中,兩種驗證碼的存儲方式一致,只是有個key不一致,所以代碼也是通用的。
5、send 方法一個抽象方法,分別由ImageCodeProcessorService和SmsCodeProcessorService來具體實現,也是根據泛型來判斷具體調用哪一個具體的實現類的send方法。
上面說過驗證的生成應該也是通過實現類
ValidateCodeGeneratorService
public interface ValidateCodeGeneratorService {
/**
* 這個常量也是用來 type+CodeGeneratorService獲取對於bean對象
*/
String CODE_GENERATOR = "CodeGeneratorService";
/**
* 生成驗證碼
* 具體是圖片驗證碼 還是短信驗證碼就需要對應的實現類
*/
ValidateCode generate(ServletWebRequest request);
}
圖形驗證碼具體實現類
mageCodeGeneratorServiceImpl
@Data
@Component("imageCodeGeneratorService")
public class ImageCodeGeneratorServiceImpl implements ValidateCodeGeneratorService {
private static final String IMAGE_WIDTH_NAME = "width";
private static final String IMAGE_HEIGHT_NAME = "height";
private static final Integer MAX_COLOR_VALUE = 255;
@Autowired
private ValidateCodeProperties validateCodeProperties;
@Override
public ImageCode generate(ServletWebRequest request) {
int width = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_WIDTH_NAME, validateCodeProperties.getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_HEIGHT_NAME,validateCodeProperties.getImage().getHeight());
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
// 生成畫布
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
// 生成数字驗證碼
StringBuilder sRand = new StringBuilder();
for (int i = 0; i < validateCodeProperties.getImage().getLength(); i++) {
String rand = String.valueOf(random.nextInt(10));
sRand.append(rand);
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand.toString(), validateCodeProperties.getImage().getExpireIn());
}
/**
* 生成隨機背景條紋
*
* @param fc 前景色
* @param bc 背景色
* @return RGB顏色
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > MAX_COLOR_VALUE) {
fc = MAX_COLOR_VALUE;
}
if (bc > MAX_COLOR_VALUE) {
bc = MAX_COLOR_VALUE;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
短信驗證碼具體實現類
SmsCodeGeneratorServiceImpl
@Data
@Component("smsCodeGeneratorService")
public class SmsCodeGeneratorServiceImpl implements ValidateCodeGeneratorService {
@Autowired
private ValidateCodeProperties validateCodeProperties;
@Override
public SmsCode generate(ServletWebRequest request) {
//生成隨機數
String code = RandomStringUtils.randomNumeric(validateCodeProperties.getSms().getLength());
return new SmsCode(code, validateCodeProperties.getSms().getExpireIn());
}
}
獲取的實現類寫好了,我們在寫發送具體的發送實現類,發送類的實現類是實現AbstractValidateCodeProcessorService抽象類的。
圖片發送實現類
ImageCodeProcessorServiceImpl
@Component("imageCodeProcessorService")
public class ImageCodeProcessorServiceImpl extends AbstractValidateCodeProcessorService<ImageCode> {
private static final String FORMAT_NAME = "JPEG";
/**
* 發送圖形驗證碼,將其寫到相應中
*
* @param request ServletWebRequest實例對象
* @param imageCode 驗證碼
*/
@Override
protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
ImageIO.write(imageCode.getImage(), FORMAT_NAME, request.getResponse().getOutputStream());
}
}
短信發送具體實現類。這裏只是後台輸出就好了,實際中只要接入對於的SDK就可以了。
SmsCodeProcessorServiceImpl
@Component("smsCodeProcessorService")
public class SmsCodeProcessorServiceImpl extends AbstractValidateCodeProcessorService<SmsCode> {
private static final String SMS_CODE_PARAM_NAME = "mobile";
@Override
protected void send(ServletWebRequest request, SmsCode smsCode) throws Exception {
//這裡有一個參數也是前端需要傳來的 就是用戶的手機號
String mobile = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), SMS_CODE_PARAM_NAME);
// 這裏僅僅寫個打印,具體邏輯一般都是調用第三方接口發送短信
System.out.println("向手機號為:" + mobile + "的用戶發送驗證碼:" + smsCode.getCode());
}
整個大致就是這樣,我們再來測試一下。
獲取驗證碼接口
@RestController
@Slf4j
public class ValidateCodeController {
@Autowired
private Map<String, ValidateCodeProcessorService> validateCodeProcessorMap;
@RequestMapping("/code/{type}")
public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws Exception {
if(!StringUtils.equalsAny(type, "image", "sms")){
log.info("type類型錯誤 type={}",type);
return;
};
//根據type獲取具體的實現類
ValidateCodeProcessorService validateCodeProcessorService = validateCodeProcessorMap.get(type.concat(ValidateCodeProcessorService.CODE_PROCESSOR));
validateCodeProcessorService.processor(new ServletWebRequest(request, response));
}
}
獲取成功
獲取短信驗證碼需要多傳一個參數就是mobile 手機號碼
因為這裏發送短信沒有接第三方SDK,而是直接在控制台輸出
1、Spring Security技術棧開發企業級認證與授權(JoJo)
這一套架構設計真的非常的好,代碼可讀性和可用性都非常高,以後如果要接第三個驗證碼只要實現發送和獲取的接口來自定義實a現就好了。很受啟發!
別人罵我胖,我會生氣,因為我心裏承認了我胖。別人說我矮,我就會覺得好笑,因為我心裏知道我不可能矮。這就是我們為什麼會對別人的攻擊生氣。
攻我盾者,乃我內心之矛(20)
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計最專業,超強功能平台可客製化
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※回頭車貨運收費標準
※推薦評價好的iphone維修中心
※教你寫出一流的銷售文案?
※台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!
※台中搬家公司費用怎麼算?