【java】動態代理+ThreadLocal實現資料來源及事務管理
一、前言
小demo只是思想的一個簡單實現,距離用在生產環境還有一定距離,只是五一勞動節放假宅在家用來鍛鍊一下思維,不嚴謹的地方還望見諒。想要看更完整的示例程式碼,請檢視mybatis原始碼,pooled類的實現。
二、難點分析
JDBC資料來源的規範來自java.sql.DataSource介面,裡面就一個方法getConnection的兩個過載(資料來源的思想也已經很普遍了,這裡就不再贅述,需要了解的請自行查詢),所以資料來源的首要任務就是提供資料庫連線物件Connection,這個太容易實現了,毫無難點可言,但是獲得了連線,使用完之後是需要關閉的,此時連線的關閉不是釋放資源,而是將連線物件還回資料庫連線池。
難點一:連線物件的關閉不是釋放資源,而是將連線物件還回資料庫連線池——close方法的改寫
上面的描述太過隨意浮躁,沒有體現難點之所在。java.sql.Connection作為資料庫連線的規範介面而存在,他規定了連線物件的兩大功能,一是和資料庫建立連結,而是為此連結建立執行物件,不同的資料庫廠商實現有不同,但是規範都得遵守,無論是mysql還是Oracle,他們提供的資料庫驅動包中對java.sql.Connection的實現都要按照規則來,否則程式設計師們就不樂意了,不好進行開發、不好進行維護啊。那就看一下規範,java.sql.Connection介面中對close方法的描述。
是不是粗大事了?不是等待啊,是立即釋放連結物件和JDBC資源,很強烈的語氣助詞“immediately”和“strongly”。釋放資源,也就是關閉和資料庫的連結,不要Connection物件了,口說無憑,直接看mysql驅動包中close方法的實現。/** * Releases this <code>Connection</code> object's database and JDBC resources * immediately instead of waiting for them to be automatically released. * <P> * Calling the method <code>close</code> on a <code>Connection</code> * object that is already closed is a no-op. * <P> * It is <b>strongly recommended</b> that an application explicitly * commits or rolls back an active transaction prior to calling the * <code>close</code> method. If the <code>close</code> method is called * and there is an active transaction, the results are implementation-defined. * <P> * * @exception SQLException SQLException if a database access error occurs */ void close() throws SQLException;
附送更詳細的小文連結一枚:點選開啟連結
/**
* In some cases, it is desirable to immediately release a Connection's
* database and JDBC resources instead of waiting for them to be
* automatically released (cant think why off the top of my head) <B>Note:</B>
* A Connection is automatically closed when it is garbage collected.
* Certain fatal errors also result in a closed connection.
*
* @exception SQLException
* if a database access error occurs
*/
public void close() throws SQLException {
synchronized (getConnectionMutex()) {
if (this.connectionLifecycleInterceptors != null) {
new IterateBlock<Extension>(this.connectionLifecycleInterceptors.iterator()) {
void forEach(Extension each) throws SQLException {
((ConnectionLifecycleInterceptor)each).close();
}
}.doForAll();
}
realClose(true, true, false, null);
}
}
/**
* Closes connection and frees resources.
*
* @param calledExplicitly
* is this being called from close()
* @param issueRollback
* should a rollback() be issued?
* @throws SQLException
* if an error occurs
*/
public void realClose(boolean calledExplicitly, boolean issueRollback,
boolean skipLocalTeardown, Throwable reason) throws SQLException {
SQLException sqlEx = null;
if (this.isClosed()) {
return;
}
this.forceClosedReason = reason;
try {
if (!skipLocalTeardown) {
if (!getAutoCommit() && issueRollback) {
try {
rollback();
} catch (SQLException ex) {
sqlEx = ex;
}
}
reportMetrics();
if (getUseUsageAdvisor()) {
if (!calledExplicitly) {
String message = "Connection implicitly closed by Driver. You should call Connection.close() from your code to free resources more efficiently and avoid resource leaks.";
this.eventSink.consumeEvent(new ProfilerEvent(
ProfilerEvent.TYPE_WARN, "", //$NON-NLS-1$
this.getCatalog(), this.getId(), -1, -1, System
.currentTimeMillis(), 0, Constants.MILLIS_I18N,
null,
this.pointOfOrigin, message));
}
long connectionLifeTime = System.currentTimeMillis()
- this.connectionCreationTimeMillis;
if (connectionLifeTime < 500) {
String message = "Connection lifetime of < .5 seconds. You might be un-necessarily creating short-lived connections and should investigate connection pooling to be more efficient.";
this.eventSink.consumeEvent(new ProfilerEvent(
ProfilerEvent.TYPE_WARN, "", //$NON-NLS-1$
this.getCatalog(), this.getId(), -1, -1, System
.currentTimeMillis(), 0, Constants.MILLIS_I18N,
null,
this.pointOfOrigin, message));
}
}
try {
closeAllOpenStatements();
} catch (SQLException ex) {
sqlEx = ex;
}
if (this.io != null) {
try {
this.io.quit();
} catch (Exception e) {
;
}
}
} else {
this.io.forceClose();
}
if (this.statementInterceptors != null) {
for (int i = 0; i < this.statementInterceptors.size(); i++) {
this.statementInterceptors.get(i).destroy();
}
}
if (this.exceptionInterceptor != null) {
this.exceptionInterceptor.destroy();
}
} finally {
this.openStatements = null;
if (this.io != null) {
this.io.releaseResources();
this.io = null;
}
this.statementInterceptors = null;
this.exceptionInterceptor = null;
ProfilerEventHandlerFactory.removeInstance(this);
synchronized (getConnectionMutex()) {
if (this.cancelTimer != null) {
this.cancelTimer.cancel();
}
}
this.isClosed = true;
}
if (sqlEx != null) {
throw sqlEx;
}
}
總而言之,和資料庫建立連結的資源都被賦值為null了,就差直接給Connection=null,實際上這段程式碼在JDK API直接就直接附送了,關閉連線的時候不要忘了null一下。 那麼,難點就應該浮現出來了,呼叫Connection物件的close方法不是還回池中,而是直接關閉,因此需要對close方法進行改寫。不要說在自定義的資料來源中新增close方法,規範裡面沒有,而且對於使用的開發人員來說也不友好。
再來說說事務,JDBC來進行事務開發,API很簡單啊,就那麼幾個固定的方法,頂多需要關注的就是程式的隔離級別和資料庫支援的隔離級別的差異,但是這是基於Connection在方法中建立並銷燬,如果採用連線池,那麼Connection物件怎麼在多執行緒中保持安全?不能我從池中拿了一個Connection,別人也從池中獲取到同一個Connection,而且更重要的是,我拿到這個Connection之後,放在哪裡?存放的地方應該是我能訪問而別人不能訪問。
難點二:多執行緒環境下保證Connection物件的安全——Connection的獲取和存放
要想在多執行緒中進行事務處理,就需要藉助於ThreadLocal物件(這貨是引子,拋磚引玉的引子,真正存放東西的是每個執行緒中的ThreadLocalMap,這個是ThreadLocal定義的靜態內部類,真正存放執行緒區域性變數的地方,具體請見另一篇轉載的部落格:《深入理解ThreadLocal》),一個執行緒一個Connection,執行緒的生命週期中無論何時拿到的都是同一個Connection(因為本來就是存在Thread的例項屬性ThreadLocal.threadLocalMap中),具體詳見下面的詳解,最後一節附上demo的所有原始碼。
基本就那麼多了,彙總一下整個過程,從自定義的資料來源物件中獲取連線物件,然後開啟事務,執行完成之後,close將連線物件還回池中。
三、實現詳解
下面,就來一一破解上述難點。
1. Connection close方法改寫
小demo這麼來實現,通過DBPoll的getProxConnection方法來進行改寫,在DBPool物件初始化的時候,就已經例項化一定的Connection物件,並將這些Connection物件改寫了close方法的代理存入一個List,這個List就模擬了連線池的思想。程式碼節選如下,完成的demo程式碼以及註釋參看第四節。
package cn.wxy.pool;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.ArrayList;
import cn.wxy.utils.DBUtils;
/**
* 為了簡便實現,實現採用了單例,並沒有像mybatis那樣做成多環境
* @author Administrator
*/
public class DBPool {
/**
* threadLocal:執行緒區域性變數,事務控制的起點
* pool:用於存放初始化的連線物件
* poolSize:初始化的連線數
*/
private ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
private ArrayList<Connection> pool = new ArrayList<Connection>();
private static int poolSize = 2;
/**
* 預設構造方法
*/
private DBPool() {
init();
}
/**
* 如果要自定義連線池大小,則應該在獲取連線池物件之前呼叫該方法
* @param poolSize
*/
public static void initPoolSize(int poolSize){
DBPool.poolSize = poolSize;
}
/**
* 靜態內部類實現單例
* @author Administrator
*/
private static class InnerDBPool {
private static DBPool pool = new DBPool();
}
/**
* 全域性訪問介面
* @return
*/
public static DBPool getPool(){
return InnerDBPool.pool;
}
/**
* 初始化方法
* 從DBUtils中獲取連線的代理物件,並進行快取備用
*/
public void init(){
for(int i=0; i<poolSize; i++){
pool.add(getProxConnection());
}
}
/**
* 採用JDK動態代理的方式獲取連結物件的單例
* 注意:
* 1. JDK動態代理只能對介面進行代理,因此返回值只能轉成介面,而不能轉成具體的類;
* 2. 第二參個數是代理的介面陣列,可以採用getClass.getIntefaces()和以下實現兩種方式
* 但是根據提供的資料庫驅動包,getInterfaces()方法可能出現異常,因此為了更好的通用性,建議採用以下實現的方式
* @return
*/
private Connection getProxConnection(){
Connection conn = DBUtils.getDbUtils().getRealConnection();
return (Connection) Proxy.newProxyInstance(conn.getClass().getClassLoader(),
new Class[]{Connection.class},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if("close".equals(method.getName())){
synchronized (pool) {
/**
* 此處應該從執行緒區域性變數中獲取,而不是延用方法中的conn物件
* 方法中的conn物件是原始的連線物件,其close方法是關閉資源,而不是還回池中
*/
Connection proxyConn = threadLocal.get();
threadLocal.remove();
if(pool.contains(proxyConn) || proxyConn == null)
return null;
return pool.add(proxyConn);
}
}
return method.invoke(conn, args);
}
});
}
}
通過JDK動態代理來進行改寫,執行緒池中存放不再是原始的Connection物件,而是Connection物件的代理例項,close方法也不再是釋放資源,而是將Connection代理還回連線池的資料結構list中。2. Connection的執行緒安全
要保證Connection在多執行緒中的安全,那麼從連結池中獲取到Connection的時候,就要將此連線物件放入當前執行緒的執行緒區域性變數中,同一個執行緒任何時候獲取連線,都不應該直接從連線池中獲取,而是優先從執行緒區域性變數中獲取,否則視為第一次獲取Connection物件,從連線池中獲取並放入執行緒區域性變數。關於執行緒區域性變數的原理,請參看另一篇博文,部分程式碼節選如下,完成的demo原始碼在第四節有下載連線。
package cn.wxy.pool;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.util.ArrayList;
import cn.wxy.utils.DBUtils;
/**
* 為了簡便實現,實現採用了單例,並沒有像mybatis那樣做成多環境
* @author Administrator
*/
public class DBPool {
/**
* 獲取連結物件:任何時候要使用連線物件都應該從此獲取,而不應該從DBUtils中獲取
* 1. 從當前執行緒區域性變來那個中獲取連結物件,否則到2
* 2. 從執行緒池中獲得連線物件,否則異常
* @return
*/
public Connection getConnection(){
Connection conn = threadLocal.get();
if(conn != null)
return conn;
if(pool.size() > 0){
synchronized (pool) {
if(pool.size() > 0){
conn = pool.remove(0);
threadLocal.set(conn);
return conn;
}
}
}
throw new RuntimeException("當前執行緒池已經用完,請稍候使用!");
}
}
獲得了連線物件,要想進行事務控制就太簡單了,本例中事務控制的原始碼如下。
package cn.wxy.utils;
import java.sql.Connection;
import java.sql.SQLException;
import cn.wxy.pool.DBPool;
public class TransactionManager {
private static DBPool pool = DBPool.getPool();
/**
* 開啟事務,即關閉自動提交
*/
public static void startTransaction(){
Connection conn = pool.getConnection();
if(conn != null)
try {
conn.setAutoCommit(false);
System.out.println("事務開啟········");
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 1. 提交事務
* 2. 將連線的事務重置成自動提交,因為連線不關閉,而是還回池中,因此需要還原狀態
*/
public static void commitTransaction(){
Connection conn = pool.getConnection();
if(conn != null)
try {
conn.commit();
conn.setAutoCommit(true);
System.out.println("事務提交···");
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 1. 事務回滾
* 2. 重置事務狀態為自動提交,理由同上
*/
public static void rollbackTransaction(){
Connection conn = pool.getConnection();
if(conn != null)
try {
conn.rollback();
conn.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void releaseConnection(){
try {
pool.getConnection().close();
System.out.println("連線還回池中···");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
拿到連線進行事務控制相對簡單,但是要在service編碼人員無感的情況下織入到程式碼中,就需要利用AOP程式設計,此處也是採用JDK動態代理來實現,通過工廠類返回被改寫的Service代理物件,從而完成事務的動態織入,部分原始碼節選如下所示,完整的demo原始碼在第四節。
package cn.wxy.utils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ResourceBundle;
/**
* 物件工廠
* 從中獲取需要被代理的物件service
* 從中獲取dao物件
* 通過ResourceBundle實現簡單的熱載入,以犧牲效能為代價
* @author Administrator
*/
public class BeanFactory {
private String defaultConfigFileName = "beanFactory-config";
/**
* 單例工廠
*/
private BeanFactory(){
}
/**
* 靜態內部類實現單例工廠
* @author Administrator
*/
private static class InnerBeanFactory {
private static BeanFactory factory = new BeanFactory();
}
/**
* 獲取工廠全域性介面
* @return
*/
public static BeanFactory getFactory(){
return InnerBeanFactory.factory;
}
/**
* 判斷獲取bean傳入的bean name引數
* @param beanName
* @return
*/
public boolean checkBeanName(String beanName){
if(beanName == null || beanName.trim().length() < 1)
return false;
return true;
}
public <T> T getBean(String beanName){
if(!checkBeanName(beanName))
return null;
if(beanName.endsWith("Service")){
return getServiceBean(beanName);
}
return getOtherBean(beanName);
}
/**
* 如果是返回的是Service,則需要通過JDK動態代理進行事務的新增,然後再返回
* @param beanName
* @return
*/
@SuppressWarnings("unchecked")
private <T> T getServiceBean(String beanName){
try {
T t = (T) Class.forName(ResourceBundle.getBundle(defaultConfigFileName).getString(beanName)).newInstance();
return (T) Proxy.newProxyInstance(t.getClass().getClassLoader(), t.getClass().getInterfaces(), new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
try {
TransactionManager.startTransaction();
Object retValue = method.invoke(t, args);
TransactionManager.commitTransaction();
return retValue;
} catch (Exception e) {
TransactionManager.rollbackTransaction();
} finally {
TransactionManager.releaseConnection();
}
return null;
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 非service的bean直接反射返回,因為不需要新增事務
* @param beanName
* @return
*/
@SuppressWarnings("unchecked")
private <T> T getOtherBean(String beanName){
try {
return (T) Class.forName(ResourceBundle.getBundle(defaultConfigFileName).getString(beanName)).newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
最後,附上部分測試程式碼以及測試結果。
package cn.wxy.test;
import java.sql.Connection;
import java.sql.SQLException;
import org.junit.Test;
import cn.wxy.dao.UserDao;
import cn.wxy.domain.User;
import cn.wxy.pool.DBPool;
import cn.wxy.service.UserService;
import cn.wxy.utils.BeanFactory;
import cn.wxy.utils.DBUtils;
public class DBTest {
/**
* 測試連線池
*/
@Test
public void testPool(){
//DBPool.initPoolSize(3);
DBPool pool = DBPool.getPool();
pool.getPoolInfo();
pool.travelPoll();
Connection c1 = pool.getConnection();
Connection c2 = pool.getConnection();
System.out.println(c1 == c2);
pool.getPoolInfo();
pool.travelPoll();
try {
c1.close();
c2.close();
} catch (SQLException e) {
e.printStackTrace();
}
pool.getPoolInfo();
pool.travelPoll();
/**
* 測試結果
* 初始化大小:2, 當前連線池大小:2
[email protected]
[email protected]
true
初始化大小:2, 當前連線池大小:1
[email protected]
初始化大小:2, 當前連線池大小:2
[email protected]
[email protected]
*/
}
/**
* 測試bean factory
*/
@Test
public void testFactory(){
BeanFactory factory = BeanFactory.getFactory();
UserDao userDao = factory.getBean("userDao");
UserService userService = factory.getBean("userService");
userDao.insert(null);
userService.addUser(null);
/*執行結果:
impl insert
事務開啟········
user service impl
事務提交···
連線還回池中···*/
}
/**
* demo整體測試
*/
@Test
public void test(){
User user1 = new User(null, "插入人1", "123", "male", 23);
User user2 = new User(null, "插入人2", "234", "female", 22);
UserService userService = BeanFactory.getFactory().getBean("userService");
userService.addUser(user1, user2);
}
}
正常操作控制檯輸出結果:
事務開啟········
事務提交···
連線還回池中···
資料庫記錄顯式截圖如下:
異常操作,控制檯輸出以及資料截圖檢測事務是否控制成功,詳情如下。
事務開啟········
事務回滾了···
連線還回池中···
至此,小demo結束,謝謝觀賞!
四、原始碼下載
原始碼下載:點選開啟連結
演示demo截圖
附註:
本文如有錯漏,煩請不吝指正,謝謝!
演示demo程式碼簡陋,還望見諒!
相關推薦
【java】動態代理+ThreadLocal實現資料來源及事務管理
一、前言 小demo只是思想的一個簡單實現,距離用在生產環境還有一定距離,只是五一勞動節放假宅在家用來鍛鍊一下思維,不嚴謹的地方還望見諒。想要看更完整的示例程式碼,請檢視mybatis原始碼,pooled類的實現。 二、難點分析 JDBC資
【轉】動態代理實現AOP
今天說和小張哥一起討論AOP,正好看到了相關的視訊,今天就總結一下AOP是如何使用動態代理來實現的。 AOP對JAVA程式設計師來說並不陌生,他是spring的一個核心內容——面向切面程式設計,先把概念放在這裡,因為這一篇部落格不會展開講述AOP是什麼
java中動態代理的實現
pan ack ger data- 動態代理 bind 使用 intercept framework 動態代理的實現 使用的模式:代理模式。代理模式的作用是:為其他對象提供一種代理以控制對這個對象的訪問。類似租房的中介。 兩種動態代理:(1)jdk動態代理,jdk動態代理是
【Java】Swing+IO流實現一個簡單的文件加密程序
als oncommand override fault 源文件 abs directory imp select EncrytService package com.my.service; import java.io.File; import java
【Java】Swing+IO流實現一個簡單的文件加密程序(較完整版)
move 初始 baidu images 文件選擇器 while login 一個 ktr 留著參考 beans package com.my.bean; import java.io.Serializable; public class
【Java】使用Atomic變數實現鎖
Atomic原子操作 Java從JDK1.5開始提供了java.util.concurrent.atomic包,方便程式設計師在多執行緒環境下,無鎖的進行原子操作。原子變數的底層使用了處理器提供的原子指令,但是不同的CPU架構可能提供的原子指令不一樣,也有可能需要某種形式的內部鎖,所
【Java】模擬Sping,實現其IOC和AOP核心(一)
在這裡我要實現的是Spring的IOC和AOP的核心,而且有關IOC的實現,註解+XML能混合使用! 參考資料: IOC:控制反轉(Inversion of Control,縮寫為IoC),是面向物件程式設計中的一種設計原則,可以用來減低計算機程式碼之間的耦合度。其中最常見的方式叫做依賴注入(D
【Java】基於jsoup爬蟲實現(從智聯獲取工作資訊)
這幾天在學習Java解析xml,突然想到Dom能不能解析html,結果試了半天行不通,然後就去查了一些資料,發現很多人都在用Jsoup解析html檔案,然後研究了一下,寫了一個簡單的例項,感覺還有很多地方需要潤色,在這裡分享一下我的例項,歡迎交流指教!後續想通過Java把資料匯入到Excel或者
【Java】模擬Sping,實現其IOC和AOP核心(二)
接著上一篇,在上一篇完成了有關IOC的註解實現,這一篇用XML的方式實現IOC,並且完成AOP。 簡易的IOC框圖 註解的方式實現了左邊的分支,那麼就剩下右邊的XML分支: XmlContext:這個類是也是AbstractApplicationContext的子類,和AnnotationContext
【JAVA】Srping和JDBC實現資料庫操作
前言 建立資料庫 首先建立我們的資料庫(這裡我使用的是Mysql),為了演示方便,我這裡簡單的建立一個spring資料庫,然後資料庫有一個user使用者表: 建立一個名為spring的資料庫。 建立一個名為user的資料表,表包括id、email、name
【JAVA】Spring和JdbcTemplate實現資料庫操作
前言 看完【JAVA】Srping和傳統JDBC實現資料庫操作之後,是否覺得傳統的JDBC太繁瑣了,就算是隻寫一個簡單的資料庫插入功能都要寫好多與業務無關的程式碼,那麼使用spring封裝的JdbcTemplate就很有必要了,當然JdbcTemplate也是
【java】foreach是如何實現的?
1.正文 因為想要了解編譯器是如何實現foreach功能的,就先寫一個foreach迴圈,看看位元組碼長啥樣。 public class ForEach { List<String> list; public void display1(){ f
【Java】SpringMVC整合poi實現excel的匯入匯出
2.特點:結構: HSSF - 提供讀寫Microsoft Excel格式檔案的功能。 XSSF - 提供讀寫Microsoft Excel OOXML格式檔案的功能。 HWPF - 提供讀寫Microsoft Word格式檔案的功能。 HSLF - 提供讀寫Microsof
【JAVA】使用 iText XMLWorker實現HTML轉PDF
使用 iText XML Worker實現HTML轉PDF package com.yfli.iText; import java.io.FileInputStream; import java.i
【Java】重入鎖 實現原理
ReentrantLock 是java繼synchronized關鍵字之後新出的執行緒鎖,今天看了看實現原始碼。主要是通過自旋來實現的。使用自旋的基本思路就是為所有的執行緒構建一個node,連成一個佇列,然後每一個node都輪詢前驅節點,如果前驅已經釋放鎖了,那麼當前階段就
【Java】Red5伺服器搭建(實現線上直播,流媒體視訊播放)
引言 流媒體檔案是目前非常流行的網路媒體格式之一,這種檔案允許使用者一邊下載一邊播放,從而大大減少了使用者等待播放的時間。另外通過網路播放流媒體檔案時,檔案本身不會在本地磁碟中儲存,這樣就節省了大量的磁碟空間開銷。正是這些優點,使得流媒體檔案被廣泛應用於網路播放。 流媒體伺服
【JAVA】常用加解密演算法總結及JAVA實現【BASE64,MD5,SHA,DES,3DES,AES,RSA】
BASE64 這其實是一種編解碼方法,但是隻要我們能夠將原文變成肉眼不可識別的內容,其實就是一種加密的方法。 BASE64 的編碼都是按字串長度,以每 3 個 8 bit 的字元為一組,然後針對每組,首先獲取每個字元的 ASCII 編碼,然後將 ASCII 編碼轉換成 8
【java】mysql+springMvc+easyui實現圖片的儲存和讀取顯示
需求描述 公司之前設計的稽核流程,稽核人一欄使用的是文字資訊。現根據甲方最新需求,在列印審批單時,需要在稽核人一欄顯示手寫簽名。 設計思路 設計獨立的簽名儲存模組 將使用者與簽名圖片進行關聯 將圖片資訊以blob型別儲存在資料庫中(因為本次需要儲存的
java使用動態代理來實現AOP(日誌記錄)的例項程式碼
下面是一個AOP實現的簡單例子: 首先定義一些業務方法: 複製程式碼程式碼如下: /** * Created with IntelliJ IDEA. * Author: wangjie email:[email protected] * Date: 13-9-23 * T
【Java】手把手理解CAS實現原理
先來看看概念,【CAS】 全稱“CompareAndSwap”,中文翻譯即“比較並替換”。 定義:CAS操作包含三個運算元 —— 記憶體位置(V),期望值(A),和新值(B)。 如果記憶體位置的值與期望值匹配,那麼處理器會自動將該位置值更新為新值。否則, 處理器不作任何操作。無論哪種情況,它都會在C