【Java】模擬Sping,實現其IOC和AOP核心(二)
接著上一篇,在上一篇完成了有關IOC的註解實現,這一篇用XML的方式實現IOC,並且完成AOP。
簡易的IOC框圖
註解的方式實現了左邊的分支,那麼就剩下右邊的XML分支:
XmlContext:這個類是也是AbstractApplicationContext的子類,和AnnotationContext相似,只不過這裡是要解析XML檔案而不是註解:(關於XML檔案的解析之前給過一篇部落格:【Java】XML檔案的解析對於XML檔案的處理,是不太容易的,會產生很多問題,後面只是實現核心步驟,很多屬性就不考慮了!首先給出XmlBean,和AnnotationBean一樣,都是繼承自BeanElement
1 public class XmlBean implements BeanElement { 2 private boolean DI; 3 private Object object; 4 private Object proxy; 5 private Map<Field, String> wiredMap; 6 // key:object的為注入成員 value:依賴的className 7 // 將不能注入的成員先儲存起來 8 9 protected XmlBean() { 10 this(true, null, null); 11 } 12 13 protected XmlBean(Object object, Object proxy) { 14 this(true, object, proxy); 15 } 16 17 protected XmlBean(boolean dI, Object object, Object proxy) { 18 DI = dI; 19 this.object = object; 20 this.proxy = proxy; 21 }22 23 protected void addWiredElement(Field field, String ref) throws RepeatProperty { 24 if (wiredMap == null) { 25 wiredMap = new HashMap<>(); 26 } 27 if (wiredMap.containsKey(field)) { 28 throw new RepeatProperty(object.getClass() + "成員:" + field.getName() + "已定義!"); 29 } 30 wiredMap.put(field, ref); 31 } 32 33 protected void setDI(boolean DI) { 34 this.DI = DI; 35 } 36 37 protected Map<Field, String> getWiredMap() { 38 return wiredMap; 39 } 40 41 @Override 42 @SuppressWarnings("unchecked") 43 public <E> E getProxy() { 44 return (E) proxy; 45 } 46 47 @Override 48 public Object getObject() { 49 return object; 50 } 51 52 @Override 53 public boolean isDI() { 54 return DI; 55 } 56 57 }
XmlContext
1 public class XmlContext extends AbstractApplicationContext { 2 protected XmlContext() { 3 } 4 5 protected XmlContext(String xmlPath) { 6 innerParseXml(xmlPath); 7 } 8 9 // 和註解方式中的做法一樣,只不過產生的是XML方式的BeanElement 10 private XmlBean addXmlBean(Class<?> klass, Object object, String classId, String className) throws BeansException { 11 Object proxy = aopFactory.creatCGLibProxy(klass, object); 12 XmlBean bean = new XmlBean(object, proxy); 13 add(classId, className, bean); 14 return bean; 15 } 16 17 protected void innerParseXml(String xmlPath) { 18 // 找到根標籤 19 new XMLReader() { 20 @Override 21 public void dealElment(Element element, int index) { 22 // 處理bean標籤 23 new XMLReader() { 24 @Override 25 public void dealElment(Element element, int index) { 26 // 得到id屬性和class屬性的值 27 String classId = element.getAttribute("id"); 28 String className = element.getAttribute("class"); 29 try { 30 // 由class得到類 31 Class<?> klass = Class.forName(className); 32 // 處理constructor標籤 33 new XMLReader() { 34 @Override 35 public void dealElment(Element element, int index) { 36 // TODO 處理有引數的構造方法,這裡就會遇到許多問題,在這裡我就不處理了,後面會給出解決思路 37 } 38 }.parse(element, "constructor-arg"); 39 // 由於上面沒有處理帶引數的構造方法,這裡直接通過反射機制呼叫無參構造產生物件 40 // 並且利用產生的物件生成代理物件,最後得到Bean放入beanMap中 41 Object object = klass.newInstance(); 42 XmlBean bean = addXmlBean(klass, object, classId, className); 43 44 // 處理property標籤 45 new XMLReader() { 46 @Override 47 public void dealElment(Element element, int index) { 48 try { 49 dealProperty(element, klass, bean); 50 } catch (XmlPropertyMustNeedNameException e) { 51 e.printStackTrace(); 52 } catch (Exception e) { 53 e.printStackTrace(); 54 } 55 } 56 }.parse(element, "property"); 57 } catch (Exception e1) { 58 e1.printStackTrace(); 59 } 60 } 61 }.parse(element, "bean"); 62 } 63 }.parse(XMLReader.openDocument(xmlPath), "SimpleSpring"); 64 } 65 66 private void dealProperty(Element element, Class<?> klass, XmlBean bean) 67 throws XmlPropertyMustNeedNameException, Exception { 68 // 得到property標籤name屬性的值 69 String fieldName = element.getAttribute("name"); 70 if (fieldName.length() <= 0) { 71 throw new XmlPropertyMustNeedNameException("Bean" + klass.getName() + "的Property標籤必須宣告name屬性!"); 72 } 73 // 通過反射機制得到成員 74 Field field = klass.getDeclaredField(fieldName); 75 // 得到該成員的型別 76 Class<?> fieldType = field.getType(); 77 // 得到value屬性 78 String value = element.getAttribute("value"); 79 // 得到ref屬性 80 String ref = element.getAttribute("ref"); 81 82 // 判斷ref和value是否同時存在 83 if (value.length() > 0 && ref.length() > 0) { 84 throw new CanNotJudgeParameterException("value:" + value + " ref:" + ref + "只能存在一個!"); 85 } 86 Object arg = null; 87 // value存在,則直接通過型別轉換給成員賦值 88 if (value.length() > 0) { 89 if (!fieldType.isPrimitive() && !fieldType.equals(String.class)) { 90 throw new ValueOnlyPrimitiveType("Value只能用於八大基本型別!"); 91 } 92 // TypeConversion是我自己寫的,將字串轉換為基本型別的工具 93 arg = TypeConversion.getValue(value, fieldType.getSimpleName()); 94 field.setAccessible(true); 95 field.set(bean.getObject(), arg); 96 } 97 if (ref.length() > 0) { 98 // ref屬性存在,由於存在相互依賴關係,所以現在不做處理,只是將其儲存起來 99 // 設定該bean的狀態為尚未注入 100 bean.setDI(false); 101 bean.addWiredElement(field, ref); 102 } 103 } 104 105 }
XmlContext能做的工作也十分有限,只能完成簡單的注入,剩下的注入工作留給下一級處理!
在這裡之所以沒有處理constructor標籤,是因為對與構造方法的處理存在許多因素:比如:
1 public class Test { 2 public Test(String one, int two) { 3 ...... 4 } 5 public Test(int two, String one) { 6 ...... 7 } 8 }
通過XML檔案讀取出來的都是字串,如何區分它是字串“123”,而不是int型別123?這兩個構造方法到底執行哪個?再比如說:
1 public Test(int one, int two, Student student) { 2 ...... 3 } 4 5 public Test(String one, int two, Student student) { 6 ...... 7 } 8 9 public Test(int two, String one, Student student) { 10 ...... 11 }
通過反射機制,我們就需要得到構造方法的集合getConstructors();然後篩選出引數個數符合要求的子集,再遍歷這個子集的每一個構造方法,然後遍歷當前構造方法的所有引數,一個一個比對引數型別是否符合要求,直到找到符合要求的那一個為止,但是,如果說我們是想執行第三個構造方法,它卻找到的是第一個,完全就出問題了!所以Spring的解決辦法是給出一個type屬性
1 <bean id="xxx" class="xxx.xxx.Test"> 2 <constructor-arg idnex="0" value="1" type="int.class"> 3 <constructor-arg idnex="1" value="2" type="java.lang.String"> 4 <constructor-arg idnex="2" ref="student"> 5 </bean>
只有這樣做才能真真區分,所以以後在使用Spring的constructor標籤時,當構造方法有歧義時,一定要給出type屬性,避免出錯,也減少了查詢時的遍歷!
接下來就是最後一個類,xml分支的最高容器:ClassPathXmlApplicationContext上面的XmlContext只是完成了基本的注入問題,還有後續有關於注入之間的依賴關係,甚至是依賴迴圈(關於依賴迴圈在我的上一篇中有專門介紹,這裡就不再介紹了)
1 public class ClassPathXmlApplicationContext extends XmlContext { 2 public ClassPathXmlApplicationContext() { 3 } 4 5 public ClassPathXmlApplicationContext(String xmlPath) { 6 super(xmlPath); 7 } 8 9 public ClassPathXmlApplicationContext parseXml(String xmlPath) { 10 innerParseXml(xmlPath); 11 return this; 12 } 13 14 @Override 15 public <T> T getBean(Class<T> klass) throws BeansException { 16 String className = klass.getName(); 17 BeanElement bean = beanMap.get(className); 18 19 if (bean == null) { 20 throw new BeansException("Bean :" + klass + "不存在!"); 21 } 22 // 在這裡還是隻考慮XmlBean的注入,不考慮AnnotationBlean註解的完成情況 23 if (!bean.isDI() && bean instanceof XmlBean) { 24 autowired(className, (XmlBean)bean); 25 } 26 27 return bean.getProxy(); 28 } 29 30 private void autowired(String klassName, XmlBean bean) throws BeansException { 31 // 和AnnotationBean的解決思路一樣,先設定狀態為已注入,防止迴圈依賴的無限遞迴 32 bean.setDI(true); 33 // 得到尚未注入的成員map 34 Map<Field, String> wiredMap = bean.getWiredMap(); 35 if (wiredMap == null || wiredMap.isEmpty()) return; 36 // 遍歷map 37 for (Field field : wiredMap.keySet()) { 38 String ref = wiredMap.get(field); 39 String tagClassName = beanNameMap.get(ref); 40 // ref如果是id則在beanNameMap中找,如果是className就在beanMap中找 41 BeanElement wiredBean = tagClassName == null ? beanMap.get(ref) : beanMap.get(tagClassName); 42 if (bean == null) { 43 return; 44 } 45 if (!wiredBean.isDI() && wiredBean instanceof XmlBean) { 46 autowired(ref, (XmlBean)wiredBean); 47 } 48 field.setAccessible(true); 49 try { 50 field.set(bean.getObject(), wiredBean.getObject()); 51 } catch (Exception e) { 52 throw new BeansException(klassName + "依賴關係不正確!"); 53 } 54 } 55 wiredMap.clear(); 56 } 57 58 }
看過註解方式的話再看XML就會發現兩者其實是一回事,都是通過兩者提供的對映關係,利用反射機制完成注入!只不過兩者提供的對映關係在解析起來時各有各的特點!
Xml方式的實現這裡就簡單實現了,來看看使用情況:
1 public class StudentA { 2 String name; 3 private StudentB B; 4 5 public StudentA() { 6 } 7 8 @Override 9 public String toString() { 10 return "A:" + name + "->" + B; 11 } 12 13 } 14 15 @Component 16 public class StudentB { 17 private String name; 18 private StudentC C; 19 20 public StudentB() { 21 } 22 23 @Override 24 public String toString() { 25 return "B:" + name + "->" + C; 26 } 27 28 } 29 30 @Component 31 public class StudentC { 32 private String name; 33 private StudentA A; 34 35 public StudentC() { 36 } 37 38 @Override 39 public String toString() { 40 return "C:" + name; 41 } 42 43 }
xml的配置:
1 <SimpleSpring> 2 <bean id="haha" class="com.zc.ioc.demo.StudentA"> 3 <property name="name" value="我是A"></property> 4 <property name="B" ref="com.zc.ioc.demo.StudentB"></property> 5 </bean> 6 <bean class="com.zc.ioc.demo.StudentB"> 7 <property name="name" value="我是B"></property> 8 <property name="C" ref="com.zc.ioc.demo.StudentC"></property> 9 </bean> 10 <bean class="com.zc.ioc.demo.StudentC"> 11 <property name="name" value="我是C"></property> 12 <property name="A" ref="haha"></property> 13 </bean> 14 </SimpleSpring>
主函式:
1 public static void main(String[] args) throws BeansException { 2 // 或者是使用BeanFactory beanFactory = new ClassPathXmlApplicationContext("/test_simple_spring.xml"); 3 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/test_simple_spring.xml"); 4 StudentA bean = applicationContext.getBean(StudentA.class); 5 System.out.println(bean); 6 }
輸出:
那麼試一試註解和Xml方式的混合使用:
1 @Component 2 public class StudentA { 3 @Value(value="我是A") 4 String name; 5 @Autowired 6 private StudentB B; 7 8 public StudentA() { 9 } 10 11 @Override 12 public String toString() { 13 return "A:" + name + "->" + B; 14 } 15 16 } 17 18 @Component 19 public class StudentB { 20 @Value(value="我是B") 21 private String name; 22 @Autowired 23 private StudentC C; 24 25 public StudentB() { 26 } 27 28 @Override 29 public String toString() { 30 return "B:" + name + "->" + C; 31 } 32 33 } 34 @Component 35 public class StudentC { 36 @Value(value="我是C") 37 private String name; 38 @Autowired 39 private StudentD D; 40 41 @Autowired 42 private StudentA A; 43 44 public StudentC() { 45 } 46 47 @Override 48 public String toString() { 49 return "C:" + name + "->" + D; 50 } 51 52 } 53 54 public class StudentD { 55 private String name; 56 57 public StudentD() { 58 } 59 60 @Override 61 public String toString() { 62 return "D:" + name; 63 } 64 65 }
Xml配置:
1 <SimpleSpring> 2 <bean class="com.zc.ioc.demo.StudentD"> 3 <property name="name" value="我是D"></property> 4 </bean> 5 </SimpleSpring>
主函式:
1 public static void main(String[] args) throws BeansException { 2 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/test_simple_spring.xml"); 3 StudentD studentD = applicationContext.getBean(StudentD.class); 4 System.out.println(studentD); 5 6 applicationContext= new AnnotationConfigApplicationContext("com.zc.moedl"); 7 StudentA studentA = applicationContext.getBean(StudentA.class); 8 System.out.println(studentA); 9}
輸出結果:
看起來是沒有問題了,但是如果Xml和註解之間的出現順序不同,結果也會不一樣,還得仔細考慮,而且我做的這個是延遲注入,只有在getBean的時候才會完成最後的注入,並且若是註解中需要一個Xml的bean注入,而xml的這個bean又依賴於註解中的一個bean,那麼這套方法是不可行的!
AOP前面多次談到AOP,以及我們的Bean是通過原始物件+代理物件,這裡來看看AOP部分的實現:AOP說到底主要目的不是產生代理物件,而是要通過代理物件執行方法,並對方法進行有效的攔截!簡單起見,將攔截分為置前,置後,以及出現異常時的攔截。而攔截又是何時產生的?還是為了簡單實現,後面都只使用CGLibProxy,有關CGLib的代理我在上一篇有介紹,這裡也就不累贅了。
關於攔截器的產生,我之前的實現方式是給要攔截的方法添加註解,給出攔截Id,然後提供一套方法,給指定Id號的方法建立攔截器,但是,在知道Spring的處理後,這種方式很快被否定了!在工程中,往往很多需要攔截的方法是不允許侵入式修改的,又或者是被打成了jar包,那麼就更不可能對其添加註解,所以給出新的解決思路: 由使用者自己寫一個方法,然後給這個方法添加註解,使其和要攔截的方法產生對對映關係,這樣我們實際執行的攔截器方法完全是由使用者提供,並不會干預原始碼! 前面說過只是處理置前,置後,以及出現異常時的攔截,所以會給出三種不同的註解,用於區分!由於是要使用註解,那麼就要用到包掃描【Java】包、jar包的掃描包掃描就需要對類進行區分,只處理帶有標識的類,所以還缺少一個對類的註解:
@Aspect
1 @Retention(RetentionPolicy.RUNTIME) 2 @Target(ElementType.TYPE) 3 public @interface Aspect { 4 }
這個註解只是為了表明這個類存放著使用者編寫的攔截器方法!
主要的是下面三個註解:@Before
1 @Retention(RetentionPolicy.RUNTIME) 2 @Target(ElementType.METHOD) 3 public @interface Before { 4 Class<?> klass(); 5 String method(); 6 }
置前攔截方法的註解,klass表明對哪個類進行置前攔截,method表明對哪個方法進行攔截,但發現僅僅通過這好像不能找到具體的方法,但仔細想一想,置前攔截是對要攔截的方法引數進行判斷,使用者在編寫攔截時必然知道攔截的方法是什麼,引數個數和型別當然也知道,那我們只要讓使用者寫的方法的引數和要攔截的方法引數保持一致就行了,如果不一致,就異常處理!這樣就能通過使用者編寫的方法,知道被攔截的方法引數,進而定位到具體要攔截的方法!
@After
1 @Retention(RetentionPolicy.RUNTIME) 2 @Target(ElementType.METHOD) 3 public @interface After { 4 Class<?> klass(); 5 String method(); 6 Class<?>[] parameterTypes() default {}; 7 }
置後攔截方法的註解,同置前攔截一樣,klass表明對哪個類進行置前攔截,method表明對哪個方法進行攔截。由於之後攔截是對方法執行結果的操作,使用者寫的方法的引數有且只有一個,且引數型別要與原方法的返回值型別匹配,這樣,我們就不能像處理@Before時一樣,必須申明被攔截的方法的引數型別,只有這樣才能定位到具體的被攔截方法!
@Throwable
1 @Retention(RetentionPolicy.RUNTIME) 2 @Target(ElementType.TYPE) 3 public @interface Throwable { 4 Class<?> klass(); 5 String method(); 6 Class<?>[] parameterTypes(); 7 }
出現異常時攔截的註解,和@After一樣,它只是處理異常,那麼使用者所提供的方法的引數有且只有一個,型別是執行被攔截方法產生的異常型別,它也必須傳遞被攔截方法的引數,使其定位到具體的被攔截方法!
其實在Spring裡面使用了更為犀利的手段,它並沒有使用Class<?> 只是使用了一個字串就解決了類、方法、引數的定位,只不過它就需要對字串解析,利用了正則表示式,雖然比我的方法繁瑣,但面向使用者的使用是十分友好的!
AOP框圖
看起來我把它做的很複雜,事實上這裡用到的全是介面,是非常靈活的,如果說不想使用這套方式,那麼可以自己實現Advice介面;如果說攔截器鏈用的時list儲存,以後想更換為連結串列也是可以的;攔截器的產生不想使用上面說的註解方式,那麼自己去實現IntercepterFactory介面!
AopFactory
1 public class AopFactory { 2 private Advice advice; 3 4 public AopFactory() { 5 } 6 7 public AopFactory setAdvice(Advice advice) { 8 this.advice = advice; 9 return this; 10 } 11 12 public <E> E creatCGLibProxy(Class<?> klass) throws Exception { 13 return creatCGLibProxy(klass, klass.newInstance()); 14 } 15 16 public <E> E creatCGLibProxy(Object object) {相關推薦
【Java】模擬Sping,實現其IOC和AOP核心(二)
接著上一篇,在上一篇完成了有關IOC的註解實現,這一篇用XML的方式實現IOC,並且完成AOP。 簡易的IOC框圖 註解的方式實現了左邊的分支,那麼就剩下右邊的XML分支: XmlContext:這個類是也是AbstractApplicationContext的子類,和AnnotationContext
【Java】模擬Sping,實現其IOC和AOP核心(一)
在這裡我要實現的是Spring的IOC和AOP的核心,而且有關IOC的實現,註解+XML能混合使用! 參考資料: IOC:控制反轉(Inversion of Control,縮寫為IoC),是面向物件程式設計中的一種設計原則,可以用來減低計算機程式碼之間的耦合度。其中最常見的方式叫做依賴注入(D
【轉載】Vue 2.x 實戰之後臺管理系統開發(二)
null element asc 其他 就會 ans 目錄 asi all 2. 常見需求 01. 父子組件通信 a. 父 -> 子(父組件傳遞數據給子組件) 使用 props,具體查看文檔 - 使用 Prop 傳遞數據(cn.vuejs.org/v2/guide
【python】爬蟲篇:python對於html頁面的解析(二)
我,菜雞,有什麼錯誤,還望大家批評指出!! 前言: 根據自己寫的上一篇文章,我繼續更第二部分的內容,詳情請點選如下連結 【python】爬蟲篇:python連線postgresql(一):https://blog.csdn.net/lsr40/article/details/833118
安裝Centos7 、 Mysql8 叢集,實現讀寫分離 高可用(二)-- 資料庫安裝篇
零、寫在前面 為了嘗試新技術和業務需要,因此打算搞一套這樣的環境玩兒一下 伺服器的安裝請見上一篇一、硬體規劃 我用的是自己的本機(8核、16G),劃分出三臺虛擬機器(一主兩從--MyS
【CSS】淺談css中格式化上下文BFC、IFC(二)
Inline Formatting Context Inline Formatting Context的縮寫就是IFC。中文名叫,行內格式化上下文。行內框參與IFC。 什麼是行框? 在IFC中,每個框都是一個接一個地水平排列,起點是包含塊的頂部,水平方向
【統計】SQL中的case when then else end用法(二)
轉載自:https://www.cnblogs.com/prefect/p/5746624.html(一)和(二)的文章開頭一樣,後面例子不一樣Case具有兩種格式。簡單Case函式和Case搜尋函式。--簡單Case函式 CASE sex WHEN '1'
【Java】Red5伺服器搭建(實現線上直播,流媒體視訊播放)
引言 流媒體檔案是目前非常流行的網路媒體格式之一,這種檔案允許使用者一邊下載一邊播放,從而大大減少了使用者等待播放的時間。另外通過網路播放流媒體檔案時,檔案本身不會在本地磁碟中儲存,這樣就節省了大量的磁碟空間開銷。正是這些優點,使得流媒體檔案被廣泛應用於網路播放。 流媒體伺服
【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】認識 JDK,JRE,JVM
JDK,JRE,JVM 今天我們討論下這三個Java工具 JDK 全稱Java Development ToolKit(Java 開發工具包)。 JDK是整個JAVA的核心,其包括了Java執行環境(Java Runtime Envirnment)
【Java】基於jsoup爬蟲實現(從智聯獲取工作資訊)
這幾天在學習Java解析xml,突然想到Dom能不能解析html,結果試了半天行不通,然後就去查了一些資料,發現很多人都在用Jsoup解析html檔案,然後研究了一下,寫了一個簡單的例項,感覺還有很多地方需要潤色,在這裡分享一下我的例項,歡迎交流指教!後續想通過Java把資料匯入到Excel或者
【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】動態代理+ThreadLocal實現資料來源及事務管理
一、前言 小demo只是思想的一個簡單實現,距離用在生產環境還有一定距離,只是五一勞動節放假宅在家用來鍛鍊一下思維,不嚴謹的地方還望見諒。想要看更完整的示例程式碼,請檢視mybatis原始碼,pooled類的實現。 二、難點分析 JDBC資
【java】mysql+springMvc+easyui實現圖片的儲存和讀取顯示
需求描述 公司之前設計的稽核流程,稽核人一欄使用的是文字資訊。現根據甲方最新需求,在列印審批單時,需要在稽核人一欄顯示手寫簽名。 設計思路 設計獨立的簽名儲存模組 將使用者與簽名圖片進行關聯 將圖片資訊以blob型別儲存在資料庫中(因為本次需要儲存的
【JAVA】jdk安裝,mac | windows | linux
tools.jar inux ica jdk安裝 code 添加 1.8 追加 環境變量 一、mac 1、jdk查看路徑: /usr/libexec/java_home -V 2、環境變量: export JAVA_HOME=/Library/Java/JavaVirtua