Java设计模式——代理模式实现及原理
简介
Java编程的目标是实现现实不能完成的,优化现实能够完成的,是一种虚拟技术。生活中的方方面面都可以虚拟到代码中。代理模式所讲的就是现实生活中的这么一个概念:中介。
代理模式的定义:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
代理模式包含如下角色:
ISubject:抽象主题角色,是一个接口。该接口是对象和它的代理共用的接口。
RealSubject:真实主题角色,是实现抽象主题接口的类。
Proxy:代理角色,内部含有对真实对象RealSubject的引用,从而可以操作真实对象。代理对象提供与真实对象相同的接口,以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装。
实现动态代理的关键技术是反射。
静态代理
代理模式有几种,虚拟代理,计数代理,远程代理,动态代理。主要分为两类,静态代理和动态代理。静态代理比较简单,是由程序员编写的代理类,并在程序运行前就编译好的,而不是由程序动态产生代理类,这就是所谓的静态。
考虑这样的场景,管理员在网站上执行操作,在生成操作结果的同时需要记录操作日志,这是很常见的。此时就可以使用代理模式,代理模式可以通过聚合和继承两种方式实现:
- /**方式一:聚合式静态代理
- * @author Goser (mailto:goskalrie@163.com)
- * @Since 2016年9月7日
- */
- //1.抽象主题接口
- public interface Manager {
- void doSomething();
- }
- //2.真实主题类
- public class Admin implements Manager {
- public void doSomething() {
- System.out.println("Admin do something.");
- }
- }
- //3.以聚合方式实现的代理主题
- 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操作结束");
- }
- }
- //4.测试代码
- Admin admin = new Admin();
- Manager m = new AdminPoly(admin);
- m.doSomething();
- //方式二:继承式静态代理
- //与上面的方式仅代理类和测试代码不同
- //1.代理类
- public class AdminProxy extends Admin {
- @Override
- public void doSomething() {
- System.out.println("Log:admin操作开始");
- super.doSomething();
- System.out.println("Log:admin操作开始");
- }
- }
- //2.测试代码
- AdminProxy proxy = new AdminProxy();
- proxy.doSomething();
聚合实现方式中代理类聚合了被代理类,且代理类及被代理类都实现了同一个接口,可实现灵活多变。继承式的实现方式则不够灵活。
比如,在管理员操作的同时需要进行权限的处理,操作内容的日志记录,操作后数据的变化三个功能。三个功能的排列组合有6种,也就是说使用继承要编写6个继承了Admin的代理类,而使用聚合,仅需要针对权限的处理、日志记录和数据变化三个功能编写代理类,在业务逻辑中根据具体需求改变代码顺序即可。
动态代理
一般来说,对代理模式而言,一个主题类与一个代理类一一对应,这也是静态代理模式的特点。
但是,也存在这样的情况,有n各主题类,但是代理类中的“前处理、后处理”都是一样的,仅调用主题不同。也就是说,多个主题类对应一个代理类,共享“前处理,后处理”功能,动态调用所需主题,大大减小了程序规模,这就是动态代理模式的特点。
JDK动态代理
实现
- //1. 抽象主题
- public interface Moveable {
- void move() throws Exception;
- }
- //2. 真实主题
- public class Car implements Moveable {
- public void move() throws Exception {
- Thread.sleep(new Random().nextInt(1000));
- System.out.println("汽车行驶中…");
- }
- }
- //3.事务处理器
- public class TimeHandler implements InvocationHandler {
- private Object target;
- public TimeHandler(Object target) {
- super();
- this.target = target;
- }
- /**
- * 参数:
- *proxy 被代理的对象
- *method 被代理对象的方法
- *args 方法的参数
- * 返回:
- *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("汽车结束行驶…汽车行驶时间:" + (stopTime - startTime) + "毫秒!");
- return null;
- }
- }
- //测试类
- 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 实现接口
- *h InvocationHandler
- */
- Moveable m = (Moveable) Proxy.newProxyInstance(cls.getClassLoader(),cls.getInterfaces(), h);
- m.move();
- }
- }
代码讲解:
在测试代码中,Proxy.newProxyInstance()方法需要3个参数:类加载器(要进行代理的类)、被代理类实现的接口,事务处理器。所以先实例化Car,实例化InvocationHandler的子类TimeHandler,将各参数传入Proxy的静态方法newProxyInstance()即可获得Car的代理类,前面的静态代理,代理类是我们编写好的,而动态代理则不需要我们去编写代理类,是在程序中动态生成的。
JDK动态代理步骤
1. 创建一个实现InvocationHandler接口的类,它必须实现invoke()方法
2. 创建被代理的类及接口
3. 调用Proxy的静态方法,创建一个代理类
4. 通过代理调用方法
而为什么要进行如此操作,可以从Proxy和InvocationHandler的源码中找打答案。对源码不感兴趣的可以将下面的
源码部分小节略过。
JDK动态代理原理与源码
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);
- /*用指定的调用处理程序调用它的构造函数.*/
- try {
- //获得类的构造函数
- final Constructor<?> cons =cl.getConstructor(constructorParams);
- final InvocationHandler ih = h;
- if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) {
- //当需要代理的类实现了一个非public的接口时,因为这样的接口需要特殊的权限,因此调用doPrivilege
- (native 修饰的方法)创建代理实例。
- 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());
- }
- }
可以看到,获得代理类的代码是
Class<?>cl = getProxyClass0(loader,intfs);
并由此获得代理类的构造函数,生成代理类的实例返回给该方法的调用者。
继续跟进getProxyClass0()方法:
- /** 生成代理类。调用该方法前必须使用checkproxyaccess方法执行权限检查。*/
- private static Class<?> getProxyClass0(ClassLoader loader,
- Class<?>... interfaces) {
- //检查实现的接口数,65535这个数字好特殊,端口数好像也是这个,这个数字是由虚拟机所决定的,2^16-1个
- if (interfaces.length > 65535) {
- throw new IllegalArgumentException("interface limit exceeded");
- }
- // 如果代理类已经通过实现给定接口的类加载器创建了,则返回缓存中的该类的副本;否则将通过ProxyClassFactory
- //创建代理类
- return proxyClassCache.get(loader, interfaces);
- }
还是没有看到代理类是怎么生成的,只知道代理类是从proxyClassCache中取得的,这个变量是与缓存相关的一个对象
,查看该变量的声明与初始化:
- private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
- proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
可以发现proxyClassCache是个用来缓存代理类的类变量,大家知道类变量的特点是与类一一对应,在一个虚拟机中类只有一个,对应着在一个虚拟机中类变量也只有一个,且在此处,在Proxy类被加载的时候就赋值了。在赋值操作的参数中有ProxyClassFactory()这么一个构造函数,这个是动态代理中的关键:生成代理类的类文件字节码。继续跟进去,找到代理类的生成之处了:
- /** 根据给定的类加载器和接口数组生成代理类的工厂类*/
- private static final class ProxyClassFactory
- implements BiFunction<ClassLoader,Class<?>[], Class<?>>
- {
- // 所有代理类名称的前缀
- private static final String proxyClassNamePrefix = "$Proxy";
- //用于生成唯一代理类名称的下一个序号
- 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) {
- /* 验证类加载器将此接口的名称解析为实际对象的名称。*/
- 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");
- }
- /* 验证类对象确实是一个接口。*/
- 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; // 代理类的包名
- /*记录非公开代理接口的包,以便将代理类定义在同一个包中。确认所有非公共代理接口都在同一个包中。*/
- 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) {
- // 如果没有非公开的代理接口,使用com.sun.proxy作为包名
- proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
- }
- /* 生成代理类名的序号*/
- long num = nextUniqueNumber.getAndIncrement();
- //生成全类名
- String proxyName = proxyPkg + proxyClassNamePrefix + num;
- /*生成代理类字节码 */
- byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName,interfaces);
- try {
- return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
- } catch (ClassFormatError e) {
- throw new IllegalArgumentException(e.toString());
- }
- }
在ProxyClassFactory中,可以看到产生代理类的具体逻辑,大致上是,根据传递的被代理类及其实现的接口生成代理类的字节码加载到缓存中,但是加载到缓存中只是一个.java文件也不能用,所以底层还有编译等操作。到这里,可以大致的看清JDK中动态代理的面孔了,实现的步骤为:
1. 创建代理类的源码;
2. 对源码进行编译成字节码;
3. 将字节码加载到内存;
4. 实例化代理类对象并返回给调用者;
底层的代码我们看不到,但是我们可以查看其生成的字节码:
- //获得字节码的测试方法
- 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();
- }
- }
- //生成的字节码:
- 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());
- }
- }
生成的字节码比较长,但是在字节码中最关键的信息是代理类的声明:
public final class $Proxy1 extends Proxy
可以看到生成的代理类是继承了Proxy类的,这就是说明了为什么使用JDK动态代理不能实现继承式动态代理,原因是Java不允许多继承,而生成的代理类本身就已经继承了Proxy类。
至此,JDK的动态代理的使用及底层原理分析完毕,揭下动态代理的神秘面纱,果然是枚美女。
至于最底层的native方法是怎么动态生成代理类的字节码我们也可以简单的模拟一下,先分析下模拟的步骤:首先要生成一段代理类的源码,然后将源码编译后生成代理类的实例返回给调用者。依据此步骤开始编写我们的模拟代码:
- /**
- * 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 +
- "//以聚合方式实现的代理主题" + 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操作结束\");" + 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()将源码写入磁盘
- //编写到处,可以运行一下程序,可以在当前目录中看到生成的.java文件
- FileUtils.writeStringToFile(file,sourceCode);
- //获得当前系统中的编译器
- JavaCompiler complier = ToolProvider.getSystemJavaCompiler();
- //获得文件管理者
- StandardJavaFileManager fileMgr =complier.getStandardFileManager(null, null, null);
- Iterable its =fileMgr.getJavaFileObjects(filename);
- //编译任务
- CompilationTask task = complier.getTask(null, fileMgr, null, null, null, its);
- //开始编译,执行完可在当前目录下看到.class文件
- task.call();
- fileMgr.close();
- //load到内存
- ClassLoader loader = ClassLoader.getSystemClassLoader();
- Class cls = loader.loadClass("cn.com.goser.proxy.jdk.simulate.$Proxy0");
- //生成代理类对象
- 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();
- }
- }
运行测试代码,结果和手工编写的结果一致,完成了JDK中动态代理的实现模拟。
cglib动态代理
前面分析到,因为Java只允许单继承,而JDK生成的代理类本身就继承了Proxy类,因此,使用JDK实
现的动态代理不能完成继承式的动态代理,但是我们可以使用cglib来实现继承式的动态代理。
大名鼎鼎的spring中就含有cglib动态代理,在此也以Spring中自带的cglib完成动态代理的实现:
- //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();
- }
- /**
- * 拦截所有目标类方法的调用
- * 参数:
- * obj目标实例对象
- *method 目标方法的反射对象
- * args方法的参数
- * proxy代理类的实例
- */
- public Object intercept(Object obj, Method method, Object[] args,
- MethodProxy proxy) throws Throwable {
- //代理类调用父类的方法
- System.out.println("日志开始");
- proxy.invokeSuper(obj, args);
- System.out.println("日志结束");
- return null;
- }
- }
- //3.测试
- public class Test {
- public static void main(String[] args) {
- CGLibProxy proxy = new CGLibProxy();
- Train t = (Train) proxy.getProxy(Train.class);
- t.move();
- }
- }
小结
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理。在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样对每一个方法或方法组合进行处理。Proxy
很美很强大,但是仅支持 interface 代理。Java 的单继承机制注定了这些动态代理类们无法实现对 class 的动态代理。好在有cglib为Proxy提供了弥补。class与interface的区别本来就模糊,在java8中更是增加了一些新特性,使得interface越来越接近class,当有一日,java突破了单继承的限制,动态代理将会更加强大。
转自http://blog.csdn.net/goskalrie/article/details/52458773
Java设计模式——代理模式实现及原理的更多相关文章
- Java设计模式-代理模式之动态代理(附源代码分析)
Java设计模式-代理模式之动态代理(附源代码分析) 动态代理概念及类图 上一篇中介绍了静态代理,动态代理跟静态代理一个最大的差别就是:动态代理是在执行时刻动态的创建出代理类及其对象. 上篇中的静态代 ...
- JAVA 设计模式 代理模式
用途 代理模式 (Proxy) 为其他对象提供一种代理以控制对这个对象的访问. 代理模式是一种结构型模式. 结构
- Java设计模式の代理模式
目录 代理模式 1.1.静态代理 1.2.动态代理 1.3.Cglib代理 代理模式 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是 ...
- Java设计模式 - 代理模式
1.什么是代理模式: 为另一个对象提供一个替身或占位符以访问这个对象. 2.代理模式有什么好处: (1)延迟加载 当你需要从网络上面查看一张很大的图片时,你可以使用代理模式先查看它的缩略图看是否是自己 ...
- Java设计模式—代理模式
代理模式(Proxy Pattern)也叫做委托模式,是一个使用率非常高的模式. 定义如下: 为其他对象提供一种代理以控制对这个对象的访问. 个人理解: 代理模式将原类进行封装, ...
- Java设计模式-代理模式(Proxy)
其实每个模式名称就表明了该模式的作用,代理模式就是多一个代理类出来,替原对象进行一些操作,比如我们在租房子的时候回去找中介,为什么呢?因为你对该地区房屋的信息掌握的不够全面,希望找一个更熟悉的人去帮你 ...
- 2020-04-20:对Java接口代理模式的实现原理的理解?
静态代理Java中的静态代理要求代理类(ProxySubject)和委托类(RealSubject)都实现同一个接口(Subject).静态代理中代理类在编译期就已经确定,而动态代理则是JVM运行时动 ...
- Java设计模式--代理模式+动态代理+CGLib代理
静态代理 抽象主题角色:声明真实主题和代理主题的共同接口. 代理主题角色:代理主题内部含有对真实主题的引用,从而在任何时候操作真实主题对象:代理主题提供一个与真实主题相同的接口,以便在任何时候都可以代 ...
- Java设计模式——代理模式
public interface People { public void work(); } public class RealPeople implements People { public v ...
随机推荐
- Git学习总结(5)——搭建Git简易高效服务器
1. mysysgit+gitblit安装流程 1.1资源 需先下载好的资源(公司用的1.6,1.7+请自行匹配对应的mysysgit+gitblit): jdk1.6 Git-1.8.4-pr ...
- HDU 4971 A simple brute force problem.
A simple brute force problem. Time Limit: 1000ms Memory Limit: 65536KB This problem will be judged o ...
- Windows Server 2016 辅助域控制器搭建
Windows Server 2016 主域控制器搭建完成后,继续进行辅助域控制器搭建.1.更改服务器的IP地址2.修改服务器的名称3.打开服务器管理器,选择添加角色和功能4.选择,下一步5.选择,下 ...
- DbVisualizer Personal 中文乱码问题的通用解决方法
在SQL Commander中,sql语句中假设有中文.显示是口. 解决的方法例如以下: 在Tools->Tool Properties->General->Appearance-& ...
- 洛谷P1722 矩阵 II
题目背景 usqwedf 改编系列题. 题目描述 如果你在百忙之中抽空看题,请自动跳到第六行. 众所周知,在中国古代算筹中,红为正,黑为负…… 给定一个1*(2n)的矩阵(usqwedf:这不是一个2 ...
- c# array arraylist 泛型list
1 array 数组 是存储相同类型元素的固定大小的数据的顺序集合.在内存中是连续存储的,所以索引速度非常快,而且赋值和修改元素也非常简单. //定义字符串数组 大小为3 string[] str1 ...
- 使用Java开发微信公众平台(四)——消息的接收与响应
上一篇文章(http://www.jerehedu.com/fenxiang/171807_for_detail.htm )中,我们学习了使用Java语言开发微信公众平台的第一部分——环境搭建与开发接 ...
- 如何通过使用fiddler对Android系统设备抓包总结
http://www.open-open.com/lib/view/open1427509907668.html
- JavaScript入门:003—JS中的变量
编程语言都是同样的,JS中也是有变量的.首先JS的变量是区分大写和小写的,这个须要注意.比方number和Number是不同的变量.无论是经常使用类型的,还是对象类型,比方 Object obj和Ob ...
- Qt样式表——选择器详解(父子关系)
在上一节中,小豆君给大家介绍了样式表的基本概念和大致用法.今天我们来详细了解下样式表中选择器的用法. 所谓选择器,就是指定你所设置的样式对哪个或哪些控件起作用. 到目前为止,Qt样式表支持CSS2中定 ...