Java設(shè)計(jì)模式之代理模式原理及實(shí)現(xiàn)代碼分享
簡(jiǎn)介
Java編程的目標(biāo)是實(shí)現(xiàn)現(xiàn)實(shí)不能完成的,優(yōu)化現(xiàn)實(shí)能夠完成的,是一種虛擬技術(shù)。生活中的方方面面都可以虛擬到代碼中。代理模式所講的就是現(xiàn)實(shí)生活中的這么一個(gè)概念:中介。
代理模式的定義:給某一個(gè)對(duì)象提供一個(gè)代理,并由代理對(duì)象控制對(duì)原對(duì)象的引用。
代理模式包含如下角色:
ISubject:抽象主題角色,是一個(gè)接口。該接口是對(duì)象和它的代理共用的接口。
RealSubject:真實(shí)主題角色,是實(shí)現(xiàn)抽象主題接口的類。
Proxy:代理角色,內(nèi)部含有對(duì)真實(shí)對(duì)象RealSubject的引用,從而可以操作真實(shí)對(duì)象。代理對(duì)象提供與真實(shí)對(duì)象相同的接口,以便在任何時(shí)刻都能代替真實(shí)對(duì)象。同時(shí),代理對(duì)象可以在執(zhí)行真實(shí)對(duì)象操作時(shí),附加其他的操作,相當(dāng)于對(duì)真實(shí)對(duì)象進(jìn)行封裝。
實(shí)現(xiàn)動(dòng)態(tài)代理的關(guān)鍵技術(shù)是反射。
靜態(tài)代理
代理模式有幾種,虛擬代理,計(jì)數(shù)代理,遠(yuǎn)程代理,動(dòng)態(tài)代理。主要分為兩類,靜態(tài)代理和動(dòng)態(tài)代理。靜態(tài)代理比較簡(jiǎn)單,是由程序員編寫的代理類,并在程序運(yùn)行前就編譯好的,而不是由程序動(dòng)態(tài)產(chǎn)生代理類,這就是所謂的靜態(tài)。
考慮這樣的場(chǎng)景,管理員在網(wǎng)站上執(zhí)行操作,在生成操作結(jié)果的同時(shí)需要記錄操作日志,這是很常見的。此時(shí)就可以使用代理模式,代理模式可以通過(guò)聚合和繼承兩種方式實(shí)現(xiàn):
/**方式一:聚合式靜態(tài)代理
* @author Goser (mailto:goskalrie@163.com)
* @Since 2016年9月7日
*/
//1.抽象主題接口
public interface Manager {
void doSomething();
}
//2.真實(shí)主題類
public class Admin implements Manager {
public void doSomething() {
System.out.println("Admin do something.");
}
}
//3.以聚合方式實(shí)現(xiàn)的代理主題
public class AdminPoly implements Manager{
private Admin admin;
public AdminPoly(Admin admin) {
super();
this.admin = admin;
}
public void doSomething() {
System.out.println("Log:admin操作開始");
admin.doSomething();
System.out.println("Log:admin操作結(jié)束");
}
}
//4.測(cè)試代碼
Admin admin = new Admin();
Manager m = new AdminPoly(admin);
m.doSomething();
//方式二:繼承式靜態(tài)代理
//與上面的方式僅代理類和測(cè)試代碼不同
//1.代理類
public class AdminProxy extends Admin {
@Override
public void doSomething() {
System.out.println("Log:admin操作開始");
super.doSomething();
System.out.println("Log:admin操作開始");
}
}
//2.測(cè)試代碼
AdminProxy proxy = new AdminProxy();
proxy.doSomething();
聚合實(shí)現(xiàn)方式中代理類聚合了被代理類,且代理類及被代理類都實(shí)現(xiàn)了同一個(gè)接口,可實(shí)現(xiàn)靈活多變。繼承式的實(shí)現(xiàn)方式則不夠靈活。
比如,在管理員操作的同時(shí)需要進(jìn)行權(quán)限的處理,操作內(nèi)容的日志記錄,操作后數(shù)據(jù)的變化三個(gè)功能。三個(gè)功能的排列組合有6種,也就是說(shuō)使用繼承要編寫6個(gè)繼承了Admin的代理類,而使用聚合,僅需要針對(duì)權(quán)限的處理、日志記錄和數(shù)據(jù)變化三個(gè)功能編寫代理類,在業(yè)務(wù)邏輯中根據(jù)具體需求改變代碼順序即可。
動(dòng)態(tài)代理
一般來(lái)說(shuō),對(duì)代理模式而言,一個(gè)主題類與一個(gè)代理類一一對(duì)應(yīng),這也是靜態(tài)代理模式的特點(diǎn)。
但是,也存在這樣的情況,有n各主題類,但是代理類中的“前處理、后處理”都是一樣的,僅調(diào)用主題不同。也就是說(shuō),多個(gè)主題類對(duì)應(yīng)一個(gè)代理類,共享“前處理,后處理”功能,動(dòng)態(tài)調(diào)用所需主題,大大減小了程序規(guī)模,這就是動(dòng)態(tài)代理模式的特點(diǎn)。
JDK動(dòng)態(tài)代理
實(shí)現(xiàn)
//1. 抽象主題
public interface Moveable {
void move() throws Exception;
}
//2. 真實(shí)主題
public class Car implements Moveable {
public void move() throws Exception {
Thread.sleep(new Random().nextInt(1000));
System.out.println("汽車行駛中…");
}
}
//3.事務(wù)處理器
public class TimeHandler implements InvocationHandler {
private Object target;
public TimeHandler(Object target) {
super();
this.target = target;
}
/**
* 參數(shù):
*proxy 被代理的對(duì)象
*method 被代理對(duì)象的方法
*args 方法的參數(shù)
* 返回:
*Object 方法返回值
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
long startTime = System.currentTimeMillis();
System.out.println("汽車開始行駛…");
method.invoke(target, args);
long stopTime = System.currentTimeMillis();
System.out.println("汽車結(jié)束行駛…汽車行駛時(shí)間:" + (stopTime - startTime) + "毫秒!");
return null;
}
}
//測(cè)試類
public class Test {
public static void main(String[] args) throws Exception{
Car car = new Car();
InvocationHandler h = new TimeHandler(car);
Class<!--?--> cls = car.getClass();
/**
*loader 類加載器
*interfaces 實(shí)現(xiàn)接口
*h InvocationHandler
*/
Moveable m = (Moveable) Proxy.newProxyInstance(cls.getClassLoader(),cls.getInterfaces(), h);
m.move();
}
}
代碼講解:
在測(cè)試代碼中,Proxy.newProxyInstance()方法需要3個(gè)參數(shù):類加載器(要進(jìn)行代理的類)、被代理類實(shí)現(xiàn)的接口,事務(wù)處理器。所以先實(shí)例化Car,實(shí)例化InvocationHandler的子類TimeHandler,將各參數(shù)傳入Proxy的靜態(tài)方法newProxyInstance()即可獲得Car的代理類,前面的靜態(tài)代理,代理類是我們編寫好的,而動(dòng)態(tài)代理則不需要我們?nèi)ゾ帉懘眍?,是在程序中?dòng)態(tài)生成的。
JDK動(dòng)態(tài)代理步驟
1.創(chuàng)建一個(gè)實(shí)現(xiàn)InvocationHandler接口的類,它必須實(shí)現(xiàn)invoke()方法
2.創(chuàng)建被代理的類及接口
3.調(diào)用Proxy的靜態(tài)方法,創(chuàng)建一個(gè)代理類
4.通過(guò)代理調(diào)用方法
而為什么要進(jìn)行如此操作,可以從Proxy和InvocationHandler的源碼中找打答案。對(duì)源碼不感興趣的可以將下面的源碼部分小節(jié)略過(guò)。
JDK動(dòng)態(tài)代理原理與源碼
newProxyInstance()方法的源碼:
public static Object newProxyInstance(ClassLoader loader,
Class<!--?-->[] interfaces,
InvocationHandler h)
throws IllegalArgumentException{
if (h == null) {
throw new NullPointerException();
}
final Class<!--?-->[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*查找或生成指定的代理類*/
Class<!--?--> cl = getProxyClass0(loader, intfs);
/*用指定的調(diào)用處理程序調(diào)用它的構(gòu)造函數(shù).*/
try {
//獲得類的構(gòu)造函數(shù)
final Constructor<!--?--> cons =cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) {
//當(dāng)需要代理的類實(shí)現(xiàn)了一個(gè)非public的接口時(shí),因?yàn)檫@樣的接口需要特殊的權(quán)限,因此調(diào)用doPrivilege(native 修飾的方法)創(chuàng)建代理實(shí)例。
return AccessController.doPrivileged(newPrivilegedAction<object>() {
public Object run() {
return newInstance(cons,ih);
}
});
} else {
return newInstance(cons,ih);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString());
}
}
可以看到,獲得代理類的代碼是
Classcl = getProxyClass0(loader,intfs);
并由此獲得代理類的構(gòu)造函數(shù),生成代理類的實(shí)例返回給該方法的調(diào)用者。
繼續(xù)跟進(jìn)getProxyClass0()方法:
/** 生成代理類。調(diào)用該方法前必須使用checkproxyaccess方法執(zhí)行權(quán)限檢查。*/
private static Class<!--?--> getProxyClass0(ClassLoader loader,
Class<!--?-->... interfaces) {
//檢查實(shí)現(xiàn)的接口數(shù),65535這個(gè)數(shù)字好特殊,端口數(shù)好像也是這個(gè),這個(gè)數(shù)字是由虛擬機(jī)所決定的,2^16-1個(gè)
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// 如果代理類已經(jīng)通過(guò)實(shí)現(xiàn)給定接口的類加載器創(chuàng)建了,則返回緩存中的該類的副本;否則將通過(guò)ProxyClassFactory創(chuàng)建代理類
return proxyClassCache.get(loader, interfaces);
}
還是沒(méi)有看到代理類是怎么生成的,只知道代理類是從proxyClassCache中取得的,這個(gè)變量是與緩存相關(guān)的一個(gè)對(duì)象,查看該變量的聲明與初始化:
private static final WeakCache<classloader,>[], Class<!--?-->>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());</classloader,>
可以發(fā)現(xiàn)proxyClassCache是個(gè)用來(lái)緩存代理類的類變量,大家知道類變量的特點(diǎn)是與類一一對(duì)應(yīng),在一個(gè)虛擬機(jī)中類只有一個(gè),對(duì)應(yīng)著在一個(gè)虛擬機(jī)中類變量也只有一個(gè),且在此處,在Proxy類被加載的時(shí)候就賦值了。在賦值操作的參數(shù)中有ProxyClassFactory()這么一個(gè)構(gòu)造函數(shù),這個(gè)是動(dòng)態(tài)代理中的關(guān)鍵:生成代理類的類文件字節(jié)碼。繼續(xù)跟進(jìn)去,找到代理類的生成之處了:
/** 根據(jù)給定的類加載器和接口數(shù)組生成代理類的工廠類*/
private static final class ProxyClassFactory
implements BiFunction<classloader,class<?>[], Class<!--?-->>
{
// 所有代理類名稱的前綴
private static final String proxyClassNamePrefix = "$Proxy";
//用于生成唯一代理類名稱的下一個(gè)序號(hào)
private static final AtomicLong nextUniqueNumber = new AtomicLong();
@Override
public Class<!--?--> apply(ClassLoader loader,Class<!--?-->[] interfaces) {
Map<class<?>, Boolean>interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<!--?--> intf : interfaces) {
/* 驗(yàn)證類加載器將此接口的名稱解析為實(shí)際對(duì)象的名稱。*/
Class<!--?--> interfaceClass =null;
try {
interfaceClass = Class.forName(intf.getName(),false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from classloader");
}
/* 驗(yàn)證類對(duì)象確實(shí)是一個(gè)接口。*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*確保接口唯一*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}
String proxyPkg = null; // 代理類的包名
/*記錄非公開代理接口的包,以便將代理類定義在同一個(gè)包中。確認(rèn)所有非公共代理接口都在同一個(gè)包中。*/
for (Class<!--?--> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
String name =intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces fromdifferent packages");
}
}
}
if (proxyPkg == null) {
// 如果沒(méi)有非公開的代理接口,使用com.sun.proxy作為包名
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/* 生成代理類名的序號(hào)*/
long num = nextUniqueNumber.getAndIncrement();
//生成全類名
String proxyName = proxyPkg + proxyClassNamePrefix + num;
/*生成代理類字節(jié)碼 */
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName,interfaces);
try {
return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
}
}
在ProxyClassFactory中,可以看到產(chǎn)生代理類的具體邏輯,大致上是,根據(jù)傳遞的被代理類及其實(shí)現(xiàn)的接口生成代理類的字節(jié)碼加載到緩存中,但是加載到緩存中只是一個(gè).java文件也不能用,所以底層還有編譯等操作。到這里,可以大致的看清JDK中動(dòng)態(tài)代理的面孔了,實(shí)現(xiàn)的步驟為:
1.創(chuàng)建代理類的源碼;
2.對(duì)源碼進(jìn)行編譯成字節(jié)碼;
3.將字節(jié)碼加載到內(nèi)存;
4.實(shí)例化代理類對(duì)象并返回給調(diào)用者;
底層的代碼我們看不到,但是我們可以查看其生成的字節(jié)碼:
//獲得字節(jié)碼的測(cè)試方法
byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy1", Car.class.getInterfaces());
FileOutputStream out = null;
try {
out = new FileOutputStream(System.getProperty("user.dir") + "\\$Proxy1.class");
out.write(classFile);
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//生成的字節(jié)碼:
importcn.com.goser.proxy.imooc.staticproxy.Moveable;
importjava.lang.reflect.InvocationHandler;
importjava.lang.reflect.Method;
importjava.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy1 extends Proxy
implements Moveable
{
private static Method m1;
private static Method m3;
private static Method m0;
private static Method m2;
public $Proxy1(InvocationHandler paramInvocationHandler)
throws
{
super(paramInvocationHandler);
}
public final boolean equals(Object paramObject)
throws
{
try
{
return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
}
catch (RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch (Throwable localThrowable)
{
}
throw new UndeclaredThrowableException(localThrowable);
}
public final void move()
throws Exception
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (Exception localException)
{
throw localException;
}
catch (Throwable localThrowable)
{
}
throw new UndeclaredThrowableException(localThrowable);
}
public final int hashCode()
throws
{
try
{
return ((Integer)this.h.invoke(this, m0, null)).intValue();
}
catch (RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch (Throwable localThrowable)
{
}
throw new UndeclaredThrowableException(localThrowable);
}
public final String toString()
throws
{
try
{
return (String)this.h.invoke(this, m2, null);
}
catch (RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch (Throwable localThrowable)
{
}
throw new UndeclaredThrowableException(localThrowable);
}
static
{
try
{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m3 = Class.forName("cn.com.goser.proxy.imooc.staticproxy.Moveable").getMethod("move", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
return;
}
catch (NoSuchMethodExceptionlocalNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundExceptionlocalClassNotFoundException)
{
}
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
生成的字節(jié)碼比較長(zhǎng),但是在字節(jié)碼中最關(guān)鍵的信息是代理類的聲明:
public final class $Proxy1 extends Proxy
可以看到生成的代理類是繼承了Proxy類的,這就是說(shuō)明了為什么使用JDK動(dòng)態(tài)代理不能實(shí)現(xiàn)繼承式動(dòng)態(tài)代理,原因是Java不允許多繼承,而生成的代理類本身就已經(jīng)繼承了Proxy類。
至此,JDK的動(dòng)態(tài)代理的使用及底層原理分析完畢,揭下動(dòng)態(tài)代理的神秘面紗,果然是枚美女。
至于最底層的native方法是怎么動(dòng)態(tài)生成代理類的字節(jié)碼我們也可以簡(jiǎn)單的模擬一下,先分析下模擬的步驟:首先要生成一段代理類的源碼,然后將源碼編譯后生成代理類的實(shí)例返回給調(diào)用者。依據(jù)此步驟開始編寫我們的模擬代碼:
/**
* JDK java.lang.reflect.Proxy的模擬
* @author Goser (mailto:goskalrie@163.com)
* @Since 2016年9月7日
*/
public class Proxy {
private static final String RT = "\r\n";
public static Object newProxyInstance() throws Exception{
//聲明一段源碼
String sourceCode =
"packagecn.com.goser.proxy.jdk.simulate;"+ RT +
"importcn.com.goser.proxy.imooc.staticproxy.Admin;" + RT +
"importcn.com.goser.proxy.imooc.staticproxy.Manager;" + RT +
"http://以聚合方式實(shí)現(xiàn)的代理主題" + RT +
"public class $Proxy0 implementsManager{" + RT +
" privateAdmin admin;" + RT +
" public$Proxy0(Admin admin) {" + RT +
" super();" + RT +
" this.admin= admin;" + RT +
" }" + RT +
" publicvoid doSomething() {" + RT +
" System.out.println(\"Log:admin操作開始\");" + RT +
" admin.doSomething();" + RT +
" System.out.println(\"Log:admin操作結(jié)束\");" + RT +
" }" + RT +
"}";
String filename = System.getProperty("user.dir") + "/src/main/java/cn/com/goser/proxy/jdk/simulate/$Proxy0.java";
File file = new File(filename);
//使用org.apache.commons.io.FileUtils.writeStringToFile()將源碼寫入磁盤
//編寫到處,可以運(yùn)行一下程序,可以在當(dāng)前目錄中看到生成的.java文件
FileUtils.writeStringToFile(file,sourceCode);
//獲得當(dāng)前系統(tǒng)中的編譯器
JavaCompiler complier = ToolProvider.getSystemJavaCompiler();
//獲得文件管理者
StandardJavaFileManager fileMgr =complier.getStandardFileManager(null, null, null);
Iterable its =fileMgr.getJavaFileObjects(filename);
//編譯任務(wù)
CompilationTask task = complier.getTask(null, fileMgr, null, null, null, its);
//開始編譯,執(zhí)行完可在當(dāng)前目錄下看到.class文件
task.call();
fileMgr.close();
//load到內(nèi)存
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class cls = loader.loadClass("cn.com.goser.proxy.jdk.simulate.$Proxy0");
//生成代理類對(duì)象
Constructor ct = cls.getConstructor(Admin.class);
return ct.newInstance(new Admin());
}
}
class test{
public static void main(String[] args) throws Exception {
Manager m = (Manager)Proxy.newProxyInstance();
m.doSomething();
}
}
運(yùn)行測(cè)試代碼,結(jié)果和手工編寫的結(jié)果一致,完成了JDK中動(dòng)態(tài)代理的實(shí)現(xiàn)模擬。
cglib動(dòng)態(tài)代理
前面分析到,因?yàn)镴ava只允許單繼承,而JDK生成的代理類本身就繼承了Proxy類,因此,使用JDK實(shí)現(xiàn)的動(dòng)態(tài)代理不能完成繼承式的動(dòng)態(tài)代理,但是我們可以使用cglib來(lái)實(shí)現(xiàn)繼承式的動(dòng)態(tài)代理。
大名鼎鼎的Spring中就含有cglib動(dòng)態(tài)代理,在此也以Spring中自帶的cglib完成動(dòng)態(tài)代理的實(shí)現(xiàn):
//1.具體主題
public class Train{
public void move(){
System.out.println("火車行駛中…");
}
}
//2.生成代理
public class CGLibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class<!--?--> clazz){
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
/**
* 攔截所有目標(biāo)類方法的調(diào)用
* 參數(shù):
* obj目標(biāo)實(shí)例對(duì)象
*method 目標(biāo)方法的反射對(duì)象
* args方法的參數(shù)
* proxy代理類的實(shí)例
*/
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
//代理類調(diào)用父類的方法
System.out.println("日志開始");
proxy.invokeSuper(obj, args);
System.out.println("日志結(jié)束");
return null;
}
}
//3.測(cè)試
public class Test {
public static void main(String[] args) {
CGLibProxy proxy = new CGLibProxy();
Train t = (Train) proxy.getProxy(Train.class);
t.move();
}
}
小結(jié)
動(dòng)態(tài)代理與靜態(tài)代理相比較,最大的好處是接口中聲明的所有方法都被轉(zhuǎn)移到調(diào)用處理器一個(gè)集中的方法中處理。在接口方法數(shù)量比較多的時(shí)候,我們可以進(jìn)行靈活處理,而不需要像靜態(tài)代理那樣對(duì)每一個(gè)方法或方法組合進(jìn)行處理。Proxy 很美很強(qiáng)大,但是僅支持 interface 代理。Java 的單繼承機(jī)制注定了這些動(dòng)態(tài)代理類們無(wú)法實(shí)現(xiàn)對(duì) class 的動(dòng)態(tài)代理。好在有cglib為Proxy提供了彌補(bǔ)。class與interface的區(qū)別本來(lái)就模糊,在java8中更是增加了一些新特性,使得interface越來(lái)越接近c(diǎn)lass,當(dāng)有一日,java突破了單繼承的限制,動(dòng)態(tài)代理將會(huì)更加強(qiáng)大。
以上就是本文關(guān)于Java設(shè)計(jì)模式之代理模式原理及實(shí)現(xiàn)代碼分享的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站:快速理解Java設(shè)計(jì)模式中的組合模式、Java設(shè)計(jì)者模式簡(jiǎn)單工廠模式解析、Java設(shè)計(jì)模式之訪問(wèn)者模式使用場(chǎng)景及代碼示例等,有什么問(wèn)題可以隨時(shí)留言,小編會(huì)及時(shí)回復(fù)大家的。感謝朋友們對(duì)本站的支持!
相關(guān)文章
JDK版本管理工具jEnv解決不同jdk版本項(xiàng)目
本文主要介紹了JDK版本管理工具jEnv解決不同jdk版本項(xiàng)目,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
MyBatis-Plus中如何實(shí)現(xiàn)動(dòng)態(tài)表名
這篇文章主要介紹了MyBatis-Plus中如何實(shí)現(xiàn)動(dòng)態(tài)表名問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
關(guān)于servlet向mysql添加數(shù)據(jù)時(shí)中文亂碼問(wèn)題的解決
最近在工作中遇到一個(gè)小問(wèn)題,出現(xiàn)了中文亂碼的問(wèn)題,無(wú)奈只能想辦法解決,下面這篇文章主要給大家介紹了關(guān)于servlet向mysql添加數(shù)據(jù)時(shí)中文亂碼問(wèn)題的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-08-08
詳解Java回調(diào)的原理與實(shí)現(xiàn)
回調(diào)函數(shù),顧名思義,用于回調(diào)的函數(shù)?;卣{(diào)函數(shù)只是一個(gè)功能片段,由用戶按照回調(diào)函數(shù)調(diào)用約定來(lái)實(shí)現(xiàn)的一個(gè)函數(shù)?;卣{(diào)函數(shù)是一個(gè)工作流的一部分,由工作流來(lái)決定函數(shù)的調(diào)用(回調(diào))時(shí)機(jī)。2017-03-03
Java8新特性之類型注解_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java8新特性之類型注解的相關(guān)資料,需要的朋友可以參考下2017-06-06
SpringBoot集成echarts實(shí)現(xiàn)k線圖功能
ECharts是一款基于JavaScript的數(shù)據(jù)可視化圖表庫(kù),提供直觀,生動(dòng),可交互,可個(gè)性化定制的數(shù)據(jù)可視化圖表,本文給大家介紹了SpringBoot集成echarts實(shí)現(xiàn)k線圖功能,文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下2024-07-07
SpringBoot整合Redis實(shí)現(xiàn)熱點(diǎn)數(shù)據(jù)緩存的示例代碼
這篇文章主要介紹了SpringBoot中整合Redis實(shí)現(xiàn)熱點(diǎn)數(shù)據(jù)緩存,本文以IDEA?+?SpringBoot作為?Java中整合Redis的使用?的測(cè)試環(huán)境,結(jié)合實(shí)例代碼給大家詳細(xì)講解,需要的朋友可以參考下2023-03-03

