精华内容
下载资源
问答
  • java对图片任意大小的缩小放大操作

    千次阅读 2010-08-30 09:18:00
    /*  * 该类实现了对图片的任意大小的缩放处理,滚动鼠标滚轮对图片进行缩放处理,图片的高度和宽度最好不要超过屏幕的宽高  */ import java.awt.Color; import java.awt.Graphics;...

    代码如下:

     

    package relevantTest;
    /*
     * 该类实现了对图片的任意大小的缩放处理,滚动鼠标滚轮对图片进行缩放处理,图片的高度和宽度最好不要超过屏幕的宽高
     */
    import java.awt.Color;
    import java.awt.Graphics;
    import java.awt.Image;
    import java.awt.Toolkit;
    import java.awt.event.MouseWheelEvent;
    import java.awt.event.MouseWheelListener;
    import javax.swing.*;

    public class TestMouseWheelEvent extends JFrame{ 
     private static final long serialVersionUID = 1L;
     static Image img=(new ImageIcon("images/王梓盈.jpg")).getImage();
     static int imgWidth=img.getWidth(null);//获得图片的宽度
     static int imgHeight=img.getHeight(null);//获得图片的高度
     static int ScreenWidth=Toolkit.getDefaultToolkit().getScreenSize().width;//获得屏宽
     static int ScreenHeight=Toolkit.getDefaultToolkit().getScreenSize().height;//获得屏高
     static int xCoor=(ScreenWidth-imgWidth)/2;
     static int yCoor=(ScreenHeight-imgHeight)/2;
     public TestMouseWheelEvent(){
      PicsPanel jp=new PicsPanel();
      jp.setBackground(new Color(51,51,51));
      /*
       * 添加鼠标滚轮事件
       */
      jp.addMouseWheelListener(new MouseWheelListener(){
       public void mouseWheelMoved(MouseWheelEvent e){
        if(e.getWheelRotation()<0){//如果鼠标滚轮的“咔哒声”小于零则表示是向上滚动
        imgWidth+=e.getScrollAmount()*10;
        imgHeight+=e.getScrollAmount()*10;
        xCoor=(ScreenWidth-imgWidth)/2;
        yCoor=(ScreenHeight-imgHeight)/2;
        repaint();
        }
        else{//否则表示向下滚动
         imgWidth-=e.getScrollAmount()*3;
         imgHeight-=e.getScrollAmount()*3;
         xCoor=(ScreenWidth-imgWidth)/2;
         yCoor=(ScreenHeight-imgHeight)/2;
         repaint();//e.getScrollAmount()=3,如电脑的设置有关,一般的电脑默认是3
        }
       }
      });
      /**
       * 在该面板上创建弹出右键菜单,“悬浮面板"
       */
      
      JPopupMenu jpm=new JPopupMenu();
         jpm.setBackground(new Color(51,51,51));   
      JMenuItem copy=new JMenuItem("复制(C)");
      JMenuItem cut=new JMenuItem("剪切(X)");
      JMenuItem open=new JMenuItem("打开图片所在的文件夹(O)");
      JMenu setBackgroundColor=new JMenu("设置背景色(S)");
      JMenuItem delete=new JMenuItem("删除(D)");
      JMenuItem property=new JMenuItem("属性");  
      copy.setBackground(new Color(51,51,51));
      copy.setForeground(new Color(204,204,204));
      //copy.setFont(new Font("楷体",Font.BOLD,12));//为什么设置字体后导致弹出菜单出现变慢???
      cut.setBackground(new Color(51,51,51));
      cut.setForeground(new Color(204,204,204));
      open.setBackground(new Color(51,51,51));
      open.setForeground(new Color(204,204,204));
      setBackgroundColor.setBackground(new Color(51,51,51));
      setBackgroundColor.setForeground(new Color(204,204,204));
      delete.setBackground(new Color(51,51,51));
      delete.setForeground(new Color(204,204,204));
      property.setBackground(new Color(51,51,51));
      property.setForeground(new Color(204,204,204));  
      jpm.add(copy);
      jpm.add(cut);
      jpm.addSeparator();  
      jpm.add(open);
      jpm.addSeparator();
      jpm.add(setBackgroundColor);
      jpm.addSeparator();
      jpm.add(delete);
      jpm.addSeparator();
      jpm.add(property);
      jp.setComponentPopupMenu(jpm);//确定弹出菜单的父组件
      /**
       * 添加菜单子项事件监听代码  
       */  
      this.add(jp);
      this.setTitle("测试鼠标滚轮事件");
      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      //默认的框架显示位置是桌面左上角
      this.setSize(1366,768);
      this.setVisible(true);
      
     }
     static class PicsPanel extends JPanel{  
      private static final long serialVersionUID = 1L;
      protected void paintComponent(Graphics g){
       super.paintComponent(g);
       g.drawImage(img,xCoor,yCoor,imgWidth,imgHeight,this);
      }
      
     }
     
     
     
     public static void main(String[] args){  
      new TestMouseWheelEvent();
     }

    }

    展开全文
  • 集成 COM 和 Java 组件

    千次阅读 2008-05-06 10:39:00
    很长时间以来,互操作性问题使得微软的组件对象模型®(COM)和 Java组件之间的集成成为一项令人畏惧的工作。IBM alphaWorks 提供的 Java-COM 桥开发工具简化了集成工作,而且还为应用程序从 COM 迁移到 Java ...
    很长时间以来,互操作性问题使得微软的组件对象模型®(COM)和 Java ™ 组件之间的集成成为一项令人畏惧的工作。IBM alphaWorks 提供的 Java-COM 桥开发工具简化了集成工作,而且还为应用程序从 COM 迁移到 Java 平台提供了改良方法。IBM Rational 的 Cheng-Yee Lin、Thomas Houser 和 Peter Parapounsky 是桥接技术的三位缔造者,他们将解释桥接的一些基础知识,并将展示一个表现桥接功能的示例应用程序。

    随着企业的需求和复杂性的增长,可能需要把拥有完全不同底层实现的应用程序和组件组合成一个集成的解决方案。Java 技术和 COM 技术在 Windows 平台上的大型应用程序和组件开发方面,都占据着主要的位置,但在集成世界中,这两项技术之间的互操作性(或者说 桥接)方面仍然存在着大量没有解决的问题。最近几年,出现了几个工具,它们启用了 Java 组件与 COM 组件之间的轻量级集成。但是,其中有些工具的可应用性非常有限,例如它们只支持从 Java 组件到 COM 的桥接(即从 Java 代码调用 COM 服务器的方法)。而面向通用性设计的桥接工具则会因为密集的交互而造成很高的性能开销。

    本文将介绍桥接技术,它平衡了性能与实用性。IBM Rational Java-COM 桥(RJCB)不仅支持从 Java 组件到 COM 以及从 COM 到 Java 组件的桥接,而且在组件通过桥进行密集的互动时,还提供了合理的性能。建立 RJCB 桥使用的工具(Java-COM 桥开发工具,DTJCB)与开放源代码的 Eclipse IDE 集成(请参阅侧栏 DTJCB 作为 Eclipse 的扩展 ),使得在单一 Java 环境中建立和使用 RJCB 桥变得很容易。您可以通过使用 DTJCB 建立的桥,用 Microsoft 的 工具建立从 COM 到 Java 组件的桥接。

    DTJCB 还为您提供了一条渐进的迁移路线,可以把大型的 COM 应用程序转换成 Java 技术。您不必一夜之间改变整个应用程序,使用 RTJCB,可您可以一个组件一个组件地执行迁移,从而保证对应用程序使用过程造成的破坏最小。

    了解 RJCB 技术

    RJCB 技术采用 Java 本机接口(JNI)框架在桥 Java 代码和 COM 之间实现桥接。JNI 让您可以在 Java 语言中调用本机代码,反之亦然。您可以在 Java 语言中声明一个方法,并用 C 或 C++ 来定义方法主体。反过来,您也可以在 C 或 C++ 代码中调用 Java 的方法。

    图 1 表示了 RJCB 桥的结构。

    DTJCB 作为 Eclipse 扩展

    DTJCB 是一个重要的 Eclipse 插件(目前 Eclipse 的版本是 3.0),Eclipse 是一个开放源代码的 Java 开发工具(请参阅 参考资料)。Eclipse 被广泛地用作多个开放源代码工具和商业产品的基础开发环境。DTJCB 可以“安装”并集成到这些工具和产品中。这样,您既可以在自己喜欢的基于 Eclipse 的工具中使用 DTJCB,您也可以下载 Eclipse IDE,把它安装为基础的 Eclipse 环境,把 DTJCB 添加到它的插件列表中,然后就可以进行桥接了。为了简便起见,我们把任何安装了 DTJCB 插件的基于 Eclipse 的工具都简称为“DTJCB”。


    图 1. RJCB 桥的结构
    图 1. RJCB 桥的结构

    图 1 中绿色的盒子代表随 RJCB 一起安装的 RJCBRT.jar 和 RJCBRT.dll 文件。它们提供了支持类和服务,生成的桥代码将使用这些类和服务(您的 Java 代码在一定程度内也使用它们)。红色的盒子代表 RJCB 桥生成器生成的特定于具体 COM API 的代码。

    每个 COM API 都由一个称为 类型库 的特殊文件进行描述。独立类型库通常以 .tlb 为扩展名。类型库也可以嵌在可执行(.exe、.dll、以及 .ocx)文件内。RJCB 桥生成器读取类型库,并根据类型库所描述的 API 生成 Java 代码和 C++ 桥代码。

    COM 接口通常提供两个不同的方法调用机制: 后期绑定(late-bound)前期绑定(early-bound)。使用后期绑定调用,需要在运行时解析方法名称,并把所有方法参数打包成特殊的 variant 数组,或者从这个特殊的 variant 数组解包出方法参数。而前期绑定调用则可以利用能够确切知道要调用的方法以及方法参数类型的优势。对于进程内 COM 服务器(在相同的 COM 进程中),前期绑定调用等价于 C++ 的 vtable 调用(C++ 调用 虚拟 方法的标准机制)。

    后期绑定机制则是通过实现一个称为 IDispatch 的特殊的超级接口来提供的。 IDispatch 接口提供了一种方法,该方法可以根据名称查找 COM 接口的方法(或者属性),然后返回方法的 dispidIDispatch 接口还提供了另外一个方法,可以通过 dispid 和包含调用参数的 variant 数组调用 COM 接口的方法。比起简单的 C++ vtable 调用,通过 IDispatch 接口调用 COM 方法的效率非常低(既使您不用 IDispatch 接口的后期绑定特性,只是用硬编码的 dispid 调用方法,也是如此)。

    开发 RJCB 桥接技术的主要目的是建立可能的、最迅速的 Java/COM 桥。我们把 Java 和 COM 之间的一些高流量 API 桥接起来,这样需要我们的桥能提供最大可能的性能。出于这个原因,RJCB 桥只支持那些提供前期绑定 vtable 接口的 COM API。RJCB 生成的桥代码可以进行直接的、针对特定接口的 vtable 调用。它不使用 IDispatch 超级接口。

    RJCB 代码生成器为 COM 类型库中定义的每个 vtable 接口辅助类 生成代理。它还生成 Java 单元,里面包含类型库中定义的 模块 常量和 枚举 标量。





    回页首


    研究桥代码示例

    为了展示这个过程,让我们来看一看为一个简单的 COM API 示例生成的桥代码。这个示例 COM API 声明了一个有几个常量的模块;一个有几个枚举标量的枚举类型;一个只有一个 Name 属性的接口;一个它实现了仅有的那个接口的辅助类。(更典型一些的 COM API 还应当包含更多的接口,每个接口还要包含更多的方法和属性。)

    清单 1 显示了这个简单的 COM API 的接口定义语言(IDL)规范


    清单 1. SimpleTestModule 的IDL 规范
    module SimpleTestModule
    {
        static const int SIMPLETEST_INT_CONST = 99;
        static const LPCOLESTR SIMPLETEST_STRING_CONST = L"This is a test.";
    };
    typedef [public] enum SimpleTestEnum {
        STE_VALUE1 =  0,
        STE_VALUE2 =  1,
        STE_VALUE3 =  2
    } SimpleTestEnum;
    [
        object,
        uuid(1C551D4C-B3D8-4BCA-BDC0-6D870D84CA7F),
        helpstring("ISimpleTest Interface"),
        dual,
        pointer_default(unique)
    ]
    interface ISimpleTest : IDispatch
    {
        [propget, helpstring("property Name"), id(1)]
        HRESULT Name([out, retval] BSTR* theName);
        [propput, helpstring("property Name"), id(1)]
        HRESULT Name([in] BSTR theName);
    };
    [
        uuid(14CED841-ED27-4450-9255-FE384C6C3B0D),
        helpstring("SimpleTest Class")
    ]
    coclass SimpleTest
    {
        [default] interface ISimpleTest;
    };
        

    RJCB 代码生成器生成的对应的 Java 源文件,如清单 2 和 3 所示:


    清单 2. SimpleTestModule.java
    package com.ibm.simpletest;
    public interface SimpleTestModule {
        public static final int SIMPLETEST_INT_CONST = 99;
        public static final String SIMPLETEST_STRING_CONST = "This is a test.";
    }
        


    清单 3. SimpleTestEnum.java

    package com.ibm.simpletest;
    public interface SimpleTestEnum {
        public static final int STE_VALUE1 = 0;
        public static final int STE_VALUE2 = 1;
        public static final int STE_VALUE3 = 2;
    }
    

    针对接口生成了三个不同的 Java 文件。其中一个文件包含 Java 语言中与接口对应的等价物,如清单 4 所示:


    清单 4. ISimpleTest.java
    package com.ibm.simpletest;
    public interface ISimpleTest {
        public static final String IID = "1C551D4C-B3D8-4BCA-BDC0-6D870D84CA7F";
        public static final Class BRIDGECLASS = SimpleTestBridgeObjectProxy.class;
        public static final String CLSID = "7B422507-1B5B-49F7-BCEF-0FDE519C621A";
        /** 
         * getName. property Name
         */
        public String getName() throws java.io.IOException;
        /** 
         * setName. property Name
         */
        public void setName(String theName) throws java.io.IOException;
    }
    

    第二个文件包含接口中每个方法的 JNI 本机声明,如清单 5 所示:


    清单 5. ISimpleTestJNI.java
    package com.ibm.simpletest;
    public class ISimpleTestJNI {
        public static native String getName(long native_this) throws java.io.IOException;
        public static native void setName(long native_this, String theName) throws java.io.IOException;
    }
    

    第三个文件包含 Java 代理类,它调用 JNI 方法实现接口。代理类还有一个引用,指向它所代表的实际的本机 COM 对象,引用由叫作 native_object 的成员变量表示(该变量是在一个支持性的超类中定义的,所有代理类都将扩展这个超类)。清单 6 显示了 Java 代理类文件:


    清单 6. ISimpleTestProxy.java
    package com.ibm.simpletest;
    public class ISimpleTestProxy extends SimpleTestBridgeObjectProxy implements ISimpleTest {
        protected ISimpleTestProxy(String clsid, String iid) throws java.io.IOException
        {
            super(clsid, iid);
        }
        public ISimpleTestProxy(String clsid, String dumb1, Object dumb2) throws java.io.IOException
        {
            super(clsid, ISimpleTest.IID);
        }
        public ISimpleTestProxy(long native_object)
        {
            super(native_object);
        }
        public ISimpleTestProxy(Object com_proxy_object) throws java.io.IOException
        {
            super(com_proxy_object, ISimpleTest.IID);
        }
        protected ISimpleTestProxy(Object com_proxy_object, String iid) throws java.io.IOException
        {
            super(com_proxy_object, iid);
        }
        // ISimpleTest methods
        public String getName() throws java.io.IOException
        {
            String theName = ISimpleTestJNI.getName(native_object);
            return theName;
        }
        public void setName(String theName) throws java.io.IOException
        {
            ISimpleTestJNI.setName(native_object, theName);
        }
    }
    

    还要为辅助类也生成一个辅助类的代理类,名为 SimpleTest。清单 7 显示了 SimpleTest.java 文件:


    清单 7. SimpleTest.java
    package com.ibm.simpletest;
    public class SimpleTest extends ISimpleTestProxy {
        public static final String CLSID = "14CED841-ED27-4450-9255-FE384C6C3B0D";
        public SimpleTest(long native_object)
        {
            super(native_object);
        }
        public SimpleTest(Object com_proxy_object) throws java.io.IOException
        {
            super(com_proxy_object, ISimpleTest.IID);
        }
        public SimpleTest() throws java.io.IOException
        {
            super(CLSID, ISimpleTest.IID);
        }
    }
    

    Java 辅助类的代理类扩展了其 默认接口的代理类。如果还实现了额外的接口,那么它还要包含对额外接口的实现(就象是接口代理一样)。

    辅助类代理也包含一个默认的无参数构造器,用来建立它所代表的 COM 对象的实例。构造器用类型库辅助类的声明中定义的 CLSID 来实例化 COM 对象。

    ISimpleTestJNI.java 中声明的本机方法的主体,在 ISimpleTestJNI.cpp 中定义,如清单 8 所示:


    清单 8. ISimpleTestJNI.cpp
    JNIEXPORT jstring JNICALL Java_com_ibm_simpletest_ISimpleTestJNI_getName(
        JNIEnv* env, jclass,
        jlong native_this)
    {
        SimpleTestLib::ISimpleTest* this_intf = (SimpleTestLib::ISimpleTest*)native_this;
        CComBSTR nativeTheName;
        CHRT(this_intf->get_Name(&nativeTheName));
        return JSTRING_FROM_CCOMBSTR(env, nativeTheName);
    }
    JNIEXPORT void JNICALL Java_com_ibm_simpletest_ISimpleTestJNI_setName(
        JNIEnv* env, jclass,
        jlong native_this, jstring theName)
    {
        SimpleTestLib::ISimpleTest* this_intf = (SimpleTestLib::ISimpleTest*)native_this;
        BSTR_FROM_JSTRING nativeTheName(env, theName);
        CHRTV(this_intf->put_Name(nativeTheName));
    }
    

    从清单 8 中您可以看到,方法主体只是通过 COM 接口指针调用对应的 COM 方法,进行必要的参数转换(例如在 Java 语言和 COM 所表示的字符串之间转换)。代码还负责把 COM 的错误返回值转换成 Java 语言异常(通过 CHRT* 宏)。

    使用双向桥时,就是告诉 RJCB 桥生成器,COM API 中的某些接口是用 Java 语言实现的,在 C++ 端还生成了额外的两个文件,它们为 COM 接口的 Java 实现提供 COM 代理。COM 实际上与 Java 代理一样,只是方向不同。COM 代理以成员变量的形式持有一个引用,指向其所代表的 Java 对象。

    在我们简单的示例中, ISimpleTest 接口的代理文件叫作 ISimpleTestProxy.h 和 ISimpleTestProxy.cpp。由于在 C++ 中进行 COM 编程很复杂,所以这些程序读起来有点难。清单 9 显示了从 ISimpleTestProxy.cpp 文件摘录的一段代码:


    清单 9. ISimpleTestProxy.cpp 的代码段
    STDMETHODIMP ISimpleTestProxy::get_Name(BSTR* theName)
    {
        JNIEnv* env = 0;
        jobject java_object;
        CHRR(_RJCBService->GetJavaObject(this, &env, &java_object));
        JNILocalFrame _JNILocalFrame(env);
        if (theName == 0) {
            return E_INVALIDARG;
        }
        jstring jniTheName = (jstring)env->CallObjectMethod(java_object, _ISimpleTestProxyInfo->
          m_getName_method_id);
        *theName = BSTR_FROM_JSTRING(env, jniTheName).Detach();
        return _RJCBService->CatchException(env);
    }
    STDMETHODIMP ISimpleTestProxy::put_Name(BSTR theName)
    {
        JNIEnv* env = 0;
        jobject java_object;
        CHRR(_RJCBService->GetJavaObject(this, &env, &java_object));
        JNILocalFrame _JNILocalFrame(env);
        env->CallVoidMethod(java_object, _ISimpleTestProxyInfo->
          m_setName_method_id, JSTRING_FROM_BSTR(env, theName));
        return _RJCBService->CatchException(env);
    }
    

    请注意,在清单 9 中,方法主体只是通过代理所代表的 Java 对象调用对应的 Java 方法,从而进行必要的参数转换(例如在 COM 和 Java 语言所表示的字符串之间转换)。代码还负责把 Java 语言的异常转换成 COM 的错误返回值。

    DTJCB 和 Java 接口工具的区别

    一个供 Java 使用的,与 DTJCB 接口类似的工具(又叫作 Bridge2Java)—— 也可以从 IBM alphaWorks 站点得到(请参阅 参考资料)。下表总结了两项技术之间的差异。该表只是针对比较提供一些一般性的信息。如果您想在实际工作中使用这些工具,那么请您在选择之前进行更详细的分析。

    特性 DTJCB 接口工具
    与 Eclipse 开发环境集成 Yes No
    支持 Java 到 COM 的调用 Yes Yes
    通过 IDispatch 接口进行 Java 到 COM 的调用 No Yes
    Java 到 COM 的 vtable 调用直接发到特定接口 Yes No
    支持 COM 到 Java 的调用(也就是说,COM 接口的 Java 实现) Yes No
    可以从 COM 客户机初始化 Java 服务器 Yes No
    Java 服务器可以动态地注册到 COM 的运行时对象表中 Yes No
    可以由独立的类加载器(也就是说,各有各的类路径)加载多个 Java 服务器 Yes No
    可以通过 VBScript 以编程方式访问运行中的 Java 应用程序的 API。(不用生成任何桥) Yes No
    支持类型安全的转换(也就是说,通过 QueryInterface 进行类型转换) Yes No
    可伸缩。生成桥的类型库可以从其他类型库中导入(也就是说,Java 代码可以引用导入类型库生成的桥中定义的类型) Yes No
    可以通过 java.util.Enumeration 接口访问 COM _NewEnum 属性 Yes No
    支持用 ProgId 而不是 CLSID 生成 COM 对象 Yes No
    支持多线程 Yes ?
    运行时跟踪选项 Yes ?




    回页首


    使用 DTJCB:概述

    要通过 DTJCB 使用 RJCB 技术,需要为“服务器”组件建立、生成桥,然后把代码添加到将通过桥访问“服务器”的“客户机”组件中。DTJCB 可以让您在单一的环境中实现这个过程。

    您要从服务器组件开始,用 COM 或 Java 语言建立这些组件,然后用 Eclipse 环境在 RJCB 桥项目中生成桥。完成的项目中会包含桥的代理代码,用 Java 语言和 Visual C++ 生成,还有代理代码使用的运行时库。然后,您要用 Eclipse 中的标准 build 命令生成桥的各个部分。

    当您从 Java 客户机访问 COM 服务器时,可以继续在 Eclipse 中工作,建立 Java 项目。还可以添加与桥项目中的代理交互的 Java 客户机代码,它会通过桥运行时库与 COM 服务器进行对话。在 Eclipse 中使用 DTJCB,提供了一种端对端的体验(从建立桥到进行实际的桥接调用)。

    当您从 COM 客户机访问 Java 服务器时,您首先要在微软的 Windows 环境中注册 Java 服务器,这样才可以将它“暴露”给 COM 环境。然后就可以使用基于 COM 的开发工具(例如微软 Visual Studio 的 Visual C++ 或 Visual Basic)编写通过桥访问 Java 服务器的客户机代码。在后面的章节中,我们会用一些示例来演示这个过程。





    回页首


    用 DTJCB 开发 Java COM 客户机

    让我们来看看如何为示例 DLL 文件中的 COM 服务器建立桥,把它的接口暴露到 Java 技术这端,并从 Java 客户机调用这个接口。我们以一个办公家具店为例,这家家具店运行着一个 COM 服务器,负责跟踪公司的库存。它的 COM 接口暴露在 OfficeFurniture.dll 文件中。而您想把库存信息从 COM 服务器传递到 Java 客户机应用程序。

    创建 RJCB 桥项目

    要开始桥的创建过程,请在 Eclipse 新项目向导中选择 Java-COM 桥项目,如图 2 所示。


    图 2. 新项目的向导
    图 2. 新项目的向导

    单击 Next 按钮,打开新建 Java-COM 桥项目页,在这里指定项目名称和位置,如图 3 所示。


    图 3. 新建 Java-COM 桥项目页
    图 3. 新建 Java-COM 桥项目页

    下一页显示目前在项目中的桥(如果有的话)。对于我们的新项目来说,其中为空,如图 4 所示。


    图 4. Java-COM 桥项目的内容页
    图 4. Java-COM 桥项目的内容页

    单击 Add... 按钮,打开 Java-COM 桥设置页,在这里提供所要建立的 COM 服务器和桥的信息。图 5 显示了我们示例的页面。您要指定桥的名称,把 COM 服务器作为源类型库,输入 Java 代码访问服务器要使用的 Java 包的名称。(您可以在开发工具中找到更多选项和信息。)


    图 5. Java-COM 桥设置页
    图 5. Java-COM 桥设置页

    如果对创建的桥的设置感到满意,请单击 OK 按钮。桥的名称就会出面在桥项目的内容页中,如图 6 所示。


    图 6. Java-COM 桥项目内容页
    图 6. Java-COM 桥项目内容页

    单击向导的 Finish 按钮,可以看到 Eclipse 的 Java Perspective 中的 Package Explorer 被桥代理代码所填充,该代码是用 Java 语言和 Visual C++ 写成的,如图 7 所示。


    图 7. Package Explorer 中生成的文件
    图 7. Package Explorer 中生成的文件

    建立 RJCB 桥项目

    现在您已经生成了桥的代理代码,就可以用 Eclipse 中标准的 build 菜单创建桥了。请注意,桥的代码依赖于JDK(不仅仅是 JRE)和微软 Visual C++ 6.0(SP 5),所以要确保它们已经安装在系统上。

    Eclipse 既支持自动 build 模式,也支持手动 build 模式。两种模式都能创建桥所必需的二进制文件。在我们的示例中,build 命令生成了 OfficeFurnitureBridge.dll 和 OfficeFurnitureBridge.jar 文件。

    现在就可以使用桥了。您可以展开代理的 Java 文件,查看可以通过桥使用哪个接口。例如,图 8 显示了在 Java 语言端可以使用的 OfficeFurniture.dll 中的接口方法。


    图 8. 通过桥看到的接口方法
    图 8. 通过桥看到的接口方法

    创建作为 COM 客户机的 Java 项目

    按照在 Eclipse 中建立普通 Java 代码开发的过程,您可以很容易地创建 Java 项目,并编写访问接口的代码。从使用新建项目向导建立 Java 项目开始,如图 9 所示。


    图 9. 新建项目向导
    图 9. 新建项目向导

    在下一页中,指定 Java 项目的名称,如图 10 所示。


    图 10. 新建 Java 项目页面
    图 10. 新建 Java 项目页面

    然后,在接下来的 Java 设置页面中,可以在项目和库各自的附签中提供它们之间的依存关系,如图 11 和图 12 所示。


    图 11. 项目依赖性的设置
    图 11. 项目依赖性的设置

    图 12. 库依赖性的设置
    图 12. 库依赖性的设置

    在单击 Finish 按钮时,Eclipse 将建立 Java 项目。您在 Package Explorer 中可以看到桥和 Java 项目,如图 13 所示。


    图 13. Package Explorer 中的桥和 Java 项目
    图 13. Package Explorer 中的桥和 Java 项目

    使用客户机项目中的 RJCB 桥项目

    现在要做的,是添加调用 COM 服务器接口的 Java 代码。要使用 Eclipse 中标准的新建 Java 类命令,首先要向刚才创建的 Java 项目中添加一个新的类,如图 14 所示。


    图 14. 添加新的 Java 类
    图 14. 添加新的 Java 类

    现在添加 Java 代码,引用通过桥暴露的 COM 服务器接口。首先,把清单 10 中的代码段添加到 Test 类的 main() 方法的主体中,如下所示:


    清单 10. main() 方法的新代码
    int count = 0;
    try {
    // Load RJCB Runtime library
    com.ibm.rjcb.RJCBUtilities.loadRJCB
      ("C://eclipse//plugins//com.ibm.rjcb.dtk.common_1.0.0//RJCBRT.dll");
    // Load Bridge dll
    string dll_location = "C://eclipse//workspace//"
    + "SampleBridgeProject//MyJ2CBridge//"
    + "c++//OfficeFurnitureBridge//Release//OfficeFurnitureBridge.dll";
    System.load(dll_location);
    // Access COM object through the bridge
    IOfficeCatalog catalog = new OfficeCatalog();
    count = catalog.Count();
    System.out.println("count = " + count);
    for (int index = 0; index < count; index++) {
    System.out.println("item=" + index
    + " ID=" + catalog.GetID(index)
    + " Name=" + catalog.GetName(index));
    }
    } catch (java.io.IOException e) {
    e.printStackTrace();
    }
    

    在清单 10 中, catalog 是实际的 COM 对象的代理对象,您可以调用它的方法。然后,可以使用类上的上下文菜单 Organize Imports,Eclipse 会自动把需要的导入项目添加到这个类中,如清单 11 所示:


    清单 11. 通过 Eclipse 自动添加的导入项
    import com.officefurniture.IOfficeCatalog;
    import com.officefurniture.OfficeCatalog;
    

    然后,还是使用 build 命令生成 Java 项目,并设置 Run 配置,如图 15 所示。


    图 15. Run 配置的对话框
    图 15. Run 配置的对话框

    Java 客户机执行的结果将在 Output 视图中显示,如图 16 所示。


    图 16. Output 视图中的结果
    图 16. Output 视图中的结果

    很漂亮,是不是?您已经建立了能够与基于 COM 的服务器进行交互的 Java 代码。





    回页首


    开发 Java COM 服务器

    现在让我们看看如何定义一个在 Java 端实现的接口,同时通过桥把该接口暴露给 COM 端,并从 COM 客户机调用这个接口。我们将用 ICookie 接口来演示完成这项任务的步骤。清单 12 显示了这个接口的 IDL 规范:


    清单 12. ICookie 的 IDL 规范
    import "oaidl.idl";
    import "ocidl.idl";
    [
    object,
    uuid(364DC55D-9C03-4dc6-AD82-5CAC8F1077FF),
    dual,
    helpstring("ICookie Interface"),
    pointer_default(unique)
    ]
    interface ICookie : IDispatch
    {
    [id(1), helpstring("method GetModel")] 
    HRESULT GetName([retval, out] BSTR *name);
    [id(2), helpstring("method GetModel")] 
    HRESULT GetKind([retval, out] BSTR *name);
    };
    [
    uuid(658EEF6A-D9C9-4a06-870D-2FA8A31A3026),
    version(1.0),
    helpstring("COM to Java test 1.0 Type Library")
    ]
    library CookieLib
    {
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");
    [
    uuid(F0E00502-16E2-43e2-B12C-02EE037C402B),
    helpstring("Cookie Class")
    ]
    coclass Cookie
    {
    [default] interface ICookie;
    };
    };
    

    建立 RJCB 桥项目

    DTJCB 支持从类型库建立桥。所以,这里的秘诀就是先用微软的 MID 编译器处理清单 12 中的 IDL 规范,建立一个名为 cookie.tlb 的类型库。

    从类型库建立 RJCB 桥项目的步骤与我们在前面一节( 用 DTJCB 开发 Java COM 客户机)介绍的步骤类似,只是桥设置的规范有些差别。我们把这个桥项目称作 SampleBridgeProject2。您需要输入如图 17 所示的桥设置。


    图 17. MyC2JBridge 的桥设置
    图 17. 桥设置

    注意图 17 中的设置与 图 5 中设置的差异。源库是一个扩展名为 .tlb 的文件,图 5 中的扩展名则是 .dll。这个设置告诉 TJCB 是在为 Java 组件而不是在为 COM 组件创建桥。而且, ICookie 在 “Java implemented Interfaces” 下面有一个复选标记。

    其余步骤实际上与前面的例子的相同。在桥生成过程的最后,您可以看到桥项目和接口的 Java 代理,如图 18 所示。


    图 18. SampleBridgeProject2 的桥项目
    图 18. SampleBridgeProject2 的桥项目

    现在您可以编写 Java 类来实现 ICookie 接口,用它调用服务器的代码。为了简化这个示例,我们添加了一个新类,它的方法返回一些简单数据。可以使用标准的 Eclipse 机制,用新建类向导添加类,如图 19 所示。


    图 19. 新建 MyCookie
    图 19. 新建类 MyCookie

    现在提供两个方法的简单实现,如清单 13 所示:


    清单 13. GetNameGetKind 的实现
    public class MyCookie implements ICookie {
    /* (non-Javadoc)
    * @see com.Cookie.ICookie#GetName()
    */
    public String GetName() throws IOException {
    return "Butter-n-Sweet";
    }
    /* (non-Javadoc)
    * @see com.Cookie.ICookie#GetKind()
    */
    public String GetKind() throws IOException {
    return "Buttermilk";
    }
    }
    

    构建 RJCB 桥项目

    现在就可以构建项目了。同样,因为 DTJCB 与 Eclipse 集成在一起,所以您可以使用 Eclipse 标准的 build 命令。

    注册 Java COM 服务器

    COM 技术严重依赖于 Windows 的注册表。所以需要确保正确地注册了正确的组件,其中包括:

    • DTJCB 生成的桥 DLL 文件。
    • 将于其中运行桥的 JVM,包括正确的类路径。
    • 实现接口的 Java 类以及其定制程序 ID。
    • RJCB 的运行时库 DLL。

    可以用 Windows 的 regsvr32.exe 命令以及 DTJCB 支持的 registerJavaVM.exeregisterJavaClass.exe 命令注册所有这些组件。一旦全部注册完这些组件,就可以让它们与 COM 客户机一起工作了。

    在 COM 客户机项目中使用 RJCB

    可以用几种方式来创建 COM 客户机。在这个练习中,假设您正在微软的 Visual Studio 中使用 Visual Basic 6.0。您首先要创建一个标准的 EXE 项目,并用 Projects/Reference 命令添加对 cookie.tlb 文件的引用。图 20 展示了该命令打开的 References 对话框,以及您应当如何添加这个引用。


    图 20. 将引用添加到项目中
    图 20. 将引用添加到项目中

    现在把 Command 按钮添加到窗体中,如图 21 所示。


    图 21. 窗体上的 Command 按钮
    图 21. 窗体上的 Command 按钮
    然后,添加通过桥进行调用、并从 Java 类检索信息的代码。图 22 显示了用户单击 Command1 按钮时执行的代码。
    图 22. Command 按钮执行的代码
    图 22. Command 按钮执行的代码

    请注意,代码会实例化 Cookie 对象,并通过桥透明地调用其方法。执行的结果就是在用户单击 Command1 按钮时,出现一个消息框,显示从 Java 类检索到的信息,如图 23 所示。


    图 23. 生成的消息框
    图 23. 生成的消息框
    使用独立 RJCB

    不必一定用 Eclipse 建立 RJCB 桥并。RJCB 也有自己独立的工具,您可以用它们生成桥。这样,就可以把生成的桥用于任何 Java 开发环境。

    RJCB 产品二进制的完整集可以在 DTJCB 安装的 com.ibm.rjcb.dtk.ui_1.0.0/generator 文件夹中找到。

    要在 Eclipse 之外建立 RJCB 桥,请用图形用户界面接口(GUI)模式启动 bin/generateBridge.exe 工具(也就是说,没有命令行参数)。它显示的窗体与 DTJCB 的窗体类似,在窗体中,您可以指定输入类型库、目标文件夹、Java 包名称等设置。然后单击 GenerateBuild 按钮就可以生成或生成/构建桥。

    您也可以用批处理模式,用命令行参数运行 generateBridge 工具。(DTJCB Eclipse 集成就是用这种方式调用该工具的。)执行工具时使用 /? 选项可以查看命令行参数的说明。决定需要在批处理模式中用哪个命令行参数生成桥的最好方法,就是先用 GUI 模式运行 generateBridge 工具。这样会生成一个叫作 bridgename.bat 的文件,其中包含在批处理模式下调用 generateBridge 工具的命令,而且包含所有必需的参数,同您在 GUI 的字段和选项中指定的值一一对应。每次在 GUI 中单击 GenerateBuild 按钮,都会生成该文件。

    因为 Eclipse 是行业中的首选 Java 开发环境,而且可以免费获得(请参阅 参考资料),所以我们极力推荐您用它和 DTJCB (请参阅侧栏 DTJCB 作为 Eclipse 扩展)来开发您的桥。与 Eclipse 的集成提供了更好的 all-in-one 体验。





    回页首


    部署 RJCB 桥

    可以用几种不同的方式来部署 RJCB 桥,具体的操作取决于使用桥的客户机的特征。客户机可以是单独的应用程序,也可以是 Eclipse 插件,或者是基于 COM 的客户机。我们将分别针对这些场景,讨论在每种场景中您需要部署什么。我们还会介绍如何在团队环境中开发、共享已经部署的部分。

    把“部分”部署到客户机环境

    对于应用程序的最终用户来说,不会在 Eclipse 内部或是基于 COM 的开发工具内部运行应用程序。所以要把桥以文件集合的形式部署到最终用户的系统上,包括可以部署的桥的各个部分和 RJCB 桥运行时。

    在另外一些情况下,在应用程序开发的时候,桥是共享的。在这些情况下,可以用 Eclipse 插件的形式部署桥,把它和容纳 RJCB 运行时的公共插件放在一起。这样可以使得正在进行中的 Eclipse 中的代码开发更容易有些。

    哪些是要部署的“部分”?

    通过前面看过的示例场景,您对桥本身和 RJCB 运行时需要部署的文件可能有了一些基本的了解:

    • 桥的可执行部分在两个文件中:一个 JAR 文件和一个 DLL 文件。这两个文件的文件名的格式是类型库文件名加上后缀 “Bridge”。例如,为 MSO.DLL 文件构建的桥的可执行部分是 MSOBridge.DLL 和 MSOBridge.JAR。

    • 当您将桥部署为 Eclipse 插件时,必须部署插件的 JAR 和 XML 文件,这样 Eclipse 才能正确地标识和加载加载插件。

    • RJCB 运行时文件是所谓公共插件的一部分,它负责加载运行时 DLL,为 Eclipse 中的所有桥插件导出 RJCB.jar。当您将桥部署为 Eclipse 插件时,还必须部署公共插件。对于独立 Java 程序来说,桥的客户机则必须自行处理这些操作,首先要把这些文件复制到合适的位置,然后在第一次使用桥之前动态地加载运行时 DLL,或者设置正确的 PATH 变量指向 DLL 的位置。独立 Java 程序客户机还必须把 RJCB 的 Java 库 RJCBRT.jar 文件添加到它的类路径中。请注意,我们推荐用运行时 DLL 的绝对路径显式地加载该文件。这样可以确保即使在机器上安装了 RJCB.DLL 的多个版本,也能加载正确的 RJCB.DLL 版本。

    部署场景的更多细节

    以下部署场景基于 RJCB 桥的类型(单向或双向)、客户机的类型(Eclipse 插件或独立 Java 程序),以及部署的桥是否要求额外的 COM 注册(与仅仅复制部署的文件相对应):

    • 把单向 RJCB 桥部署到 Eclipse 插件端(不需要注册任何 DLL):

      1. 把公共 RJCB 插件(com.ibm.xtools.rjcb.common)复制到目标 Eclipse 平台文件夹下的插件文件夹(如果在目标文件夹中还没有的话)。公共插件会包含并加载 RJCB 运行时。

      2. 把 RJCB 桥插件复制到目标 Eclipse 平台文件夹的插件文件夹中。每个桥插件在其 <requires> 段中都有一个对公共插件的引用:

        <requires>
        <import plugin="com.ibm.xtools.rjcb.common" export="true"/>
        </requires>
        



      3. 把桥插件的插件 ID(例如 xxx.yyy.zzz)添加到客户机插件的 plugin.xml 文件的 <requires> 段中,您需要通过客户机插件访问桥插件:

        <requires>
        <import plugin="xxx.yyy.zzz"/>
        </requires>
        



    • 把桥部署为独立 Java 客户机,不注册任何 DLL:

      1. 把桥的 JAR 文件放在客户机机器上的任意位置。

      2. 把桥的 DLL 文件放在客户机机器上的任意位置。

      3. 把 RJCBRT.DLL 运行时和 RJCBRT.jar 文件放在任意位置。

      4. 在第一次引用桥之前,用绝对路径加载 RJCBRT.DLL:

         RJCBUtilities.load(<absolute path to RJCBRT.DLL>)
        



      5. 把代码添加到 Java 客户机程序中,在第一次引用桥之前用 System.load 加载桥的 DLL:

         System.load(<absolute path to DLL>);
        



      6. 把桥 JAR 文件和运行时 RJCBRT.jar 添加到 Java 程序的类路径中。


    • 把桥插件部署到独立 Java 客户机,并注册必需的 DLL:

      1. 把桥 JAR 放在客户机计算机上的任意位置。

      2. 把桥 DLL 放在客户机计算机上的任意位置。

      3. regsvr32 命令注册桥 DLL。

      4. 把 RJCBRT.DLL 和 RJCBRT.jar 放在任意位置。

      5. regsvr32 命令注册 RJCBRT.DLL。

      6. 把桥 JAR 文件和 RJCBRT.jar 添加到 Java 程序的类路径中。

    • 把桥(Java 语言实现的 COM 服务器)部署到 COM 客户机:

      1. 把桥 JAR 文件放在任意位置。

      2. 把桥 DLL 放在任意位置。

      3. regsvr32 命令注册桥 DLL。

      4. 用 RJCB registerJavaVM.exe 工具注册用来实例化 Java 服务器的 JVM。

      5. 用 RJCB registerJavaClass.exe 工具注册 Java 服务器类。

      6. regsvr32 命令注册运行时 RJCBRT.DLL

    • 部署桥插件,把它从 Eclipse 导出为可部署的插件:

      1. 在 Eclipse 的 Package Explorer 中选中桥项目。

      2. 打开“Export”对话框。

      3. 选择 “Deployable Plugins and Fragments”。

      4. 在正在部署对话框中,选择目标 Eclipse 安装的 “eclipse” 文件夹作为“Destination Directory”,并提交对话框。

      5. 如果桥不是单向的(只从 Java 语言到 COM),那么必须用 regsvr32 命令注册桥 DLL。




    回页首


    在团队中用 RJCB 进行开发

    通常,由开发团队中的某一个人创建桥,并把它放在源代码控制当中,这样就可以在团队成员之间共享它。RJCB 桥项目的以下部分应当放在源代码控制之下:

    • 桥项目中的以下文件:
      • .project
      • .classpath
      • plugin.xml
      • build.properties
      • src (subfolder)
      这使参与项目的开发人员可以用 Eclipse 的 “Import”向导容易地把 RJCB 桥项目导入 Eclipse 环境,使它指向项目的位置,并可以在 Eclipse 内部构建和生成项目。

    • 所有 bridge.xml 文件(每个桥一个这样的文件)都位于桥的根文件夹中。

    • 用来生成桥的类型库源文件。DTJCB 把该文件从其原始位置复制到 RJCB 桥项目目录下的桥的位置上。您也需要这样做,这样,就可以在团队成员间共享源类型库。(其他团队成员可能没有该文件,或者有的只是其他位置上的该文件。)

    • 如何并不是所有团队成员都拥有桥的可执行文件,那么可以随意共享这些文件,例如,安装了 Visual Studio,但无法自己构建桥的安歇团队成员。

    共享 RJCB 运行时

    RJCB 桥客户机可以共享 RJCB 运行时的一个副本,也可以使用自己私有、独立的副本。要想共享该副本,必须在开发人员的机器上进行全局地配置运行库:

    • PATH 中有包含 RJCBRT.DLL 的目录。

    • regsvr32.exe 命令注册 RJCBRT.DLL。

    • 在类路径中有相应的 RJCBRT.JAR 文件。

    如果只有一个单向的 Java-COM(反过来是 COM 到 Java 语言)桥,还可以使用 RJCB 的独立副本,它甚至可以是不同的版本。这是有可能的,因为不一定要用 regsvr32 命令注册 RJCB 运行时并把它放在 PATH 中。





    回页首


    从 COM 到 Java 技术的迁移

    如果您很耐心地阅读本文,一直看到了现在,那么您可能会有一组基于 Java 和 COM 的应用程序或组件需要桥接。虽然 RJCB 桥提供了一个在两类应用程序之间进行互操作的方法,但在创建大型企业解决方案所有必需的桥时,仍然不是一项轻而易举的任务。

    管理这些应用程序或组件更好的策略,应当是把所有相互联接的应用程序或组件的实现统一到单一技术中。这会为您节约大量花费在处理底层组件互操作性问题上的精力,从而让您有更多的时间和资源解决真正需要解决的业务问题。把这作为我们的目标,让我们看一看怎样才能把所有组件迁移成基于 Java 的。

    策略:改革与发展

    让我们假设一个企业现有的应用程序中存在着 Java 和 COM 组件的混合,并希望把它们都转换成 Java 组件。如果这家企业有许多时间和资料,那么有可能用 Java 语言从头开始对整个应用程序进行重新设计并实现它。这样做有可能会形成一个整洁的架构,需要较少的开发工作和维持工作。而不利的一面是,风险会非常高,从一个应用程序切换到另外一个的冲击可能会非常巨大。如果应用程序规模较小,则可以采用这种方式。

    现实通常更具挑战性。企业可能没有时间和精力做这么大的投入,所以渐进的迁移可能是更现实的方案。一个一个地用 Java 组件替换 COM 组件,最终把应用程序的所有组件转换成 Java 技术会比较好。最直接的问题就是,您怎样才能有效地管理这些细微步骤,同时不会对应用程序的可用性造成重大破坏。

    用 RJCB 进行迁移

    RJCB 桥有助于把所有组件维持在一起。当您决定把 COM 组件转换成 Java 技术时,组件的新 Java 实现可以直接与它依赖的 Java 组件交互。对于仍然存在于 COM 中的依赖组件,您可以建立 RJCB 桥,使这些组件可以与新的 Java 实现交互。通过这个方法,一旦新实现完成,就可以与应用程序中其余的组件进行互操作。

    通过进行这种小型、递增的处理,应用程序可以一直保持可用,几乎没有什么中断,而且消除了完成迁移时的应用程序切换。该方法消除了与这种革命性迁移方法有关的巨大风险,赋予迁移计划更好的成功机会。





    回页首


    结束语

    Java 技术和 COM 是当今基于组件架构世界中的两大骨干技术。只要它们继续在组件互操作性上发挥重大作用,就需要弥补两项技术之间的间隙。RJCB 桥是一个有效的解决方案,而 DTJCB 则为您提供了一个多用途的实现 RJCB 桥的工具。



    参考资料



    作者简介

     

    Cheng-Yee Lin 是 IBM Rational 建模产品的开发经理。他曾经参与过多项与编译器、UML 和基于 API 集成有关的项目。Lin 先生拥有耶鲁大学计算机科学硕士和博士学位。


     

    Tom Houser 是 Rational Java/COM 桥技术的缔造者。早在加入 IBM 之前,他就已经在软件工具开发行业工作了 24 年,处理过编译器、调试器和建模工具。Houser 先生拥有麦迪逊威斯康星大学的计算机科学学士学位。


     

    Peter Parapounsky 是 IBM 软件集团下属的 Rational Software 的软件工程师。过去的 5 年中,他一直忙于 Rational XDE 产品的某些方面的研究。Parapounsky 先生拥有布拉格索非亚理工大学的计算机科学硕士学位。

    展开全文
  • 有了它,Java语法也可以得甜甜的

    千次阅读 多人点赞 2019-12-09 11:18:47
    Hutool是一个而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。 Hutool中的工具方法来自于每个用户的精雕细琢,它涵盖了...

    简介

    Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。

    Hutool中的工具方法来自于每个用户的精雕细琢,它涵盖了Java开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当;

    Hutool是项目中“util”包友好的替代,它节省了开发人员对项目中公用类和公用工具方法的封装时间,使开发专注于业务,同时可以最大限度的避免封装不完善带来的bug。

    这是官方对它的介绍,简单点说,它通过一些封装,将原来略显复杂的API进一步优化,使得你在使用的时候能够更加方便快捷,当然语法也会比原来更加简单易懂。

    包含组件

    一个Java基础工具类,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类,同时提供以下组件:

    模块 介绍
    hutool-aop JDK动态代理封装,提供非IOC下的切面支持
    hutool-bloomFilter 布隆过滤,提供一些Hash算法的布隆过滤
    hutool-cache 简单缓存实现
    hutool-core 核心,包括Bean操作、日期、各种Util等
    hutool-cron 定时任务模块,提供类Crontab表达式的定时任务
    hutool-crypto 加密解密模块,提供对称、非对称和摘要算法封装
    hutool-db JDBC封装后的数据操作,基于ActiveRecord思想
    hutool-dfa 基于DFA模型的多关键字查找
    hutool-extra 扩展模块,对第三方封装(模板引擎、邮件、Servlet、二维码、Emoji、FTP、分词等)
    hutool-http 基于HttpUrlConnection的Http客户端封装
    hutool-log 自动识别日志实现的日志门面
    hutool-script 脚本执行封装,例如Javascript
    hutool-setting 功能更强大的Setting配置文件和Properties封装
    hutool-system 系统参数调用封装(JVM信息等)
    hutool-json JSON实现
    hutool-captcha 图片验证码实现
    hutool-poi 针对POI中Excel的封装
    hutool-socket 基于Java的NIO和AIO的Socket封装

    可以根据需求对每个模块单独引入,也可以通过引入hutool-all方式引入所有模块。

    安装

    1、Maven项目

    在项目的pom.xml的dependencies中加入以下内容:

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.0.7</version>
    </dependency>
    

    2、非Maven项目

    下载地址: https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.0.7/

    在这里插入图片描述
    下载hutool-all-X.X.X.jar即可。

    下面对某几个组件的使用进行一个入门。

    类型转换

    1、常用类型转换

    在传统的类型转换过程中,我们需要使用到包装类的valueof()方法,例如:

    String s = "1234";
    int num = Integer.valueOf(s);
    double d = Double.valueOf(s);
    float f = Float.valueOf(s);
    

    但很显然实际情况并没有这么简单,在企业级的开发项目中,从前端传递过来的参数各式各样,类型繁多,我们如何知晓参数类型并作对应的转换呢?一般会先将所有参数转成String类型,如Web中的HttpServletRequest的getParamer()方法得到的数据类型就永远是String。转成String之后再将参数转成对应的数据类型,此时还需要考虑转换异常的问题,所以通常还需要在转换代码外面使用try-catch。

    为了使转换过程变得轻松愉快,HuTool为我们提供了类型转换工具——Convert。

    来看看使用Convert该如何进行转换:

    String s = "1234";
    int num = Convert.toInt(s);
    double d = Convert.toDouble(s);
    float f = Convert.toFloat(s);
    

    首先在代码风格上,Convert作了一个统一,统一使用Convert类作为工具类进行类型转换,而无需使用每个类型对应的包装类。

    当然了,Convert类的作用可远不止如此,比如:

    String[] s = { "1", "2", "3", "4", "5" };
    Integer[] intArray = Convert.toIntArray(s);
    Double[] doubleArray = Convert.toDoubleArray(s);
    Float[] floatArray = Convert.toFloatArray(s);
    Character[] charArray = Convert.toCharArray(s);
    

    将数组转换为任意类型的数组。

    对于集合,Convert同样支持转换:

    Object[] objs= {"hello","world","java",1};
    List<?> list = Convert.toList(objs);
    

    还有日期类型:

    String s = "2019-12-07";
    Date date = Convert.toDate(s);
    

    关于日期的处理,HuTool为我们提供了专门的工具类,这个我们放到后面说。

    2、其它类型转换

    通过Convert的convert()方法也能够实现上述的所有操作,不信我们可以试一试:

    String s = "1234";
    int num = Convert.convert(Integer.class, s);
    double d = Convert.convert(Double.class, s);
    float f = Convert.convert(Float.class, s);
    

    关于其它类型大家可以自己试一试,总之,通过convert()方法可以将任意类型转换为指定类型,但这种方法终归是有局限的,试问一下,我们如何将一个数组转换成List类型呢?

    我们可以通过一个重载方法convert( TypeReference reference, Object value ),该方法需要一个TypeReference对象参数,我们就可以创建TypeReference对象并通过嵌套泛型来指定需要转换的类型,比如:

    Object[] objs = { "hello", "world", "java"};
    List<String> list = Convert.convert(new TypeReference<List<String>>() {}, objs);
    

    Convert还提供了全角与半角符号之间的转换,比如:

    //将全角符转为半角符
    String s = "1,2,3,4,5";
    String dbc = Convert.toDBC(s);
    //将半角符转为全角符
    String sbc = Convert.toSBC(dbc);
    

    可以看看运行结果,更加直观:
    在这里插入图片描述

    3、编码转换

    在一些场景下,比如表单提交,会将参数进行一个简单的加密,此时通常会使用16进制转换,当然了,我们在准备16进制转换的时候也不会自己去写,都是去百度找一个现成的。不过,有了HuTool就不需要了,它为我们提供了方法用于完成16进制的转换。

    String s = "你好世界";
    //转换为16进制字符串
    String hex = Convert.toHex(s, CharsetUtil.CHARSET_UTF_8);
    //转换为普通字符串
    String str = Convert.hexToStr(hex, CharsetUtil.CHARSET_UTF_8);
    

    运行结果:

    e4bda0e5a5bde4b896e7958c
    你好世界
    

    注意编码对象要相同,这里都使用UTF-8编码,所以顺逆的转换过程都是成功的,如果编码不同,在转为普通字符串的时候就会出现乱码。

    还有Unicode编码和字符串的转换:

    String s = "你好世界";
    //转换为Unicode编码
    String unicode = Convert.strToUnicode(s);
    //转换为普通字符串
    String str = Convert.unicodeToStr(unicode);
    

    运行结果:

    \u4f60\u597d\u4e16\u754c
    你好世界
    

    Convert类还提供了convertCharset ()用于将字符串转换为指定编码的字符串,比如在处理表单数据时通常要处理乱码问题,如下:

    String s = "你好世界";
    //将字符串先转成乱码
    String str = Convert.convertCharset(s, CharsetUtil.UTF_8, CharsetUtil.ISO_8859_1);
    //处理乱码
    String result = Convert.convertCharset(str, CharsetUtil.ISO_8859_1, CharsetUtil.UTF_8);
    

    运行结果:

    ????????????
    你好世界
    

    还有金额大小写转换的功能:

    double money = 2019.127;
    String s = Convert.digitToChinese(money);
    

    运行结果:

    贰仟零壹拾玖元壹角叁分
    

    4、自定义类型转换

    Convert类的功能是不是非常强大呢?我们继续来看,对于数据类型转换,肯定是做不到包含所有数据类型的,因为Java面向对象的特性,但是HuTool提供了自定义类型转换。

    ConverterRegistry converterRegistry = ConverterRegistry.getInstance();
    converterRegistry.putCustom(Person.class, CustomConverter.class);
    String[] str = { "20", "张三" };
    Person person = converterRegistry.convert(Person.class, str);
    System.out.println(person);
    

    运行结果:

    Person [age=20, name=张三]
    

    该转换器将一个数组类型转换为了Person对象,它是如何实现的呢?(分三步)

    1. 自定义类实现Converter接口,并重写convert()方法
    2. 注册自定义的转换器
    3. 实现转换

    先定义一个Person类:

    public class Person {
    
    	private int age;
    	private String name;
    
    	public int getAge() {
    		return age;
    	}
    
    	public void setAge(int age) {
    		this.age = age;
    	}
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	@Override
    	public String toString() {
    		return "Person [age=" + age + ", name=" + name + "]";
    	}
    }
    

    然后自定义类实现Converter接口,并重写方法:

    public class CustomConverter implements Converter<Person> {
    
    	@Override
    	public Person convert(Object value, Person person) throws IllegalArgumentException {
    		person = new Person();
    		String[] str = Convert.toStrArray(value);
    		person.setAge(Convert.toInt(str[0]));
    		person.setName(str[1]);
    		return person;
    	}
    }
    

    该方法将传递过来的数据封装成Person对象并返回,实现类型转换。

    接着注册自定义的转换器:

    ConverterRegistry converterRegistry = ConverterRegistry.getInstance();
    converterRegistry.putCustom(Person.class, CustomConverter.class);
    

    现在就可以使用我们的转换器了,也就是刚刚的代码:

    String[] str = { "20", "张三" };
    Person person = converterRegistry.convert(Person.class, str);
    System.out.println(person);
    

    需要转换的数据是什么样式的,完全由你自定义的转换器决定,非常灵活,可根据自己的需求随意定制。

    日期时间处理

    对于日期时间的处理,Java提供了Date类和Calendar类,但就是因为有了更多的选择,使得日期时间转换的操作变得混乱和复杂,为此,HuTool提供了DateUtil工具。

    1、Date、long、Calendar的相互转换

    使用DateUtil可以实现Date、long和Calendar之间的相互转换,如下:

    // Calendar转为Date
    Date date = DateUtil.date(Calendar.getInstance());
    // long转为Date
    Date date2 = DateUtil.date(System.currentTimeMillis());
    // Date转为Calendar
    Calendar calendar = DateUtil.calendar(date);
    // long转为Calendar
    Calendar calendar2 = DateUtil.calendar(System.currentTimeMillis());
    

    2、日期字符串转换为Date

    String s = "2019-12-07";
    DateTime dateTime = DateUtil.parse(s);
    System.out.println(dateTime);
    

    运行结果:

    2019-12-07 00:00:00
    

    该方法能够将日期字符串转换为Date类型,它能够自动识别以下格式的字符串:

    1. yyyy-MM-dd HH:mm:ss
    2. yyyy-MM-dd
    3. HH:mm:ss
    4. yyyy-MM-dd HH:mm
    5. yyyy-MM-dd HH:mm:ss.SSS

    3、格式化日期

    格式化日期很简单,和Java的API没什么区别。

    String s = "2019-12-07";
    DateTime date = DateUtil.parse(s);
    
    String dateStr = DateUtil.format(date, "yyyy/MM/dd");
    System.out.println(dateStr);
    String dateStr2 = DateUtil.formatDate(date);
    System.out.println(dateStr2);
    String dateStr3 = DateUtil.formatDateTime(date);
    System.out.println(dateStr3);
    String dateStr4 = DateUtil.formatTime(date);
    System.out.println(dateStr4);
    

    通过format()方法可以将日期字符串转换为指定的格式,不过,DateUtil提供了其它的一些方法来作为常用的日期格式转换,看运行结果即可:

    2019/12/07
    2019-12-07
    2019-12-07 00:00:00
    00:00:00
    

    4、获取年、月、日

    对于年、月、日的获取,DateUtil也提供了非常简便的获取方式:

    // 获取当前日期时间
    DateTime date = DateUtil.date();
    System.out.println(date);
    // 获取年
    int year = DateUtil.year(date);
    System.out.println(year);
    // 获取月	从0开始
    int month = DateUtil.month(date);	
    System.out.println(month);
    

    运行结果:

    2019-12-07 21:45:42
    2019
    11
    

    5、日期时间偏移量

    对于日期时间的偏移,DateUtil同样能够很方便地实现,如下:

    String s = "2019-12-06 21:46:00";
    DateTime date = DateUtil.parse(s);
    
    // 日期往后偏移一天
    DateTime dateTime = DateUtil.offset(date, DateField.DAY_OF_MONTH, 1);
    System.out.println(dateTime);
    
    // 日期往后偏移两天
    DateTime dateTime2 = DateUtil.offsetDay(dateTime, 2);
    System.out.println(dateTime2);
    
    // 日期往后偏移一个小时
    DateTime dateTime3 = DateUtil.offsetHour(date, 1);
    System.out.println(dateTime3);
    

    运行结果:

    2019-12-07 21:46:00
    2019-12-09 21:46:00
    2019-12-06 22:46:00
    

    关于日期时间的偏移,通过offset()方法即可实现,该方法的第二个参数可传入偏移的单位,不过DateUtil还提供了一些比较常用的偏移方法,比如偏移天数、偏移小时。

    对于与当前十分接近的日期时间,DateUtil也提供了一些较为常用的方法,比如昨天、明天、上周、下周、上个月、下个月等:

    DateTime yesrerday = DateUtil.yesterday();
    System.out.println(yesrerday);
    		
    DateTime tomorrow = DateUtil.tomorrow();
    System.out.println(tomorrow);
    		
    DateTime lastMonth = DateUtil.lastMonth();
    System.out.println(lastMonth);
    		
    DateTime nextMonth = DateUtil.nextMonth();
    System.out.println(nextMonth);
    		
    DateTime lastWeek = DateUtil.lastWeek();
    System.out.println(lastWeek);
    		
    DateTime nextWeek = DateUtil.nextWeek();
    System.out.println(nextWeek);
    

    运行结果:

    2019-12-06 22:02:29
    2019-12-08 22:02:29
    2019-11-07 22:02:29
    2020-01-07 22:02:29
    2019-11-30 22:02:29
    2019-12-14 22:02:29
    

    6、计算日期时间差

    String s1 = "2019-12-06 22:15:00";
    DateTime date1 = DateUtil.parse(s1);
    String s2 = "2019-12-08 22:15:00";
    DateTime date2 = DateUtil.parse(s2);
    // 计算相差的天数
    long day = DateUtil.between(date1, date2, DateUnit.DAY);
    System.out.println(day);
    // 计算相差的小时数
    long hour = DateUtil.between(date1, date2, DateUnit.HOUR);
    System.out.println(hour);
    

    运行结果:

    2
    48
    

    对于两个日期时间的差值,通过between()方法能够很轻松地得到,该方法的第三个参数是需要计算的差值的单位。

    7、计时器

    DateUtil类还封装了计时器功能,用过传统的Timer计时器的同学就会知道,Timer计时器略显复杂,而DateUtil的封装则恰到好处。

    TimeInterval timer = DateUtil.timer();
    // 延迟2秒
    Thread.sleep(2000);
    // 花费的时间,单位:毫秒
    long interval = timer.interval();
    System.out.println(interval);
    // 花费的时间,单位:分
    long intervalMinute = timer.intervalMinute();
    System.out.println(intervalMinute);
    

    运行结果:

    2000
    0
    

    还有很多其它的方法,篇幅有限,就不一一例举了。

    8、其它

    考虑到一些比较常见的场景,例如计算一个人的年龄,判断给定年份是否为闰年,DateUtil也给出了相应的解决办法。

    int age = DateUtil.ageOfNow("1999-08-08");
    System.out.println(age);
    boolean leapYear = DateUtil.isLeapYear(2019);
    System.out.println(leapYear);
    

    运行结果:

    20
    false
    

    IO操作

    IO操作是Java中比较重要的操作之一,为此,Java提供了InputStream、OutputStream、Reader、Writer等接口,而实现类又非常的多,往往选择一多,我们就不知道该如何选择,而HuTool为我们封装了一系列的工具类。

    FileUtil(文件操作工具类)

    既然是IO流,那就离不开文件操作,HuTool为我们提供了FileUtil工具类用来解决大部分的文件操作问题。

    对于文件操作的方法,有Linux基础的同学肯定非常熟悉,开源项目的作者努力将方法名与Linux保持一致,比如创建文件的方法不是createFile()而是touch()。

    File[] files = FileUtil.ls("E:");
    for (File file : files) {
    	System.out.println(file);
    }
    

    运行结果:

    E:\$RECYCLE.BIN
    E:\androidSdk
    E:\androidstudio
    E:\b2631f36a0808f7d3cab19543d645e63
    E:\flutter
    E:\Java
    E:\MailMasterData
    E:\QLDownload
    E:\QQPCmg
    E:\QQPCmgr
    E:\qycache
    E:\System Volume Information
    E:\test.txt
    E:\Tool
    E:\迅雷下载
    

    ls()方法会列出给定路径下的所有目录和文件。

    FileUtil.touch("E:/test/hello.txt");
    

    touch()方法用于创建文件,如果父目录不存在也自动创建,比如这里的hello.txt文件,倘若E盘下没有test目录,则会先创建test文件夹,再在test目录下创建hello.txt文件。

    其它方法也如上所示使用,就不一一演示了:

    • mkdir 创建目录,会递归创建每层目录
    • del 删除文件或目录(递归删除,不判断是否为空),这个方法相当于Linux的delete命令
    • copy 拷贝文件或目录

    注意:对于del()方法,它会直接删除目录而不判断其是否为空,所以请谨慎使用。

    IOUtil(IO工具类)

    第二个便是IOUtil,该工具类主要针对输入输出流的一个封装。

    1、文件复制

    针对文件复制操作,IOUtil能够很轻松地完成。

    BufferedInputStream inputStream = FileUtil.getInputStream("C:/Users/Administrator/Desktop/eclipseworkspace/HuToolDemo/src/com/wwj/test/TestDemo.java");
    BufferedOutputStream outputStream = FileUtil.getOutputStream("E:/test.txt");
    long copySize = IoUtil.copy(inputStream, outputStream, IoUtil.DEFAULT_BUFFER_SIZE);
    

    这里就复制了一下当前的java文件并将其保存至E盘下的test.txt文件中。

    2、读流写流

    读流写流也是IO操作中使用频率非常高的操作,它跟传统的方式没有太大区别,只不过对调用者进行了统一。

    BufferedInputStream inputStream = FileUtil.getInputStream("C:\\Users\\Administrator\\Desktop\\eclipseworkspace\\HuToolDemo\\src\\com\\wwj\\test\\TestDemo.Java");
    BufferedOutputStream outputStream = FileUtil.getOutputStream("E:\\test.txt");
    int len = -1;
    byte[] buffer = new byte[1024];
    while ((len = inputStream.read(buffer)) != -1) {
    	outputStream.write(buffer, 0, len);
    }
    IoUtil.close(inputStream);
    IoUtil.close(outputStream);
    

    下面列出关于读写流操作相关的方法:

    • readBytes 返回byte数组(读取图片等)
    • readHex 读取16进制字符串
    • readObj 读取序列化对象(反序列化)
    • readLines 按行读取

    对于写流操作,IoUtil提供了两个write()的重载方法,当然也可以直接使用输出流的write()方法,而事实上,IoUtil的write()方法也是这么做的。

    我们再来看看IoUtil如何读写图片,对于图片的读写操作,它提供了readBytes()方法,使用该方法读写图片简直不要太简单:

    BufferedInputStream inputStream = FileUtil.getInputStream("C:\\Users\\Administrator\\Desktop\\eclipseworkspace\\HuToolDemo\\默认图表.png");
    BufferedOutputStream outputStream = FileUtil.getOutputStream("E:\\test.png");
    byte[] bytes = IoUtil.readBytes(inputStream);
    outputStream.write(bytes);
    IoUtil.close(inputStream);
    IoUtil.close(outputStream);
    

    这样即可完成图片的读写。

    IoUtil还提供了一些其它方法用于简化编程,比如:toStream()方法用于将某些对象转换为流对象;writeObjects()方法用于将可序列化对象序列化后写入到流中。

    3、释放流资源

    IO操作中的一个好习惯就是用完哪个流就关掉哪个流,而关闭操作会面临两个问题:

    • 被关闭的对象为空
    • 对象关闭失败

    而IoUtil提供的close()方法则很好地解决了这些问题,我们只需将要关闭的流传入close()方法即可。

    FileTypeUtil(文件类型判断工具类)

    FileTypeUtil是一个判断文件类型的工具类,它并不是通过文件的扩展名来确定文件类型,而是通过读取文件的首部几个二进制位来判断。

    File file = FileUtil.file("C:\\Users\\Administrator\\Desktop\\eclipseworkspace\\HuToolDemo\\默认图表.png");
    String type = FileTypeUtil.getType(file);
    Console.log(type);
    

    运行结果:

    png
    

    FileReader(文件读取)

    虽然FileUtil已经提供了关于文件读写的API,但是根据职责分离原则,HuTool还是为我们提供了FileReader类专门读取文件。

    在JDK中同样提供了FileReader类,但并不好用,HuTool正是对它进行的一个升级。

    FileReader fileReader = new FileReader("test.properties");
    String result = fileReader.readString();
    System.out.println(result);
    

    运行结果:

    username=root
    password=123456
    

    该类还提供了一些常用的方法帮助文件读取:

    • readBytes
    • readString
    • readLines

    文件写入(FileWriter)

    有文件读取,肯定就会有文件写入,使用方法和FileReader是一样的,这里就不做代码演示了。

    写入方式分为追加和覆盖两种模式,追加的话可以用append()方法,覆盖可以用write()方法;当然你也可以直接使用write()方法,并将写入模式作为第二个参数传入。

    资源访问

    在Java开发中,资源访问是比较常见的操作,例如在操作数据库、整合框架的时候,需要频繁地访问配置文件,通常我们会将配置文件放在类路径下,方便访问:

    InputStream in = TestDemo.class.getResource("test.properties").openStream();
    

    对于资源访问这种频繁而且麻烦的操作,HuTool对其进行了封装。

    ClassPathResource resource = new ClassPathResource("test.properties");
    Properties properties = new Properties();
    properties.load(resource.getStream());
    Console.log(properties);
    

    运行结果:

    {password=123456, username=root}
    

    当然,对于资源访问的封装远不止如此,这个放到后面说。

    最后

    以上内容只是HuTool项目的冰山一角,我将在下篇相关文章中继续介绍该项目下封装的一些工具类。

    展开全文
  • 文章①原文地址:http://www.blogjava.net/dyerac/archive/2006/04/03/38984.html 图形界面开发对于Java来说并非它的长项,开发者经常会碰到各种各样的限制,比如,如何打造一款任意形状的窗口?如何可以透过窗口...

    文章①

    原文地址:http://www.blogjava.net/dyerac/archive/2006/04/03/38984.html

     

    图形界面开发对于Java来说并非它的长项,开发者经常会碰到各种各样的限制,比如,如何打造一款任意形状的窗口如何可以透过窗口显示它覆盖下的内容

    考虑到Java并没有被设计成支持以上的功能,所以,你能得到的永远是方方正正的窗口,毫无新意,当然,我们可以通过JNI调用本地代码来完成,但是这就失去了java可移植性的意义,那么,用纯粹的java代码如何实现以上两种功能呢?

    下文提供了一个实现的参考

    预备知识
    1.java.awt.Robot,这个类是一个功能非常强大的类,通过它我们可以控制鼠标和键盘的响应,不过这里,我们只需要用到它的一个功能--截屏,也就是得到当前屏幕的快照(screenshot)
    2.我们可以通过调用一个swing组件的paintComponent(Graphics g)方法用特定的Graphics为其定制特殊的外观

    首先声明的一点是,本文中的实现方法只是一个欺骗的手法,因为要想实现前文中的功能,我们几乎的重写一套Swing出来,这里,简便的做法是,我们通过robot类来获得当前屏幕的快照,然后贴在我们需要的窗口上,这样,不就实现了透明的功能了吗?顺便提一下,几年前日本发明的隐形衣也是依靠这种机制,这种衣服在身后附带一套摄像机,然后即时的将拍下的内容显示在衣服的正面,因此,当别人看过来时,仿佛就通过人和衣服看到了身后的场景^_^

    另外,还要感谢Joshua Chris leniz的Swing Hack一书,以上充满创新的方法正来自他的书中

    好咯,让我们具体看一下细节的处理:
    第一,我们要得到当前屏幕的快照,保存到一个Image对象 background 中:
      public void updateBackground() {
      try {
       Robot rbt = new Robot();
       Toolkit tk = Toolkit.getDefaultToolkit();
       Dimension dim = tk.getScreenSize();
       [color=Red]background [/color]= rbt.createScreenCapture(new Rectangle(0, 0, (int) dim
         .getWidth(), (int) dim.getHeight()));
      } catch (Exception ex) {
       }
     }

    第二,我们需要让窗口显示这个图像,也就是说,让窗口的背景图像是这副图像,以达到透明的欺骗效果:

    public void paintComponent(Graphics g) {
      Point pos = this.getLocationOnScreen();
      Point offset = new Point(-pos.x, -pos.y);
      g.drawImage([color=Red]background[/color], offset.x, offset.y, null);
     }

    在swing hack一书中,作者也给出了他的实现,然而,运行结果表明该实现存在很大的问题:窗口经常无法即时更新,往往背景变了,窗口里显示的却还是以前的背景。仔细研究了他的代码,我发现了问题的根结,同时也是这种实现技术的关键要素--如何让窗口在正确的时机里更新显示,下面,我们会讨论这一点

    第三,更新窗口外观
    前两步的操作,只能得到一副当前屏幕的快照,一旦背景变化了,或者窗口移动了,变化大小了,那么我们制作的窗口将永远无法和和屏幕背景联合成整体,也就失去了透明的效果;同时,我们也不可能每时每刻都调用
    updateBackground() 方法获得最新的背景,可行的方法是,通过对事件的监听来选择适当的时间来更新外观

    我们应该可以达到这三点共识
    1。窗口移动或改变大小时,背景图像一般是不会发生变化的,我们不需要得到新的屏幕快照,只用将以前得到的背景中适当的部分贴到窗口上,调用repaint()方法就足已
    2。要获得最新的屏幕快照,必须先将窗口隐藏起来,调用updateBackground() 得到图像后再把窗口显示,我们可以用refresh方法来表示
     refresh(){
        frame.hide();
        updateBackground() ;
       frame.show();
    }
    3。如果背景改变了,那么一定是别的windows程序获得了或失去了事件焦点,也就是说我们关注的窗口将触发焦点得失事件,这个时候需要调用refresh() 得到最新的屏幕快照


    看到这里,你或许认为已经没有技术难点了,然而,此时才是 我们最需要关注的地方
    参看第三点,我们需要在窗口得失焦点时调用refresh() 方法;参看第一点,我们调用refresh() 方法时需要先将窗口隐藏,然后再显示。于是问题来了,在隐藏和显示窗口时,同样会触发得失焦点事件,得失焦点事件又将触发新的隐藏和显示窗口事件(为了得到新的屏幕快照),这就使程序陷入了死循环中,我们必须加以控制,使得第二次触发焦点得失事件时不调用refresh()方法

    作者的办法是加一个线程来控制,通过判断时间间隔长短来决定是否调用refresh()方法,可是,这个条件是程序非常的不稳定,因为往往调用时间会根据系统繁忙度而改变,使得需要更新时不能更新,不需要更新的时候反而更新了
    因此,我决定采取新的解决方案,能不能隐藏/显示窗口时不触发得失焦点事件呢?
    解决方法很简单,我抛开了传统的setVisible()或者show(),hide()方法,而是使用setLocation()方法,因为调用setLocation()方法时窗口不会失去焦点,同时,只要用类似setLocation(-2000,-2000)方法也同样可以轻松的让窗口在屏幕中消失
    下面是我的全部代码:

    import java.awt.BorderLayout;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Image;
    import java.awt.Point;
    import java.awt.Rectangle;
    import java.awt.Robot;
    import java.awt.Toolkit;
    import java.awt.event.ComponentEvent;
    import java.awt.event.ComponentListener;
    import java.awt.event.WindowEvent;
    import java.awt.event.WindowFocusListener;

    import javax.swing.JButton;
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.UIManager;

    import org.jvnet.substance.SubstanceLookAndFeel;
    import org.jvnet.substance.theme.SubstanceLightAquaTheme;

    import ch.randelshofer.quaqua.QuaquaLookAndFeel;

    public class TestEvent extends JComponent implements ComponentListener,
      WindowFocusListener {
     private JFrame frame;

     private Boolean isHiding = false, isShowing = false, start = false;

     private Image background;

     private Point p;
                         
                        //获得当前屏幕快照
     public void updateBackground() {
      try {
       Robot rbt = new Robot();
       Toolkit tk = Toolkit.getDefaultToolkit();
       Dimension dim = tk.getScreenSize();
       background = rbt.createScreenCapture(new Rectangle(0, 0, (int) dim
         .getWidth(), (int) dim.getHeight()));
      } catch (Exception ex) {
       // p(ex.toString());
       // 此方法没有申明过 ,因为无法得知上下文 。因为不影响执行效果 ,先注释掉它 ex.printStackTrace();
      }

     }

                        //将窗口掉离出屏幕以获得纯粹的背景图象
     public void refresh() {
      if (start == true) {
       this.updateBackground();
       frame.setLocation(p);
       if (p.x < 0 || p.y < 0)
        frame.setLocation(0, 0);
       this.repaint();
      }
     }

     public void componentHidden(ComponentEvent e) {
      // TODO Auto-generated method stub
      System.out.println("Hidden");
     }

                         //窗口移动时
     public void componentMoved(ComponentEvent e) {
      // TODO Auto-generated method stub
      System.out.println("moved");
      this.repaint();
     }

                         //窗口改变大小时
     public void componentResized(ComponentEvent e) {
      // TODO Auto-generated method stub
      System.out.println("resized");
      this.repaint();
     }

     public void componentShown(ComponentEvent e) {
      // TODO Auto-generated method stub
      System.out.println("shown");
     }

                         //窗口得到焦点后,用refresh()方法更新界面
     public void windowGainedFocus(WindowEvent e) {
      // TODO Auto-generated method stub
      System.out.println("gainedFocus");
      refresh();
      start = false;
     }

                         //窗口失去焦点后,将其移出屏幕
     public void windowLostFocus(WindowEvent e) {
      // TODO Auto-generated method stub
      System.out.println("lostFocus");
      if (frame.isShowing() == true) {
       System.out.println("visible");
      } else {
       System.out.println("invisible");
      }
      start = true;
      p = frame.getLocation();
      frame.setLocation(-2000, -2000);
     }

     public TestEvent(JFrame frame) {
      super();
      this.frame = frame;
      updateBackground();
      this.setSize(200, 120);
      this.setVisible(true);
      frame.addComponentListener(this);
      frame.addWindowFocusListener(this);

      // TODO Auto-generated constructor stub
     }

                        //绘制外观,注意,其中 pos,offset 是为了将特定部分的图象贴到窗口上
     public void paintComponent(Graphics g) {
      Point pos = this.getLocationOnScreen();
      Point offset = new Point(-pos.x, -pos.y);
      g.drawImage(background, offset.x, offset.y, null);
     }

     /**
      * @param args
      */
     public static void main(String[] args) {
      // TODO Auto-generated method stub
      try {
       // UIManager.setLookAndFeel("org.fife.plaf.Office2003.Office2003LookAndFeel");
       // UIManager.setLookAndFeel("org.fife.plaf.OfficeXP.OfficeXPLookAndFeel");
       // UIManager.setLookAndFeel("org.fife.plaf.OfficeXP.OfficeXPLookAndFeel");
       UIManager.setLookAndFeel(new SubstanceLookAndFeel());
       //UIManager.setLookAndFeel(new SmoothLookAndFeel());
       //UIManager.setLookAndFeel(new QuaquaLookAndFeel());
       UIManager.put("swing.boldMetal", false);
       if (System.getProperty("substancelaf.useDecorations") == null) {
        JFrame.setDefaultLookAndFeelDecorated(true);
        //JDialog.setDefaultLookAndFeelDecorated(true);
       }
       System.setProperty("sun.awt.noerasebackground", "true");
       SubstanceLookAndFeel.setCurrentTheme(new SubstanceLightAquaTheme());

       // UIManager.setLookAndFeel("org.fife.plaf.VisualStudio2005.VisualStudio2005LookAndFeel");
      } catch (Exception e) {
       System.err.println("Oops!  Something went wrong!");
      }

      JFrame frame = new JFrame("Transparent Window");
      TestEvent t = new TestEvent(frame);
      t.setLayout(new BorderLayout());
      JButton button = new JButton("This is a button");
      t.add("North", button);
      JLabel label = new JLabel("This is a label");
      t.add("South", label);
      frame.getContentPane().add("Center", t);
      frame.pack();
      frame.setSize(150, 100);
      frame.show();
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      // t.start=true;
     }

    }


    当然,以上代码还存在许多不足,比如在移动窗口时性能会有影响,应该可以通过线程来控制其更新的频率来提升效率,希望大家能一起研究一下,改好了记得通知我哦

    ps:你也许还会说,e?怎么没讲任意形状窗口如何打造?
           呵呵,其实只要把窗口按上述方法设置成透明,再去掉边框,再换上自己的边框,不就完成了马?
         相信聪明的各位一定很容易就办到咯

    另外,我还在下面附带了作者的大论,想了解更多的同学也可以去看看

    http://www.matrix.org.cn/resource/article/44/44186_Swing.html

     


     

    文章②

    原文位置:http://java.chinaitlab.com/Swing/350375.html

    要生成一个半透明的成形窗口,而又要避免使用本地的编码,唯有灵活地应用screenshot(屏幕快照).

      半透明窗口是大众对Swing最为渴求的特性之一. 也可以称之为定形窗口,这种窗口有一部分是透明的,可以透过它看到桌面背景和其它的程序.如果不通过JNI(Java Native Interface 本地接口)Java是无法为我们生成一个半透明的窗口的(即使我们可以那样做,还得本地操作平台好支持半透明窗口才行).然而这些现状无法阻止我们对半透明窗口的渴求,通过一个我最喜欢的手段screenshot,我们可以欺骗性地实现这个目的.

      仿造这样一个的半透明窗口的过程,主要的通过以下几点:
    1.在窗口显示之前,先获得一个screenshot;
    2.把上一步获取的屏幕快照,作为窗口的背景图
    3.调整位置,以便于我们捕获的screenshot和实际当前的屏幕完美结合,制造出一种半透明的假象.

      刚刚说到的部分只是小儿科,重头戏在于,如何在移动或变化半透明窗口时,及时地更新screenshot,也就是及时更新半透明窗口的背景.

      在开始我们的旅行之前,先生成一个类,让它继承 JPanel,我们用这个继承类来捕获屏幕,并把捕获的照片作为背景. 类的具体代码如下例6-1

    例 6-1 。 半透明背景组件
    public class TransparentBackground extends Jcomponent {
        private JFrame frame;
        private Image background;

    public TransparentBackground(JFrame frame) {
        this.frame = frame;
        updateBackground( );
    }
    /**
      * @todo 获取屏幕快照后立即更新窗口背景
      */
    public void updateBackground( ) {
        try {
            Robot rbt = new Robot( );
            Toolkit tk = Toolkit.getDefaultToolkit( );
            Dimension dim = tk.getScreenSize( );
            background = rbt.createScreenCapture(
            new Rectangle(0,0,(int)dim.getWidth( ),
                              (int)dim.getHeight( )));
        } catch (Exception ex) {
            //p(ex.toString( ));
    // 此方法没有申明过,因为无法得知上下文。因为不影响执行效果,先注释掉它
            ex.printStackTrace( );
        }
    }
    public void paintComponent(Graphics g) {
        Point pos = this.getLocationOnScreen( );
        Point offset = new Point(-pos.x,-pos.y);
        g.drawImage(background,offset.x,offset.y,null);
    }
    }
      首先,构造方法把一个reference保存到父的JFrame,然后调用updateBackground()方法,在这个方法中,我们可以利用java.awt.Robot类捕获到整个屏幕,并把捕获到的图像保存到一个定义了的放置背景的变量中. paintComponent()方法可以帮助我们获得窗口在屏幕上的绝对位置,并用刚刚得到的背景作为panel的背景图,同时这个背景图会因为panel位置的不同而作对应的移动,以使panel的背景和panel覆盖的那部分屏幕图像无缝重叠在一起,同时也就使panel和周围的屏幕关联起来.

    我们可以通过下面这个main方法简单的运行一下,随便放置一些组件到panel上,再把panel放置到frame中显示.
    public static void main(String[] args) {
        JFrame frame = new JFrame("Transparent Window");
        TransparentBackground bg = new TransparentBackground(frame);
        bg.setLayout(new BorderLayout( ));
        JButton button = new JButton("This is a button");
        bg.add("North",button);
            JLabel label = new JLabel("This is a label");
        bg.add("South",label);
        frame.getContentPane( ).add("Center",bg);
        frame.pack( );
        frame.setSize(150,100);
        frame.show( );
    }
    通过这段代码,运行出的效果如下图6-1所示:

    图6-1 展示中的半透明窗口

      这段代码相当简单,却带有两个不足之处。首先,如果移动窗口,panel中的背景无法自动的更新,而paintComponent()只在改变窗口大小时被调用;其次,如果屏幕曾经发生过变化,那么我们制作的窗口将永远无法和和屏幕背景联合成整体。

      谁也不想时不时地跑去更新screenshot,想想看,要找到隐藏于窗口后的东西,要获得一份新的screenshot,还要时不时的用这些screenshot来更新我们的半透明窗口,这些事情足以让用户无法安心工作。事实上,想要获取窗口之外的屏幕的变化几乎是不太可能的事,但多数变动都是发生在foreground窗口发生焦点变化或被移动之时。如果你接受这的观点(至少我接受这个观点),那么你可以只监控下面提到的几个事件,并只需在这几个事件被触发时,去更新screenshot。
    public class TransparentBackground extends JComponent
            implements ComponentListener, WindowFocusListener,
            Runnable {
        private JFrame frame;
        private Image background;
        private long lastupdate = 0;
        public boolean refreshRequested = true;
        public TransparentBackground(JFrame frame) {
            this.frame = frame;
            updateBackground( );
            frame.addComponentListener(this);
            frame.addWindowFocusListener(this);
            new Thread(this).start( );
        }
        public void componentShown(ComponentEvent evt) { repaint( ); }
        public void componentResized(ComponentEvent evt) { repaint( ); }
        public void componentMoved(ComponentEvent evt) { repaint( ); }
        public void componentHidden(ComponentEvent evt) { }

        public void windowGainedFocus(WindowEvent evt) { refresh( ); }    
        public void windowLostFocus(WindowEvent evt) { refresh( ); }
      首先,让我们的半透明窗口即panel实现ComponentListener接口,
    WindowFocusListener接口和Runnable接口。Listener接口可以帮助我们捕获到窗口的移动,大小变化,和焦点变化。实现Runnable接口可以使得panel生成一个线程去控制定制的repaint()方法。

      ComponentListener接口带有四个component开头的方法。它们都可以很方便地调用repaint()方法,所以窗口的背景也就可以随着窗口的移动,大小的变化而相应地更新。还有两个是焦点处理的,它们只调用refresh(),如下示意:
    public void refresh( ) {
        if(frame.isVisible( )) {
            repaint( );
            refreshRequested = true;
            lastupdate = new Date( ).getTime( );
        }
    }
    public void run( ) {
        try {
            while(true) {
                Thread.sleep(250);
                long now = new Date( ).getTime( );
                if(refreshRequested &&
                    ((now - lastupdate) > 1000)) {
                    if(frame.isVisible( )) {
                        Point location = frame.getLocation( );
                        frame.hide( );
                        updateBackground( );
                        frame.show( );
                    frame.setLocation(location);
                        refresh( );
                    }
                    lastupdate = now;
                    refreshRequested = false;
                    }
                }
            } catch (Exception ex) {
                p(ex.toString( ));
                ex.printStackTrace( );
            }
        }

      refresh()可以保证frame可见,并适时得调用repaint()。它也会对refreshRequest变量置真(true),同时保存当前时间值,现在所做的这些对接下来要做的事是非常重要的铺垫。

      除了每四分之一秒被唤醒一次,用来检测是否有新的刷新的要求或者是否离上次刷新时间超过了一秒,方法run()一般地处于休眠状态。如果离上次刷新超过了一秒并且frame是可见的,那么run()将保存frame的位置,隐藏frame,获取一个screenshot,更新frame背景,再根据隐藏frame时保存的位置信息,重新显示已经更新了背景的frame,接着调用refresh()方法。通过这样的控制,使得背景更新不至于比需要的多太多。

      那么我们为什么要对用一个线程控制刷新如此长篇大论呢?一个词:递归。事件处理可以直接轻松地调用repaint(),但是隐藏和显示窗口已便于获取screenshot 却交替了很多“得焦”和“失焦”事件。所有这些都会触发一个新的背景更新,导致窗口再次被隐藏,如此往返,将导致永无止境的循环。一个新的“得焦”事件,将在执行refresh()几毫秒之后被调用,所以简单地检测isRecursing标志是无法阻止循环的继续。

      另外,用户任意一个改变屏幕的动作,将会随之引出一堆的事件来,而不仅仅是简单一个。应该是最后一个事件去触发updateBackground(),而不是第一个。为了全面解决这些问题,代码产生一个线程,然后用这个线程去监控重画(repaint)要求,并保证当前的执行动作是发生在过去的1000毫秒内没有发生过此动作。如果一个客户每五秒不间断地产生事件(比如,寻找丢失的浏览窗口),那么只有在其它所有工作在一秒内完成才执行更新。这样就避免了,用户不至于在移动东西时,窗口却消失不见了的尴尬。

      另一件烦恼的事就是,我们的窗口仍旧有边框,这条边框使得我们无法完美和背景融为一体。更为痛苦的是使用setUndecorated(true)移除边框时,我们的标题栏和窗口控制栏也跟着移除了。可是这也算不上是什么大问题,因为那类使用定形窗口的应用程序一般都具有可拖动的背景【Hack#34】

      接下来,我们在下面这个简单的测试程序中把所讲的东西落实进去:
    public static void main(String[] args) {
        JFrame frame = new JFrame("Transparent Window");
        frame.setUndecorated(true);
        
        TransparentBackground bg = new TransparentBackground(frame);
        bg.snapBackground( );
        bg.setLayout(new BorderLayout( ));

       JPanel panel = new JPanel( ) {
            public void paintComponent(Graphics g) {
                g.setColor(Color.blue);
                Image img = new ImageIcon("mp3.png").getImage( );
                g.drawImage(img,0,0,null);
            }
        };
        panel.setOpaque(false);

        bg.add("Center",panel);

        frame.getContentPane( ).add("Center",bg);
        frame.pack( );
        frame.setSize(200,200);
        frame.setLocation(500,500);
        frame.show( );
    }
      这段代码通过继承JPanel,加上一个透明的PNG格式图片,人工生成一个mp3播放器界面。注意使用了setUndecorated()来隐藏边框和标题栏。调用setOpaque(false),将隐藏默认的背景(一般为灰色),这样screenshot的背景就可以和图片中透明的部分合成一个整体,去配合程序窗口周围的屏幕背景。(如图6-2)通过一系列的努力,就可以看到图6-3的效果。是不是很让人惊诧?会不会感叹Java的新版本腾空出世?


    图 6-2. mp3 播放器外观模板

    image  
    图 6-3. 运行中的mp3播放器

    展开全文
  • Java基础知识面试题(2020最新版)

    万次阅读 多人点赞 2020-02-19 12:11:27
    文章目录Java概述何为编程什么是Javajdk1.5之后的三版本JVM、JRE和JDK的关系什么是跨平台性?原理是什么Java语言有哪些特点什么是字节码?采用字节码的最大好处是什么什么是Java程序的主类?应用程序和程序的...
  • Java面试题大全(2020版)

    万次阅读 多人点赞 2019-11-26 11:59:06
    发现网上很多Java面试题都没有答案,所以花了很长时间搜集整理出来了这套Java面试题大全,希望对大家有帮助哈~ 本套Java面试题大全,全的不能再全,哈哈~ 一、Java 基础 1. JDK 和 JRE 有什么区别? JDK:Java ...
  • Java面试笔试题汇总(最全+详细答案)

    万次阅读 多人点赞 2018-05-28 09:08:36
    本篇文章来自一位很资深的前辈对于最近java面试题目所做的总结归纳,有170道题目 ,知识面很广 ,而且这位前辈对于每个题都自己测试给出了答案 ,如果你对某个题有疑问或者不明白,可以电脑端登录把题目复制下来然后...
  • 牛逼!Java 从入门到精通,超全汇总版

    万次阅读 多人点赞 2021-05-06 19:40:33
    文章目录Java 基础Head First JavaJava 核心技术卷一Java 编程思想设计模式Head First 设计模式图解设计模式设计模式重学 Java 设计模式Java 进阶Java 并发编程实战Java 并发编程艺术Java 并发编程之美图解Java多...
  • java

    千次阅读 2020-07-17 16:35:33
    jvm:jre的一部分,他是整个java实现跨平台的最核心部分,负责解释执行Java字节码文件,是运行java字节码文件的虚拟计算机,java跨平台的原因:Java源文件被虚拟机编译成字节码文件后,生成的是与平台无关的字节码,...
  • Java

    千次阅读 2020-08-01 09:56:23
    Java 1、Java 简介 Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表...
  • Java基础篇:反射机制详解

    万次阅读 多人点赞 2018-09-29 10:19:50
    反射是Java的特征之一,是一种间接操作目标对象的机制,核心是JVM在运行的时候才动态加载类,并且对于任意一个类,都能够知道这个类的所有属性和方法,调用方法/访问属性,不需要提前在编译期知道运行的对象是谁,...
  • JAVA 面试题含部分答案

    千次阅读 2019-06-12 14:50:26
    说了这么多,下面进入我们本文的主题,我们这份面试题,包含的内容了十九了模块:Java 基础、容器、多线程、反射、对象拷贝、Java Web 模块、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、...
  • Java面试题全集(中)

    万次阅读 多人点赞 2015-04-09 22:05:20
    2015年重新整理发布的Java面试题全集,这部分主要是与Java Web和Web Service相关的面试题。
  • Java面试题大全(2021版)

    万次阅读 多人点赞 2020-11-25 11:55:31
    发现网上很多Java面试题都没有答案,所以花了很长时间搜集整理出来了这套Java面试题大全,希望对大家有帮助哈~ 本套Java面试题大全,全的不能再全,哈哈~ 一、Java基础知识面试题 1、Java概述 ①. 何为编程 ...
  • Java虚拟机(JVM)面试题(2020最新版)

    万次阅读 多人点赞 2020-02-19 12:26:32
    文章目录Java内存区域说一下 JVM 的主要组成部分及其作用?说一下 JVM 运行时数据区深拷贝和浅拷贝说一下堆栈的区别?队列和栈是什么?有什么区别?HotSpot虚拟机对象探秘对象的创建为对象分配内存处理并发安全问题...
  • 深入理解Java类型信息(Class对象)与反射机制

    万次阅读 多人点赞 2017-05-01 23:19:19
    【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) ...深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解
  • java面试笔试题汇总

    千次阅读 2012-05-22 22:06:47
    java面试笔试题汇总     第一,谈谈final, finally, finalize的区别。  最常被问到。   第二,Anonymous Inner Class (匿名内部类) 是否可以extends(继承)其它类,是否可以implements(实现)interface...
  • java学习知识集锦2

    千次阅读 2007-09-03 14:00:00
    23.java.swing.JList 该组件允许用户从列表中选择一个或多个对象。单独,模型ListModel表示列表的 内容。使用构建ListModel实例的JList构造方法,可以方便地显示对象的数组或向量。 24.DefaultListModel此类以松散...
  • java面试笔试题汇总 ~很全面

    千次阅读 2012-09-01 19:23:06
    java面试笔试题汇总 java面试笔试题汇总 第一,谈谈final, finally, finalize的区别。  最常被问到。   第二,Anonymous Inner Class (匿名内部类) 是否可以extends(继承)其它类,是否可以...
  • java架构必须掌握的几点技术?关于学习架构,必须会的几点技术1. java反射技术2. xml文件处理3. properties属性文件处理4. 线程安全机制5.... gson json工具类大家对于几框架望而生畏,实际上只要...
  • Java面试笔试题汇总一(最全+详细答案)

    万次阅读 多人点赞 2019-06-26 10:01:53
    本篇文章内容过多,只能分成两部分: 汇总一:... ... 2013年年底的时候,我看到了网上流传的一个叫做《Java面试题大全》的东西,认真的阅读了以后发现里面的很...
  • 菜鸟学Java——Java反射机制(一)

    万次阅读 热门讨论 2013-11-06 20:42:09
    反射机制让Java变得更加的灵活。反射机制在Java的众多特性中是非常重要的一个。下面就让我们一点一点了解它是怎么一回事。   什么是反射 在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于...
  • 全栈必备 Java 基础

    千次阅读 2017-12-15 21:23:40
    从全栈的角度看,Java基础包括哪些呢? 虚拟机,语法,数据结构,接口,泛型,反射,注解,线程,排错.....
  • Java Bean详解

    万次阅读 多人点赞 2018-11-14 21:46:55
    JavaBean 是一种JAVA语言写成的可重用组件。为写成JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 通过提供符合一致性设计模式的公共方法将内部域暴露成员属性,set和get方法获取。众所周知,...
  • Java基础总结

    千次阅读 2016-07-06 17:47:55
    如果instance method也随着instance增加而增加的话,那内存消耗也太了,为了做到共用一段内存,Java 是根据this关键字做到的,比如:instance1.instanceMethod(); instance2.instanceMethod(); 在传递给对象参数...
  • 安卓各组件介绍一、ListView二、ActionBar三、Menu四、ViewPager 、Gallery五、GridView六、ImageView七、ProgressBar八、其他2.GitHub上优秀Android开源项目3. Android开发神器1.Xabber客户端2.oschina客户端3.手机...
  • Java多线程

    万次阅读 多人点赞 2021-06-11 16:28:49
    Java多线程Java多线程线程的创建线程常见方法线程的状态线程的优先级守护线程线程组Java线程池线程池的创建线程池的参数线程池的使用线程不安全问题Java中的锁synchronized同步方法synchronized同步语句块...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 45,025
精华内容 18,010
关键字:

java组件任意变大变小

java 订阅