
码农,搬砖的
改变变量的值 在调试的过程中可以改变非final变量的值。 条件断点 有时候断点会打在循环里,或者希望在某个条件下才触发断点,这个时候条件断点就派上用场了,在idea里对着断点右键。如下,在循环里,希望i的值为8的时候才开始调试。 代码片段&&变量视图 有时候你调试的时候,突然想增加一段代码,又不想重新启动调试,这个功能可以用上。 在调试的过程中同时改变了变量的视图,用toString来显示,可以看到list里的两个值1和2。 丢弃栈帧(Drop Frame) 大家应该都遇到过调试代码的时候想回到上一步,或者回到上一个调用方法的时候吧?IDE 为我们提供了一个Drop Frame的功能,可以让我们丢弃当前的栈帧,如果不知道这个功能,你可能只能选择重新启动debugger开始调试,这样效率有点低。假设有这样的调用关系:methodA -> methodB -> methodC -> methodD如下:代码调试到第51行,想看看从50进入到methodD内部调试,这个时候就可以使用丢弃栈帧了。在底下调用栈中右键methodC -> Drop Frame,就会回到methodB调用的那一个栈帧。 变量断点 变量断点在变量初始化或者变量值改变的时候可以是程序停在变量值改变的那行代码上。 当然,变量断点也是可以设置condition的,如上图。 方法断点&&Force step into 方法上也是可以打断点的,比如有时候我们想进入到jdk内部的方法里,因为jdk的class在编译的时候为了节省空间,去掉了调试信息,用普通的step into可能进入不了方法内部,这个时候可以在相应的方法上打个断点,或者使用Force step into进入到方法体内部。 本文的录屏软件使用的是ScreenToGif.exe,小巧(2.5M)、免安装,功能强大,已放到网盘上了,这个网盘虽然有广告,但是不像百度网盘一样限速。 未完待续。。。
本文链接 步骤 先说总体步骤: 下载源码,并编译到本地maven仓库[上传私服(可选)]; pom文件依赖datax-core和需要的reader和writer 环境变量设置datax.home(或者利用System#setProperty(String))和一些需要替换脚本中的变量:脚本中${}占位符的变量将被系统变量替换。 将datax.tar.gz中解压出来的conf、plugin等文件放到datax.home目录中。 构造参数数组:{"-job", "xxx.json", "-mode", "standalone", "-jobid", "-1"} 调用Engin#main(String[])或者Engine#entry(String[]) 引言 目前官方的使用指南里都是利用python来调用dataX执行任务。而且现有的博客基本上也是利用java来调用python命令Runtime.getRuntime().exec()来执行。个人感觉,dataX未提供java集成开发的方法,应该是定位生产系统,运维需要吧?!我们的业务场景:执行完dataX的job之后,还有一定的业务逻辑,所以希望在java应用里调用dataX执行完job之后,再执行后续逻辑。 DataX分析 笔者简单的看了一下午的DataX的逻辑,完全以使用者的视角分析DataX,必然不能完全了解DataX的整个执行过程。本文仅分析如果能够在java代码里集成DataX进行开发。 集成准备 DataX没有将代码上传到maven服务器上,所以需要自己先pull代码到本地,编译,才能在集成开发的使用通过pom引用。有条件的可以上传到自己的私服上。代码地址 代码依赖 通过pom文件加入datax-core: <dependency> <groupId>com.alibaba.datax</groupId> <artifactId>datax-core</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> 如果需要对应的reader和writer的话,加入到pom文件中,比如需要streamreader和streamwriter: <dependency> <groupId>com.alibaba.datax</groupId> <artifactId>streamreader</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.alibaba.datax</groupId> <artifactId>streamwriter</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> 要依赖datax一定要保证有对应的源码或者编译到本机的maven repository或者在对应的私服上有上传相应的编译版本,不然pom文件是找不到依赖的。 为了集成开发,可能需要一口气引用所有的reader和writer,目前所知,就得一个一个写,如果大家有好办法,麻烦告知! 准备相应的文件 从com.alibaba.datax.core.util.container.CoreConstant中可以看到,datax.home很重要,很多文件的读取都是在datax.home里面获取的。就如我们在安装版的datax中可以看到里面一些目录一样 $ ll total 4 drwxr-xr-x 2 mcbadm mcb 56 Sep 20 18:28 bin drwxr-xr-x 2 mcbadm mcb 65 Sep 20 18:28 conf drwxr-xr-x 2 mcbadm mcb 21 Sep 20 18:28 job drwxr-xr-x 2 mcbadm mcb 4096 Sep 20 18:28 lib drwxr-xr-x 4 mcbadm mcb 32 Sep 20 18:28 plugin drwxr-xr-x 2 mcbadm mcb 22 Sep 20 18:28 script drwxr-xr-x 2 mcbadm mcb 23 Sep 20 18:28 tmp 目前所知的,Engine#entry在解析配置的时候会读取conf目录下的文件,还有对应plugin/reader/xxxreader、plugin/writer/xxxwriter的plugin.json文件: { "name": "streamreader", "class": "com.alibaba.datax.plugin.reader.streamreader.StreamReader", "description": { "useScene": "only for developer test.", "mechanism": "use datax framework to transport data from stream.", "warn": "Never use it in your real job." }, "developer": "alibaba" } 编写代码 编写job代码: { "job": { "content": [ { "reader": { "name": "streamreader", "parameter": { "sliceRecordCount": 1, "column": [ { "type": "long", "value": "10" }, { "type": "string", "value": "hello,你好,世界-DataX" } ] } }, "writer": { "name": "streamwriter", "parameter": { "encoding": "UTF-8", "print": true } } } ], "setting": { "speed": { "channel": 1 } } } } 写个测试类吧: import com.alibaba.datax.core.Engine; public class EngineTest { public static void main(String[] args) { System.setProperty("datax.home", getCurrentClasspath()); String[] datxArgs = {"-job", getCurrentClasspath() + "/job/stream2stream.json", "-mode", "standalone", "-jobid", "-1"}; try { Engine.entry(datxArgs); } catch (Throwable e) { e.printStackTrace(); } } public static String getCurrentClasspath() { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); String currentClasspath = classLoader.getResource("").getPath(); // 当前操作系统 String osName = System.getProperty("os.name"); if (osName.startsWith("Windows")) { // 删除path中最前面的/ currentClasspath = currentClasspath.substring(1); } return currentClasspath; } } datax在解析完配置后,会将core.json,job.json,plugin.json合并在一起: { "common": { "column": { "dateFormat": "yyyy-MM-dd", "datetimeFormat": "yyyy-MM-dd HH:mm:ss", "encoding": "utf-8", "extraFormats": [ "yyyyMMdd" ], "timeFormat": "HH:mm:ss", "timeZone": "GMT+8" } }, "core": { "container": { "job": { "id": -1, "reportInterval": 10000 }, "taskGroup": { "channel": 5 }, "trace": { "enable": "false" } }, "dataXServer": { "address": "http://localhost:7001/api", "reportDataxLog": false, "reportPerfLog": false, "timeout": 10000 }, "statistics": { "collector": { "plugin": { "maxDirtyNumber": 10, "taskClass": "com.alibaba.datax.core.statistics.plugin.task.StdoutPluginCollector" } } }, "transport": { "channel": { "byteCapacity": 67108864, "capacity": 512, "class": "com.alibaba.datax.core.transport.channel.memory.MemoryChannel", "flowControlInterval": 20, "speed": { "byte": -1, "record": -1 } }, "exchanger": { "bufferSize": 32, "class": "com.alibaba.datax.core.plugin.BufferedRecordExchanger" } } }, "entry": { "jvm": "-Xms1G -Xmx1G" }, "job": { "content": [ { "reader": { "name": "streamreader", "parameter": { "column": [ { "type": "long", "value": "10" }, { "type": "string", "value": "hello,你好,世界-DataX" } ], "sliceRecordCount": 1 } }, "writer": { "name": "streamwriter", "parameter": { "encoding": "UTF-8", "print": true } } } ], "setting": { "speed": { "channel": 1 } } }, "plugin": { "reader": { "streamreader": { "class": "com.alibaba.datax.plugin.reader.streamreader.StreamReader", "description": { "mechanism": "use datax framework to transport data from stream.", "useScene": "only for developer test.", "warn": "Never use it in your real job." }, "developer": "alibaba", "name": "streamreader", "path": "D:/workspace/datax-test/target/test-classes/\\plugin\\reader\\streamreader" } }, "writer": { "streamwriter": { "class": "com.alibaba.datax.plugin.writer.streamwriter.StreamWriter", "description": { "mechanism": "use datax framework to transport data to stream.", "useScene": "only for developer test.", "warn": "Never use it in your real job." }, "developer": "alibaba", "name": "streamwriter", "path": "D:/workspace/datax-test/target/test-classes/\\plugin\\writer\\streamwriter" } } } } 说说插件原理 每个reader和writer都有自己的plugin.json文件,里面最重要的就是class配置了,这个类的全路径配置用于classloader将其加载进来并通过反射将其实例化。加载代码可看com.alibaba.datax.core.util.container.LoadUtil所以我们在集成的时候,plugin目录下面不需要有jar包了,只需要放json文件就行,因为我们通过pom文件依赖了对应的reader和writer,说白了,就是classpath下面有对应的reader和writer即可。 结束语 文章有点长,记录了一个下午的研究结果,应该有很多不完善的地方,希望可以和大家多交流。如果觉得有帮助,可以点个赞。
JDK源码精读汇总帖 getInteger() 然后比较少用的方法getInteger,这个方法是用来返回系统属性(String nm)的整数值的,很容易理解。 public static Integer getInteger(String nm) { return getInteger(nm, null); } public static Integer getInteger(String nm, int val) { Integer result = getInteger(nm, null); return (result == null) ? Integer.valueOf(val) : result; } public static Integer getInteger(String nm, Integer val) { String v = null; try { v = System.getProperty(nm); } catch (IllegalArgumentException e) { } catch (NullPointerException e) { } if (v != null) { try { return Integer.decode(v); } catch (NumberFormatException e) { } } return val; } decode(String) 看到上面的方法调用了decode方法,这个方法与valueOf和parseInt的区别就是后者只能分析字符串中是纯数字的,而decode可以分析类似0xff这种。 /** * 8进:010=>分析后为 8 * 10进:10=>分析后为 10 * 16进:#10|0X10|0x10=>分析后是 16 * 而valueOf 只能分析纯数字的String * 注意:这里的nm已经对应的是上面的v了,不是系统属性的key了 */ public static Integer decode(String nm) throws NumberFormatException { int radix = 10; int index = 0; boolean negative = false; Integer result; if (nm.length() == 0) throw new NumberFormatException("Zero length string"); char firstChar = nm.charAt(0); // 符号处理 if (firstChar == '-') { negative = true; index++; } else if (firstChar == '+') index++; // 进制处理 if (nm.startsWith("0x", index) || nm.startsWith("0X", index)) { index += 2; radix = 16; } else if (nm.startsWith("#", index)) { index ++; radix = 16; } // 以0开头且长度不止1位的才当成8进制,否则可能就是一个0 else if (nm.startsWith("0", index) && nm.length() > 1 + index) { index ++; radix = 8; } if (nm.startsWith("-", index) || nm.startsWith("+", index))// 处理完前面的字符之后,还有'-'/'+',则为异常情况 throw new NumberFormatException("Sign character in wrong position"); try { result = Integer.valueOf(nm.substring(index), radix);// 截取标志位(0x等)之后的数字,如果是Integer.MIN_VALUE,将会被传入2147483648,会被valueOf抛出异常,所以需要在catch里再处理 result = negative ? Integer.valueOf(-result.intValue()) : result; } catch (NumberFormatException e) { // If number is Integer.MIN_VALUE, we'll end up here. The next line // handles this case, and causes any genuine format error to be // rethrown. // 补回'-'变成-2147483648 给Integer.valueOf再处理 String constant = negative ? ("-" + nm.substring(index)) : nm.substring(index); result = Integer.valueOf(constant, radix); } return result; } hashCode() hashCode方法,直接返回value。 public int hashCode() { return value; } equals() equals方法比较value的比较。 public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; } highestOneBit(int) highestOneBit这个函数返回的是数字i转换成二进制后最高位1对应的数值(10000....),即2^n对应的数字 /** * 返回最高位的1,1后面全补0对应的数,其实就是返回2^n * 如果一个数是0, 则返回0; *如果是负数, 则返回 -2147483648:【1000,0000,0000,0000,0000,0000,0000,0000】(二进制表示的数); * 如果是正数, 返回的则是跟它最靠近的比它小的2的N次方 * 原理就是不停的右移再或运算,使得i最后变成从最高位1开始后面全是1,如11111...1 */ public static int highestOneBit(int i) { // i右移1位在与i进行或运算,保证最高位两位都是1:11... i |= (i >> 1); // 这步完成之后,最高位4位都是1,1111... i |= (i >> 2); // 这步完成之后,最高位8位都是1,11111111... i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); // 111...-011...到最高位的1,对应1000... return i - (i >>> 1); } lowestOneBit() 然后是lowestOneBit,得到i最右边的1对应的数值,2^n的值。 /** * 这个方法很好理解,只考虑最右边1与它右边的数,最右边1与左边的数无需考虑。假设i最右边是100...0,不管1右边几个零,看看-i,反码为011..1,补码为1000,则i & -i ,1左边的都会变成0,1右边变成了100...0,正好得到最低位的1 */ public static int lowestOneBit(int i) { // HD, Section 2-1 return i & -i; } numberOfLeadingZeros() 然后讨论numberOfLeadingZeros,这个方法的作用是返回参数i转成二进制后左边一共有多少个0。 public static int numberOfLeadingZeros(int i) { // 如果i是零,直接返回有32,代表二进制里32位都是零,java里int是4字节 if (i == 0) return 32; // 后面的逻辑完全是二分法的思维,判断i的第一个最左边的1在哪个位置,在判断过程中顺便累加了从左边开始0的个数,思路:先看你最左边1在高16位还是低16位,然后再判断你在16位中的高8位还是低8位,以此类推 int n = 1;// 0个数的计数器 // i右移16位之后有两种情况: // 情况1:i==0;代表左边的1位于低16位,这样的话n+=16代表至少有16个零了。然后把i左移16位,这样情况1和情况2的后续处理方法就一致了。 // 情况2:i!=0;代表左边的1位于高16位,后续在高16位中继续判断 if (i >>> 16 == 0) { n += 16; i <<= 16; } // 判断是否在低8位,在的话,0的个数加8,然后左移8位,后续与在高8位的情况同样处理 if (i >>> 24 == 0) { n += 8; i <<= 8; } if (i >>> 28 == 0) { n += 4; i <<= 4; } if (i >>> 30 == 0) { n += 2; i <<= 2; } // 到这一步,所有情况都被当成是否在最高2位的情况处理了,这个时候n已经代表了0的个数 // 如果i>>>31为0,则在低1位,最左边还有1个0,正好是n初始化为1的理由 // 如果i>>>31为1,则在高1位,最左边没有0了,初始化n为1会导致多算1个0,n-1正好是0的个数 n -= i >>> 31; return n; } numberOfTrailingZeros() numberOfTrailingZeros方法也是二分法的思想,返回最右边0的个数。 public static int numberOfTrailingZeros(int i) { // HD, Figure 5-14 int y; if (i == 0) return 32; int n = 31; y = i <<16; if (y != 0) { n = n -16; i = y; } y = i << 8; if (y != 0) { n = n - 8; i = y; } y = i << 4; if (y != 0) { n = n - 4; i = y; } y = i << 2; if (y != 0) { n = n - 2; i = y; } return n - ((i << 1) >>> 31); } 转载请注明出处 java.lang.Integer源码精读(二) 地址:https://www.jianshu.com/p/dc50508937c6
前言 不建议写这么奇葩的代码!!! 这就有点像考试喜欢出的试题,有一堆overload和override的代码,选择题选择调用的是哪个。 不建议写这种让人看着费劲的代码。 问题引出 言归正传,如果有一个这样的配置类,@Bean注解了相同name = "cupcake"的bean: public class BeanOverrideConfig { @Bean(name = "cupcake") public Cupcake cupcake1() { Cupcake cupcake = new Cupcake(); cupcake.setName("Cupcake1"); return cupcake; } @Bean(name = "cupcake") public Cupcake cupcake2() { Cupcake cupcake = new Cupcake(); cupcake.setName("Cupcake2"); return cupcake; } } 下面这个测试类能通过测试吗?注意最后一行代码Assert.assertEquals("Cupcake1", cupcake.getName());: public class BeanOverrideTest { private ApplicationContext ctx = null; @Before public void setUp() { ctx = new AnnotationConfigApplicationContext(BeanOverrideConfig.class); } @Test public void testGetBean() { Cupcake cupcake = ctx.getBean(Cupcake.class); Assert.assertNotNull(cupcake); Assert.assertEquals("Cupcake1", cupcake.getName()); } } 结果 测试通过! 原因 Spring对configuration class的加载 加载BeanDefinition的过程中有一步: org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(BeanMethod) 在这个方法中会判断现在的beanName在现有的beanDefinitionMap中是否已存在,然后决定是否覆盖。是否覆盖的策略如下org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.isOverriddenByExistingDefinition(BeanMethod, String): // Is the existing bean definition one that was created from a configuration class? // -> allow the current bean method to override, since both are at second-pass level. // However, if the bean method is an overloaded case on the same configuration class, // preserve the existing bean definition. if (existingBeanDef instanceof ConfigurationClassBeanDefinition) { ConfigurationClassBeanDefinition ccbd = (ConfigurationClassBeanDefinition) existingBeanDef; return ccbd.getMetadata().getClassName().equals( beanMethod.getConfigurationClass().getMetadata().getClassName()); } 源码里说的很清楚了,如果来自不同层级的bean method,允许覆盖,如果是the same configuration class,preserve the existing bean definition(同一configuration class的overload,保留先前的)。 回到我们的测试类,即保留方法public Cupcake cupcake1()对应的bean definition,最后测试的时候getName就返回Cupcake1。 深入 如果是下面这种配置和测试: public class BeanOverrideConfig1 { @Bean(name = "cupcake") public Cupcake cupcake1() { Cupcake cupcake = new Cupcake(); cupcake.setName("Cupcake1"); return cupcake; } } public class BeanOverrideConfig2 { @Bean(name = "cupcake") public Cupcake cupcake2() { Cupcake cupcake = new Cupcake(); cupcake.setName("Cupcake2"); return cupcake; } } public class BeanOverrideTest { private ApplicationContext ctx = null; @Before public void setUp() { ctx = new AnnotationConfigApplicationContext(BeanOverrideConfig1.class, BeanOverrideConfig2.class); } @Test public void testOverride() { Cupcake cupcake = ctx.getBean(Cupcake.class); Assert.assertNotNull(cupcake); Assert.assertEquals("Cupcake2", cupcake.getName()); } } 很显然测试能通过,即会覆盖。 @Import呢? 如果是这种情况呢? @Import(BeanOverrideConfig2.class) public class BeanOverrideConfig1 { @Bean(name = "cupcake") public Cupcake cupcake1() { Cupcake cupcake = new Cupcake(); cupcake.setName("Cupcake1"); return cupcake; } } public class BeanOverrideConfig2 { @Bean(name = "cupcake") public Cupcake cupcake2() { Cupcake cupcake = new Cupcake(); cupcake.setName("Cupcake2"); return cupcake; } } public class BeanOverrideTest { private ApplicationContext ctx = null; @Before public void setUp() { ctx = new AnnotationConfigApplicationContext(BeanOverrideConfig1.class); } @Test public void testOverride() { Cupcake cupcake = ctx.getBean(Cupcake.class); Assert.assertNotNull(cupcake); Assert.assertEquals("Cupcake1", cupcake.getName()); } } 测试通过,这种@Import的情况也没认为是同一配置类,不会覆盖。
前言 本文不讲解源码,仅记录加载过程中的一部分。看本文需要先知道spring对BeanDefinition的处理,对bean的实例化。 单元测试和配置 public class Config { } public class ContextLoadTest { private ApplicationContext ctx = null; @Before public void setUp() { ctx = new AnnotationConfigApplicationContext(Config.class); } @Test public void testContextLoads() { } } BeanDefinition的加载堆栈记载 org.eclipse.jdt.internal.junit.runner.RemoteTestRunner at localhost:58478 Thread [main] (Suspended (breakpoint at line 846 in DefaultListableBeanFactory)) owns: ConcurrentHashMap<K,V> (id=111) owns: Object (id=96) DefaultListableBeanFactory.registerBeanDefinition(String, BeanDefinition) line: 846 ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(BeanMethod) line: 266 ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClass, ConfigurationClassBeanDefinitionReader$TrackedConditionEvaluator) line: 140 ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(Set<ConfigurationClass>) line: 116 ConfigurationClassPostProcessor.processConfigBeanDefinitions(BeanDefinitionRegistry) line: 320 ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(BeanDefinitionRegistry) line: 228 PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(Collection<BeanDefinitionRegistryPostProcessor>, BeanDefinitionRegistry) line: 272 PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory, List<BeanFactoryPostProcessor>) line: 92 AnnotationConfigApplicationContext(AbstractApplicationContext).invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory) line: 687 AnnotationConfigApplicationContext(AbstractApplicationContext).refresh() line: 525 AnnotationConfigApplicationContext.<init>(Class<?>...) line: 84 BeanOverrideTest.setUp() line: 16 NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method] NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62 DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43 Method.invoke(Object, Object...) line: 498 FrameworkMethod$1.runReflectiveCall() line: 50 FrameworkMethod$1(ReflectiveCallable).run() line: 12 FrameworkMethod.invokeExplosively(Object, Object...) line: 47 RunBefores.evaluate() line: 24 BlockJUnit4ClassRunner(ParentRunner<T>).runLeaf(Statement, Description, RunNotifier) line: 325 BlockJUnit4ClassRunner.runChild(FrameworkMethod, RunNotifier) line: 78 BlockJUnit4ClassRunner.runChild(Object, RunNotifier) line: 57 ParentRunner$3.run() line: 290 ParentRunner$1.schedule(Runnable) line: 71 BlockJUnit4ClassRunner(ParentRunner<T>).runChildren(RunNotifier) line: 288 ParentRunner<T>.access$000(ParentRunner, RunNotifier) line: 58 ParentRunner$2.evaluate() line: 268 BlockJUnit4ClassRunner(ParentRunner<T>).run(RunNotifier) line: 363 JUnit4TestReference.run(TestExecution) line: 86 TestExecution.run(ITestReference[]) line: 38 RemoteTestRunner.runTests(String[], String, TestExecution) line: 538 RemoteTestRunner.runTests(TestExecution) line: 760 RemoteTestRunner.run() line: 460 RemoteTestRunner.main(String[]) line: 206 Thread [ReaderThread] (Running) 值得留意的是: ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(Set<ConfigurationClass>) line: 116 这里就是把BeanDefinition从configuration class加载进来的过程,类似解析xml。 BeanDefinition的注册: org.springframework.beans.factory.support.DefaultListableBeanFactory.registerBeanDefinition(String, BeanDefinition)
引言 了解Jedis的童鞋可能清楚,Jedis中JedisCluster是不支持pipeline操作的,如果使用了redis集群,在spring-boot-starter-data-redis中又正好用到的pipeline,那么会接收到Pipeline is currently not supported for JedisClusterConnection.这样的报错。错误来自于org.springframework.data.redis.connection.jedis.JedisClusterConnection: /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConnection#openPipeline() */ @Override public void openPipeline() { throw new UnsupportedOperationException("Pipeline is currently not supported for JedisClusterConnection."); } org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration会帮我们自动配置,无论你redis使用的是standalone、sentinel、cluster配置。这个源码很容易理解,读者可自行阅读,不理解的可以一起讨论。 Lettuce中的pipeline spring boot 2.0开始,配置spring-boot-starter-data-redis将不依赖Jedis,而是依赖Lettuce,在Lettuce中,redis cluster使用pipeline不会有问题。 知识储备 再往下看可能需要读者具备如下的能力: redis cluster hash slot JedisCluster & Jedis的关系 pipeline和*mset等命令的区别 哈希槽(hash slot) redis cluster一共有16384个桶(hash slot),用来装数据,建立集群的时候每个集群节点会负责一些slot的数据存储,比如我负责0-1000,你负责1001-2000,他负责2001-3000…… 数据存储时,每个key在存入redis cluster前,会利用CRC16计算出一个值,这个值就是对应redis cluster的hash slot,就知道这个key会被放到哪个服务器上了。 参考文档: Redis 集群教程Redis 集群规范 JedisCluster & Jedis的关系 JedisCluster本质上是使用Jedis来和redis集群进行打交道的,具体过程是: 获取该key的slot值:JedisClusterCRC16.getSlot(key) 从JedisClusterConnectionHandler实例中获取到该slot对应的Jedis实例:Jedis connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key)); 利用connection操作。 pipeline和*mset等命令的区别 redis提供了mset,hmset之类的命令,或者说集合操作可以使用sadd key 1 2 3 4 5 6 ..... 10000000000这种一口气传一堆数据的命令。 有时候你甚至会发现*mset这种一口气操作一堆数据的速度更快。那么这种使用场景会有什么弊端呢?答案是:阻塞。 操作这一堆数据需要多久,就会阻塞多久。 Redis Cluster下pipeline使用的思考 由于JedisCluster中的所有操作本质上是使用Jedis,而Jedis是支持pipeline操作的,所有,要在redis cluster中使用pipeline是有可能的,只要你操作同一个键即可,准确的说,应该是你操作的键位于同一台服务器,更直白的,你操作的键是同一个Jedis实例。ok,如果你已经晕了,那你需要回看一下“知识储备”。 说说笔者的使用场景吧,我们是把csv文件的一批数据读到内存中,同一批数据是存储到同一个key中的,最后的操作会类似于: set key member1 set key member2 set key member3 ... set key member100000 操作的是同一个key,可以利用JedisCluster获取到该key的Jedis实例,然后利用pipeline操作。 让spring-data-redis也支持pipeline的思路 提供一下代码思路。 RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); RedisConnection redisConnection = factory.getConnection(); JedisClusterConnection jedisClusterConnection = (JedisClusterConnection) redisConnection; // 获取到原始到JedisCluster连接 JedisCluster jedisCluster = jedisClusterConnection.getNativeConnection(); // 通过key获取到具体的Jedis实例 // 计算hash slot,根据特定的slot可以获取到特定的Jedis实例 int slot = JedisClusterCRC16.getSlot(key); /** * 不建议这么使用,官方在2.10版本已经修复<a href="https://github.com/xetorthio/jedis/pull/1532">此问题</a><br> * 2.10版本中,官方会直接提供JedisCluster#getConnectionFromSlot */ Field field = ReflectionUtils.findField(BinaryJedisCluster.class, null, JedisClusterConnectionHandler.class); field.setAccessible(true); JedisSlotBasedConnectionHandler jedisClusterConnectionHandler = (JedisSlotBasedConnectionHandler) field.get(jedisCluster); Jedis jedis = jedisClusterConnectionHandler.getConnectionFromSlot(slot); // 接下来就是pipeline操作了 Pipeline pipeline = jedis.pipelined(); ... pipeline.syncAndReturnAll(); 以上代码完全可以模仿spring-data-redis中RedisTemplate#executePipelined方法写成一个通用的方法,供使用者调用。
JDK源码精读汇总帖 类声明 public final class Integer extends Number implements Comparable<Integer> {} public abstract class Number implements java.io.Serializable {} 抽象类 Number 是 BigDecimal、BigInteger、Byte、Double、Float、Integer、Long 和 Short 类的超类。 Number 的子类必须提供将表示的数值转换为 byte、double、float、int、long 和 short 的方法。 Integer中对应的方法就是类型转换,将int转换成byte、double、float、long 和 short 类型。 compareTo 实现了Comparable,看看对应的方法,很好理解 public int compareTo(Integer anotherInteger) { return compare(this.value, anotherInteger.value); } /** * jdk1.7之后单独抽取出来的static方法,可以作为工具方法用于比较两个整数 */ public static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); } toString(int) 看看非常常用的toString方法 public static String toString(int i) { // 如果是Integer的最小值,直接返回字符串"-2147483648" if (i == Integer.MIN_VALUE) return "-2147483648"; // 计算形参i的位数,负数的话,size要比数字本身多1,用来存储负号(-) int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i); // 构造一个用于存储数字的字符数组 char[] buf = new char[size]; // 填充字符数组 getChars(i, size, buf); return new String(buf, true); } 先看看stringSize方法,这个方法返回的是形参i的位数 final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE }; // 要求形参x为正数 static int stringSize(int x) { // 一个个和sizeTable比较,到第i个数代表x由(i+1)个数字组成 for (int i=0; ; i++) if (x <= sizeTable[i]) return i+1; } 再看看getChars方法,这个方法就是将组成形成i的每一个数字,填充到buf数组里。在看getChars这个方法之前可以先思考一下,怎么样获取形参i的每个位置上的数字呢?其实就是循环:①每次除以10求余(%),得到的就是个位上的数字;②然后再将形参i除以10,整数相除,尾数(即前面的余数)就会被丢弃;③重复①②直到形参i变成0。如12,第一次循环:除以10求余得到2,就是个位数上的数字是2,然后除以10,得到1,个位数上的2就被丢弃了,第二次循环:除10求余得到1,然后除以10得到0,于是分别得到了2和1,循环结束了。用代码来看的话就是: while (true) { r = i % 10; i /= 10; // 伪代码↓ // 第n次循环,将r加入到buf的倒数第n位上 if (i == 0) break; 以上只考虑了正数的情况,对于负数的情况,只要再循环结束后,在buf的第0个位置加上一个'-'即可,完整代码如下: static void myGetChars(int i, int index, char[] buf) { int charPos = index;// 用来记录buf每次可插入的尾部 char sign = 0;// 是否为负数的标记 if (i < 0) { sign = '-'; i = -i;// 当成正数处理 } while (true) { int r = i % 10; i /= 10; buf [--charPos] = digits [r];// 将数字r转成字符'r',然后添加到buf的可插入尾部 if (i == 0) break; } if (sign != 0) { buf [--charPos] = sign; } } 看样子好像实现了getChars的功能,但是jdk里面是不是这么做的呢?答案是:no!看看源码: static void getChars(int i, int index, char[] buf) { int q, r;// r为余数,就是每次被插入到buf的数 int charPos = index;// buf可插入尾部 char sign = 0; if (i < 0) { sign = '-'; i = -i; } // i >= 65536时 // 每次迭代向buf插入2个数字,即i的最后两位,目的应该是加快迭代速度 while (i >= 65536) { q = i / 100; // really: r = i - (q * 100); // 本质就是r = i - (q * 100) // q << 6 = 2^6 = 64 // q << 5 = 2^5 = 32 // q << 2 = 2^2 = 4 // i - q * 100 得到的r就是i的末尾2位数,本质是i%100,应该是乘法运算速度大于除法,大于求余,但是减法的速度应该也不算快 r = i - ((q << 6) + (q << 5) + (q << 2)); i = q; // 插入r的个位数 buf [--charPos] = DigitOnes[r]; // 插入r的十位数 buf [--charPos] = DigitTens[r]; } // Fall thru to fast mode for smaller numbers // assert(i <= 65536, i); for (;;) { // 这里本质就是i/10, q = (i * 52429) >>> (16+3); r = i - ((q << 3) + (q << 1)); // 本质就是 r = i-(q*10) buf [--charPos] = digits [r];// 将数字r转成字符'r',然后添加到buf的可插入尾部 i = q; if (i == 0) break; } if (sign != 0) {// 如果是负数,再插入'-' buf [--charPos] = sign; } } 这里有几个比较有意思的地方: 首先是分段来获取字符,分成>=65535和<65535两段,大于等于65535的部分,每次迭代获取两个字符,这里有个疑问的地方是,为什么不分成>=10和<10两部分呢,难道是后面这一小段代码的执行速度比较快? 其次是<65535这部分,对于q = (i * 52429) >>> (16+3);,这个代码等价于q = i / 10;,应该是ALU执行乘法和移位运算的速度快过除法的运算速度。但是为什么是52429和2^19 (2的19次方,无符号右移19位相当于除以2的19次方),这是出于精度同时不会溢出这两方面考虑的。首先说精度方面,(double)52429/524288=0.100000381469,这个精度完全可以保证求出i的十分之一,比如选了一个精度不够的,求出值为0.103,那么如果i是999,999*0.103=102,并不是999的十分之一99;另外一方面,65536=2^16,52429<65536,所以i * 52429 < 2^32,不会溢出,至于i * 52429 会导致结果变成负数的问题,这只是中间结果,无符号右移19位之后,高位全部补0,结果q还是正数。 以上可以看出,jdk还是蛮严谨的,为了效率也是拼了。 但实际测试的结果感觉运算效率并没有很大提高,可能是以前的ALU没有现在的先进。 toString(int, int) 接下来看看toString(int i, int radix),这个方法主要是将形参i转成radix进制的数 public static String toString(int i, int radix) { // 如果小于2进制或大于36进制,当成10进制处理 if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) radix = 10; /* Use the faster version */ // 10进制数,直接使用更快的版本 if (radix == 10) { return toString(i); } // Integer最多占用33位,数字32位,符号1位 char buf[] = new char[33]; boolean negative = (i < 0);// 判断是否为负数,后面i被当成负数来统一处理 int charPos = 32;// buf最末尾的索引,从32开始 // 如果i是正数,取反,当成负数处理 if (!negative) { i = -i; } // i%radix的余数的绝对值放到buf当前尾部(charPos)位置 while (i <= -radix) { buf[charPos--] = digits[-(i % radix)]; i = i / radix; } buf[charPos] = digits[-i]; // 如果i是负数,加上'-' if (negative) { buf[--charPos] = '-'; } // 返回字符串,从buf的charPos位置开始截取,长度33-charPos,正好是最后一个位置,索引下标为32 return new String(buf, charPos, (33 - charPos)); } toString() 无参的toString就没什么难度了,就是把Integer封装的value转成字符串 public String toString() { return toString(value); } toHexString(int) 接下来看看几个无符号进制的转换 public static String toHexString(int i) { return toUnsignedString(i, 4); } public static String toOctalString(int i) { return toUnsignedString(i, 3); } public static String toBinaryString(int i) { return toUnsignedString(i, 1); } /** * 本质上就是理解这个方法 * 因为是无符号,所以所有数字其实都可以想象成二进制的情况,如17则是0000 0000 0000 0000 0000 0001 0001 0001,-17则是1111 1111 1111 1111 1111 1111 1110 1111 */ private static String toUnsignedString(int i, int shift) { // 无符号数,只需存32位即可 char[] buf = new char[32]; // 可插入的尾部位置 int charPos = 32; // 进制 int radix = 1 << shift; // 类似掩码的作用,二进制是对应的是0001,八进制对应0111,十六进制对应1111 int mask = radix - 1; do { // 关键理解i & mask,整个过程就像手工转换进制一样,先得到二进制,然后每几位一组转换,如0001 0001转成8进制,每3位一组:00 010 001,然后得到0 2 1,所以17的8进制表示就是21,i & mask的过程就像分组,每次迭代将mask对应的位数得到,再利用digits转换成对应的字符 buf[--charPos] = digits[i & mask]; i >>>= shift; } while (i != 0); return new String(buf, charPos, (32 - charPos)); } 演示一下i & mask的过程: i=17,mask=7,则相当于0001 0001 & 0111 得到001,digit[1] =1,第二次迭代i=000 010,000 010 & 0111得到010,digit[2]=2,迭代结束,buf=21。 i=17,mask-15,则相当于0001 0001 & 1111 得到0001,digit[1] =1,第二次迭代i=0001,0001 & 1111得到0001,digit[1]=1,迭代结束,buf=11。 parseInt(String, int) 接着看parseInt方法,进制转换的公式:a * radix^0 + b * radix^1 + c * radix^2 + ... + xx * radix^(n-1) /** * 整个过程的关键点都在判断下一次迭代会不会导致溢出 * */ public static int parseInt(String s, int radix) throws NumberFormatException { /* * WARNING: This method may be invoked early during VM initialization * before IntegerCache is initialized. Care must be taken to not use * the valueOf method. */ // 下面都是判断s不能为空,进制必须在[2, 36],可以看到jdk的源码处理也有问题,之前进制转换的,不在[2, 36]之间就当成10进制处理,现在是抛异常,规则不一样 if (s == null) { throw new NumberFormatException("null"); } if (radix < Character.MIN_RADIX) { throw new NumberFormatException("radix " + radix + " less than Character.MIN_RADIX"); } if (radix > Character.MAX_RADIX) { throw new NumberFormatException("radix " + radix + " greater than Character.MAX_RADIX"); } // 当前的转换结果 int result = 0; // 后面的处理都基于负数处理,一方面是统一规则,另外Integer.MIN_VALUE的绝对值比较大 boolean negative = false; int i = 0, len = s.length(); // 后面两个参数限制溢出的作用 int limit = -Integer.MAX_VALUE; // 界定溢出,具体看下面的注释 int multmin; // 通过Character#digit转换过来的数值 int digit; if (len > 0) { // 处理第一个字符 char firstChar = s.charAt(0); if (firstChar < '0') { // Possible leading "+" or "-" if (firstChar == '-') { negative = true; // 如果是负数,溢出标志变成Integer.MIN_VALUE limit = Integer.MIN_VALUE; } else if (firstChar != '+') throw NumberFormatException.forInputString(s); if (len == 1) // Cannot have lone "+" or "-" throw NumberFormatException.forInputString(s); i++; } // 界定是否溢出的标志,假设10进制最大是211,则multmin = 211 / 10 = 21,假设现在result为30,那么下一次迭代result *= radix肯定会大于211,溢出了。 multmin = limit / radix; while (i < len) { // Accumulating negatively avoids surprises near MAX_VALUE // 转换函数,具体在Character类了解 digit = Character.digit(s.charAt(i++),radix); if (digit < 0) { throw NumberFormatException.forInputString(s); } // 因为是当成负数(limit是负数)处理,相当于正数的result > multmin if (result < multmin) { throw NumberFormatException.forInputString(s); } result *= radix;// 迭代,就是套公式a * radix^(n-1)的过程 // 同样是判断溢出的过程,继续上面的分析,result现在是210,如果digit是2,则转换后的result = 212,也是溢出了。 if (result < limit + digit) { throw NumberFormatException.forInputString(s); } result -= digit;// 公式里相加的过程,负数的话就是减 } } else { throw NumberFormatException.forInputString(s); } return negative ? result : -result; } // 这个就是调用上面的方法 public static int parseInt(String s) throws NumberFormatException { return parseInt(s,10); } valueOf() 接着看看valueOf方法,在看valueOf方法之前,需要先看看IntegerCache,顾名思义,这个是整数的缓存,默认情况下,IntegetCache会缓存[-128, 127]的所有实例,所以正常情况下,在这两个值范围内的对象都是相等的(==返回true),因为它们引用的是同一块内存上的对象,有点单例的意思,当然可以通过启动参数-XX:AutoBoxCacheMax=size来修改缓存的内容(size必须大于127,否则当成127)来让jvm缓存[-size-1, size]的对象。看看代码: private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127);// 如果配置的值比127小,缓存上界还是127 // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } high = h; cache = new Integer[(high - low) + 1]; int j = low; // 对象一个个缓存起来,用cache数组保存 for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); } private IntegerCache() {} } 具体看看valueOf方法 // 调用的parseInt方法以及分析过 public static Integer valueOf(String s, int radix) throws NumberFormatException { return Integer.valueOf(parseInt(s,radix)); } public static Integer valueOf(String s) throws NumberFormatException { return Integer.valueOf(parseInt(s, 10)); } public static Integer valueOf(int i) { assert IntegerCache.high >= 127;// 防止IntegetCache函数上界不是127 // 在[IntegetCache.low, Integer.high]直接的直接从缓存中返回 if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i);// 否则新建一个实例 } 转载请注明出处 java.lang.Integer源码精读(一) 地址:https://www.jianshu.com/p/02c1d9092347
前言 大家可能都会阅读JDK源码,目前很多大神也分享了相应的博客,让后来者可谓是站在巨人的肩膀上。 有一点点问题,绝大多数的分享都是比较粗略的,其中很多复杂的方法没有记录设计思路,处理步骤等等。决定有时间的时候认真读一读,希望能多了解一下JDK的源码,达到精度的水平。 导航 java.lang.Integer源码精读(一) 转载请注明出处 JDK源码精读-汇总帖 地址:https://www.jianshu.com/p/14092cfab6d9
idea和sts里面都有对spring boot中properties文件的提示功能,而且可以通过点击属性可以进入到*Properties.java这类文件中。 属性提示 通过点击属性进入到属性配置类 需要说明的是,idea社区版好像不具备这个功能,还有旗舰版也需要加载了spring support这个plugin才能支持spring相关的功能,即这个spring boot properties文件的提示功能。 问题 刚从sts转到idea,最近打开一个项目发现properties文件没有提示了,一顿查发现也没有人分享,也没有答案,好不容易找到解决方法,记录一下,也分享给可能会碰到同样问题的新手。 定位 发现统一项目(或者叫module),在不同的idea project里打开的properites是不同的,如下图: 没有提示的properties文件 有提示的properies文件 通过上面两个图,应该很明显看出来了,文件的图标好像不太一样,上面是普通的properies文件,下面的是带spring图标的文件。 解决办法 进入模块设置,选中模块->右键->Open Modules Settings(快捷键F4),如图: 添加spring支持 添加spring支持 总结 就一句话,给相应的Module添加spring support即可。
今天翻《Java 8 编程参考官方教程》的时候看到一段代码: public class InstanceMethWithObjectRefDemo { private static <T> int counter(T[] vals, MyFunc<T> f, T v2) { int count = 0; for (T v1 : vals) { if (f.func(v1, v2)) count++; } return count; } public static void main(String[] args) { int count; HighTemp[] weekDayHighs = { new HighTemp(89), new HighTemp(82),new HighTemp(90), new HighTemp(89), new HighTemp(89), new HighTemp(91),new HighTemp(84), new HighTemp(83) }; count = counter(weekDayHighs, HighTemp::sameTemp, new HighTemp(89)); System.out.println(count + " days had a high of 89"); count = counter(weekDayHighs, HighTemp::lessThanTemp, new HighTemp(89)); System.out.println(count + " days had a high less than of 89"); } } @FunctionalInterface interface MyFunc<T> { boolean func(T v1, T v2); } class HighTemp { private int hTemp; public HighTemp(int hTemp) { this.hTemp = hTemp; } boolean sameTemp(HighTemp ht2) { return this.hTemp == ht2.hTemp; } boolean lessThanTemp(HighTemp ht2) { return this.hTemp < ht2.hTemp; } } 这里有一段: count = counter(weekDayHighs, HighTemp::sameTemp, new HighTemp(89)); 看到的时候有点疑问,HighTemp#sameTemp怎么就和MyFunc函数式接口兼容了呢?MyFunc需要两个T参数和一个boolean返回值。而HighTemp#sameTemp是一个参数一个返回值。愣了一会儿,想起隐式参数这回事,恍然大悟。 一般情况下方法都会有super和this,所以相当于sameTemp方法的声明是: boolean sameTemp(HighTemp this, HighTemp ht2); 这样就和MyFunc兼容了。 接口中唯一抽象方法的命名并不重要,因为函数式接口就是对某一行为进行抽象,主要目的就是支持Lambda表达式。 总结 有些知识点看的时候理解,但不到实际应用,还真容易忘,但是看过的东西总比没有学习过要好,因为在你需要它的时候,你能想起来,帮助你解决问题。
浅说动态代理 关于java的代理模式,此处不过多讲解。所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。动态代理指被代理者委托代理者完成相应的功能,是拦截器的一种实现方式,其用于拦截类或接口,内部可通过判断实现对某个方法的拦截。 日常使用中可能经常需要在方法调用时进行拦截,如调用前记录一下调用开始时间,调用结束后记录结束时间,就可以很方便的计算出调用方法的业务逻辑处理耗时。 动态代理使用 简单的看下最简单的使用: 编写一个接口: package my.java.reflect.test; public interface Animal { void sayHello(); } 委托类 public class Dog implements Animal { public void sayHello() { System.out.println("wang wang!"); } } 拦截器 package my.java.reflect.test; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class MyInvocationHandler implements InvocationHandler { private Object target; public Object bind(Object realObj) { this.target = realObj; Class<?>[] interfaces = target.getClass().getInterfaces(); ClassLoader classLoader = this.getClass().getClassLoader(); return Proxy.newProxyInstance(classLoader, interfaces, this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("proxy method"); return method.invoke(target, args); } } 测试类 package my.java.reflect.test; import org.junit.Test; public class ProxyTest { @Test public void testNewProxyInstance() { Dog dog = new Dog(); Animal proxy = (Animal) new MyInvocationHandler().bind(dog); proxy.sayHello(); } } 输出 proxy method wang wang! 动态代理原理总结 之所以将原理先总结了,因为希望把原理先用最简洁的语言说清楚,再来深入分析,否则在深入分析阶段粘贴过多的源码可能会导致阅读兴趣下降。 通过Proxy#newProxyInstance方法得到代理对象实例; 这个代理对象有着和委托类一模一样的方法; 当调用代理对象实例的方法时,这个实例会调用你实现InvocationHandler里的invoke方法。 而这里,最复杂的显然是得到代理对象实例了,怎么得到的呢?来,看看源码! 动态代理原理 要了解java动态代理的原理只要从Proxy#newProxyInstance入手即可。 先看newProxyInstance方法,关注有中文注释的地方即可 @CallerSensitive public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { if (h == null) {// 没有实现InvocationHandler,直接失败 throw new NullPointerException(); } final Class<?>[] intfs = interfaces.clone(); final SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkProxyAccess(Reflection.getCallerClass(), loader, intfs); } /* * Look up or generate the designated proxy class. * 查找或者生成代理类的Class对象 */ Class<?> cl = getProxyClass0(loader, intfs); /* * Invoke its constructor with the designated invocation handler. */ try { // 拿到代理对象的构造方法 final Constructor<?> cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) { // create proxy instance with doPrivilege as the proxy class may // implement non-public interfaces that requires a special permission return AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { return newInstance(cons, ih); } }); } else { // 构造出代理对象实例 return newInstance(cons, ih); } } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); } } // 利用反射调用构造方法,产生代理实例,没什么好说的 private static Object newInstance(Constructor<?> cons, InvocationHandler h) { try { return cons.newInstance(new Object[] {h} ); } catch (IllegalAccessException | InstantiationException e) { throw new InternalError(e.toString()); } catch (InvocationTargetException e) { Throwable t = e.getCause(); if (t instanceof RuntimeException) { throw (RuntimeException) t; } else { throw new InternalError(t.toString()); } } } 以上这段代码的核心在 /* * Look up or generate the designated proxy class. * 查找或者生成代理类的Class对象 */ Class<?> cl = getProxyClass0(loader, intfs); 查看getProxyClass0方法 private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) { if (interfaces.length > 65535) {// 你的类如果实现了超过65535个接口,这个方法疯了。 throw new IllegalArgumentException("interface limit exceeded"); } // If the proxy class defined by the given loader implementing // the given interfaces exists, this will simply return the cached copy; // otherwise, it will create the proxy class via the ProxyClassFactory // 如果委托类的接口已经存在于缓存中,则返回,否则利用ProxyClassFactory产生一个新的代理类的Class对象 return proxyClassCache.get(loader, interfaces); } 这段话的核心很显然就是proxyClassCache.get(loader, interfaces),其中proxyClassCache是一个缓存: /** * a cache of proxy classes */ private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()); 继续看proxyClassCache.get(loader, interfaces),对应的是java.lang.reflect.WeakCache<K, P, V>#get,一大段代码就为了返回一个代理类的Class对象,关注中文注释处即可: public V get(K key, P parameter) { Objects.requireNonNull(parameter); expungeStaleEntries(); Object cacheKey = CacheKey.valueOf(key, refQueue); // lazily install the 2nd level valuesMap for the particular cacheKey ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey); if (valuesMap == null) { ConcurrentMap<Object, Supplier<V>> oldValuesMap = map.putIfAbsent(cacheKey, valuesMap = new ConcurrentHashMap<>()); if (oldValuesMap != null) { valuesMap = oldValuesMap; } } // create subKey and retrieve the possible Supplier<V> stored by that // subKey from valuesMap Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter)); Supplier<V> supplier = valuesMap.get(subKey); Factory factory = null; while (true) { if (supplier != null) { // supplier might be a Factory or a CacheValue<V> instance // 关键点在这里 V value = supplier.get(); if (value != null) { return value; } } // else no supplier in cache // or a supplier that returned null (could be a cleared CacheValue // or a Factory that wasn't successful in installing the CacheValue) // lazily construct a Factory if (factory == null) { factory = new Factory(key, parameter, subKey, valuesMap); } if (supplier == null) { supplier = valuesMap.putIfAbsent(subKey, factory); if (supplier == null) { // successfully installed Factory supplier = factory; } // else retry with winning supplier } else { if (valuesMap.replace(subKey, supplier, factory)) { // successfully replaced // cleared CacheEntry / unsuccessful Factory // with our Factory supplier = factory; } else { // retry with current supplier supplier = valuesMap.get(subKey); } } } } 这段代码的核心就在V value = supplier.get();,通过这段代码,代理类的Class的对象就出来了。 查看subKeyFactory.apply(key, parameter),这段代码还是在WeakCache里: @Override public synchronized V get() { // serialize access // re-check Supplier<V> supplier = valuesMap.get(subKey); if (supplier != this) { // something changed while we were waiting: // might be that we were replaced by a CacheValue // or were removed because of failure -> // return null to signal WeakCache.get() to retry // the loop return null; } // else still us (supplier == this) // create new value V value = null; try { // 核心点在这里 value = Objects.requireNonNull(valueFactory.apply(key, parameter)); } finally { if (value == null) { // remove us on failure valuesMap.remove(subKey, this); } } // the only path to reach here is with non-null value assert value != null; // wrap value with CacheValue (WeakReference) CacheValue<V> cacheValue = new CacheValue<>(value); // try replacing us with CacheValue (this should always succeed) if (valuesMap.replace(subKey, this, cacheValue)) { // put also in reverseMap reverseMap.put(cacheValue, Boolean.TRUE); } else { throw new AssertionError("Should not reach here"); } // successfully replaced us with new CacheValue -> return the value // wrapped by it return value; } 回看proxyClassCache可以知道valueFacotry对应的是ProxyClassFactory类,这是java.reflect.Proxy的内部类: @Override public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); for (Class<?> intf : interfaces) { /* * Verify that the class loader resolves the name of this * interface to the same Class object. */ Class<?> interfaceClass = null; try { interfaceClass = Class.forName(intf.getName(), false, loader);// 看下接口对应的类文件有没有被ClassLoader加载 } catch (ClassNotFoundException e) { } if (interfaceClass != intf) { throw new IllegalArgumentException( intf + " is not visible from class loader"); } /* * Verify that the Class object actually represents an * interface. */ if (!interfaceClass.isInterface()) {// 看下是不是都是接口 throw new IllegalArgumentException( interfaceClass.getName() + " is not an interface"); } /* * Verify that this interface is not a duplicate. */ if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {// 接口不能重复 throw new IllegalArgumentException( "repeated interface: " + interfaceClass.getName()); } } String proxyPkg = null; // package to define proxy class in /* * Record the package of a non-public proxy interface so that the * proxy class will be defined in the same package. Verify that * all non-public proxy interfaces are in the same package. */ for (Class<?> intf : interfaces) { int flags = intf.getModifiers(); if (!Modifier.isPublic(flags)) {// 接口不是public的,截取包名,保证代理类跟委托类同一个包下 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 from different packages"); } } } if (proxyPkg == null) {// 是public的接口,拼接成com.sun.proxy$Proxy,再拼接一个数字 // if no non-public proxy interfaces, use com.sun.proxy package proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; } /* * Choose a name for the proxy class to generate. */ long num = nextUniqueNumber.getAndIncrement(); String proxyName = proxyPkg + proxyClassNamePrefix + num; /* * Generate the specified proxy class. */ // 核心,生成代理类的字节码 byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces); try { // 根据字节码生成代理类Class对象,native方法,看过类加载器的童鞋应该不陌生 return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { /* * A ClassFormatError here means that (barring bugs in the * proxy class generation code) there was some other * invalid aspect of the arguments supplied to the proxy * class creation (such as virtual machine limitations * exceeded). */ throw new IllegalArgumentException(e.toString()); } } 这段代码前面一大段就是检查、校验接口,然后利用ProxyGenerator生成代理类的字节码数组,接着将字节码封装成Class对象,就是代理类的Class对象了。生成字节码数组的代码就不详细说了。 将字节码数组写到文件里查看一下: byte[] data = ProxyGenerator.generateProxyClass(“Animal$Proxy”, new Class[] { Animal.class }); FileOutputStream out = new FileOutputStream("Animal$Proxy.class"); out.write(data); 利用jd-gui反编译工具可以看看代理类的源码: import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; public final class Animal$Proxy extends Proxy implements my.java.reflect.test.Animal { private static Method m3; private static Method m1; private static Method m0; private static Method m2; public Animal$Proxy(InvocationHandler paramInvocationHandler) { super(paramInvocationHandler); } public final void sayHello() { try { this.h.invoke(this, m3, null); return; } catch (Error | RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } public final boolean equals(Object paramObject) { try { return ((Boolean) this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue(); } catch (Error | RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } public final int hashCode() { try { return ((Integer) this.h.invoke(this, m0, null)).intValue(); } catch (Error | RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } public final String toString() { try { return (String) this.h.invoke(this, m2, null); } catch (Error | RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } static { try { m3 = Class.forName("my.java.reflect.test.Animal").getMethod("sayHello", new Class[0]); m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") }); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); return; } catch (NoSuchMethodException localNoSuchMethodException) { throw new NoSuchMethodError(localNoSuchMethodException.getMessage()); } catch (ClassNotFoundException localClassNotFoundException) { throw new NoClassDefFoundError(localClassNotFoundException.getMessage()); } } } 可以看到生成的代理类有一个构造方法,参数是InvocationHandler,然后回看我们第一步分析的时候,得到代理类的Class对象后,会通过反射得到代理类的构造方法,接着调用构造方法,参数就是InvocationHandler。 在代理类的源码里,最值得注意的就是m3,这个对应的是Animal的sayHello方法,当我们通过MyInvocationHandler#bind方法得到代理对象实例后,调用代理对象Animal$Proxy的sayHello方法,就会执行: public final void sayHello() { try { this.h.invoke(this, m3, null); return; } catch (Error | RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } 正好是我们在MyInvocationHandler实现的invoke方法,这样就完成了代理的功能。 用一个总结收尾 之所以将原理先总结了,因为希望把原理先用最简洁的语言说清楚,再来深入分析,否则在深入分析阶段粘贴过多的源码可能会导致阅读兴趣下降。 通过Proxy#newProxyInstance方法得到代理对象实例; 这个代理对象有着和委托类一模一样的方法; 当调用代理对象实例的方法时,这个实例会调用你实现InvocationHandler里的invoke方法。 这里面无疑第一步是最复杂的,这里大概经历了: 利用参数中的接口,通过缓存或者利用ProxyGenerator生成字节码并生成代理类的Class对象; 通过反射得到代理对象的构造方法; 通过构造方法和InvocationHandler参数通过反射实例化出代理对象实例。
从String源码的一个构造方法说起 public String(int[] codePoints, int offset, int count) {} what?codePoints是什么鬼?为了看懂这个源码,有必要了解一个这个codePoints(代码点)的相关知识,其实整个String源码都会不少的涉及的java编码的相关知识,比如indexOf(int ch, int fromIndex)。 一脸懵逼 为什么会有Unicode 学C/C++的时候我们知道了ASCII码,但这个能表示的字符有限,后来又出现了一些乱七八糟的编码表,Unicode就是企图统一一下编码而产生的。 Unicode的介绍 Unicode的第一个版本是用2个字节来编码所有的字符的,因为编码者们认为2^16=65536能容纳世界上所有语言,后来他们发现他们错了,哈哈哈,第二个版本就用4个字节来编码所有字符,这个后面说。 先以第一个版本来说,用2个字节来编码所有字符(这个要很清晰,不然会有点懵),正好UTF-16也是这么弄的,大家误解就以为Unicode就是UTF-16。这里会涉及两个步骤,一个步骤是字符与编码一一对应的问题,如a对97,b对98如此;另一个是如何将编码的二进制这些01串保存起来的问题,这就实现了UTF(unicode transformation format),有UTF-8,UTF-16…… UTF-8与UTF-16 UTF16很好理解,在第一个版本的Unicode中,就是2个字节保存一个字符。UTF8就不同,它可能用1个/2个/3个来表示。那我怎么知道它用来多少个自己来表示呢?这就需要一个规定: 0开头的,就表示1个字节表示一个字符,即0xxx xxxx,如0101 0011 110x xxxx 10xx xxxx这种表示把2个字节当成一个单元,表示一个字符。 1110 xxxx 10xx xxxx 10xx xxxx这种表示3个字节当成一个单元,表示一个字符。 由上面我们可以看出UTF-8需要判断每个字节中的开头标志信息,所以如果一当某个字节在传送过程中出错了,就会导致后面的字节也会解析出错.而UTF-16不会判断开头标志,即使错也只会错一个字符,所以容错能力强. 从上面可以看出,当1个字节表示一个字符时,能表示2^7=128个字符,2个字节表示时能表示2048个字符,3个单元表示时能表示65536个字符。由于"汉"的编码27721大于2048了所有两个字节还不够,只能用三个字节来表示。 接着看看第二个版本 相关规定: Unicode 统一编码 0x000000-0x10ffff,其中0x0000-0xffff为基本多语言平面字符 bmp 基本多语言平面字符,对应Unicode中0x0000-0xffff Unicode码空间为U+0000到U+10FFFF,一共有17个平面,每个平面可容下65536个code point。也就是17 * 65536=1,114,112。但是其中的U+D800-U+DFFF作为UTF-16编码代理区保留,也就是它们不会作为code point分配给字符,保留数目是8 * 256=2048。 看到这里,我们可以知道: 如果在BMP级别中,那么16bits(一个代码单元)就足够表示出字符的Unicode值。 那不在的呢,就属于增补字符了。那就是需要4个字节来表示,那4个字节为啥不是能表示0x00000000-0xffffffff个字符呢,只能表示到0x10ffff呢,这就有点像UTF-8的意思了,占用几个位置来表示我需要用4个字节来显示这个字符,那么需要占用多少位呢?因为只需要表示0x10 ffff - 0x10000 = 0xf ffff即可,就是20位,也就是说,虽然你给我分配4个字节32位这么多,但我只需要20位就足够表示0x10000~0x10FFFF,那剩下12位怎么办呢,这样吧,每2个字节用6位作为代理区,剩下10位用作编码,这就是U+D800-U+DFFF作为UTF-16编码代理区保留的规定了。 那为什么是0xD800~0xDFFF呢,我们来看看,假设我们是编码者,我们现在要留一段来做代理区,假设我做代理区的起点选了0xD800吧,对应的是1100 1000 0000 0000,那我把剩下10位编码上去,就能表示:1101 1000 0000 0000~1101 1011 1111 1111即0xD800~ 0xDBFF,这个就算高位代理区了,然后同理,0xDC00~ 0xDFFF就算地位代理区了。 好了,BMP区域有2048个编码被用作代理区了,所以它们是不能用来表示任何字符的。 Last but not least 简要的讨论了一下以String构造方法引出的一点问题,其实java的编码还是蛮复杂的,一口气说太多太复杂,估计大脑cpu也不够用了。
了解过HashMap都应该知道,HashMap内部会创建一个Entry<K, V> table数组来存放元素,而且这个数组的长度永远都是2的指数次方。那么问题来了,为什么选择2的指数次方呢? 首先,思考一下计算出hash值后,应该存放在数组的哪个位置?显然用求余(模)最简单。然而模的效率并不高,看看JDK是怎么做的,indexFor方法: static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); } 其中h就是hash值,length是table的长度。对2的指数次方求模运算(h % length)和h & (length-1)结果是一样的,这点从代码注释也可以知道,不明白可以看最后的备注。 接着看看为什么容量是2的指数次方的问题,假如容量分别是16和15两种情况,对应的length-1的二进制分别是1111和1110,然后我们思考hash值分别是8和9这两种情况,8 & 1111 = 8,9 & 1111 = 9,存放在table[8]和table[9]对应的位置,没有发生碰撞;然后8 & 1110 = 8,9 & 1110 = 8,两个hash值都被存放在了table的同一位置,显然发生了碰撞。放大来看,如果容量是15的话,length - 1对应的二进制是1110,进行与运算后,最后一位永远是0,即xxx0,这将会导致table中0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费不说,碰撞概率也增大。而我们2的指数次方对应的数length-1后二进制全是1,进行与运算显然不会干扰到原hash值,不会导致table中哪个位置不能存放元素。 解释一下“对2的指数次方求模运算(h % length)和h & (length-1)结果是一样的”,以4(二进制为11)为例: 0 % 4 = 0 同 4 % 4 = 0 …… 1 % 4 = 1 同 5 % 4 = 1 …… 2 % 4 = 2 同 6 % 4 = 2 …… 3 % 4 = 3 同 7 % 4 = 3 …… 00 & 11 = 0 同 100 && 11 = 0 …… 01 & 11 = 1 同 101 && 11 = 1 …… 10 & 11 = 2 同 110 && 11 = 2 …… 11 & 11 = 3 同 111 && 11 = 3 …… 明显,求余运算每4个一循环,对应二进制大于4之后,高位的数字与0进行与运算,始终为0,效果等同于每4个一循环。 总结一下:使用2的指数次方作为HashMap中存放元素数组的大小,可以避免求余操作,效率较高,同时,减少了碰撞次数。
之前看java源码的时候,看到有一个语句,整型无符号右移一个负数(如1024>>-22),当时惊呼:“还有这种操作,老师没教过啊”,老师确实没教过! 当时网上查了一下,发现:对于int类型,移位操作只关注移位参数转换成二进制后对应的后5位(bit);long则只关注后6位。 举个栗子: System.out.println(1024 >>> 10 == 1024 >>> 42);// true System.out.println(1024 >>> 10 == 1024 >>> -22);// true 其中,10转换成二进制后是1010,42转换成二进制后是10101,-22转换成二进制后是11111111111111111111111111101010,所有的都是最后5个bit是01010,所以无符号右移之后都相等。 印象中应该不少书都没讲解到这个知识点,上大学的时候,老师更加没提过这个。
很久没写写东西了,去年底开始在做微服务改造,玩spring boot还有spring cloud。总结一下,免得脑子不好使,忘记事情。 springboot和spring jpa有很好的天然集成,但是如果要用mybatis的话,还是需要自己做一点点配置。刚开始弄springboot和mybatis集成的时候,一顿搜索,发现网上好多人写的都很复杂,属于纯手工集成的方式,自己配置datasource,自己配置扫描路径,自己配置transactionManager。 其实mybatis官方已经帮我们写好了MyBatis-Spring-Boot-Starter,我们只有把它声明到pom.xml即可。而且官方也有相关的demo。但是web工程集成mybatis的时候出现的问题,不知道是不是共性问题,就是必须自己用@Bean注解配置一个DataSource实例,不然springboot就是集成不了mybatis。 具体操作步骤如下: 新建一个mave工程springboot_mybatis 最后的工程如下: QQ截图.png 配置pom.xml pom.xml配置如下: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.lee</groupId> <artifactId>springboot_mybatis</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot_mybatis</name> <description>Spring Boot Setup Demo</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> <mybatis-spring.version>1.2.3</mybatis-spring.version> <mybatis.version>3.3.0</mybatis.version> <h2.version>1.4.190</h2.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.2.0</version> </dependency> <!-- spring jdbc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.27</version> </dependency> <!-- h2 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>${h2.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> </project> Mapper 在application.properties声明mybatis相关的属性 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=org.h2.Driver spring.datasource.jdbc-url=jdbc:h2:tcp://localhost/~/springbootmybatis spring.datasource.username=sa spring.datasource.password= spring.datasource.schema=classpath:import.sql # 声明mapper所在路径 mybatis.mapper-locations=classpath:mapper/*.xml 这里用的是h2数据库,如果使用其他数据库,只要在pom添加相关的依赖和配置数据库的相关属性即可。 *Mapper.xml的配置和普通的配置没有区别 CityMapper.java接口 package com.mapper; import org.apache.ibatis.annotations.Mapper; import com.domain.City; /** * Created by liyc02 on 2017/5/12. */ @Mapper public interface CityMapper { City selectCityById(int id); } 特别注意的是使用的@Mapper注解而不是之前的@Repository注解。 数据源配置 开始以为在application.properties中声明了spring.datasource.type=com.alibaba.druid.pool.DruidDataSource就可以自动配置了,结果不行,需要自己手动配置DataSourceConfig.java package com.config; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import com.alibaba.druid.pool.DruidDataSource; @Configuration public class DataSourceConfig { @Autowired private Environment env; @Bean public DataSource getDataSource() { DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(env.getProperty("spring.datasource.driver-class-name")); ds.setUrl(env.getProperty("spring.datasource.jdbc-url")); ds.setUsername(env.getProperty("spring.datasource.username")); ds.setPassword(env.getProperty("spring.datasource.password")); return ds; } } 以上,运行App.java即可,访问浏览器,可以得到结果。 QQ截图.png
本文不打算介绍过多多线程的基本知识,旨在总结一下使用多线程中需要注意的东西。 大家都知道要写多线程代码可以实现Runnable接口或者继承Thread类。关于二者的区别,大家都知道java是单继承机制的,继承了Thread可能会让你无法再继承其他父类。可能没有考虑内存相关的问题,导致多线程失效。 直接用代码来讨论,以一个卖票系统为例,总票数15张,模拟3个窗口同时卖票。 实现Runnable /** * 不加入同步 * @author * */ public class Ticket1 implements Runnable { // 总票数 private int total = 15; public void run() { while(true) { if(total > 0) { try { Thread.sleep(200); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--); } else { break; } } } public static void main(String[] args) { Ticket1 t = new Ticket1(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); t1.setName("t1"); t2.setName("t2"); t3.setName("t3"); t1.start(); t2.start(); t3.start(); } } 这段代码的问题很明显,没有加入同步控制,输出大致如下: t3 sales ticket_15 t2 sales ticket_14 t1 sales ticket_13 t2 sales ticket_12 t3 sales ticket_11 t1 sales ticket_10 t3 sales ticket_9 t1 sales ticket_7 t2 sales ticket_8 t3 sales ticket_6 t1 sales ticket_5 t2 sales ticket_4 t2 sales ticket_3 t3 sales ticket_2 t1 sales ticket_1 t2 sales ticket_0 t3 sales ticket_-1 加入同步控制保证买票正常: package com.cmsz.upay.thread; /** * 加入同步 * @author * */ public class Ticket2 implements Runnable { // 总票数 private int total = 15; public void run() { while(true) { synchronized (this) { if(total > 0) { try { Thread.sleep(200); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--); } else { break; } } } } public static void main(String[] args) { Ticket2 t = new Ticket2(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); t1.setName("t1"); t2.setName("t2"); t3.setName("t3"); t1.start(); t2.start(); t3.start(); } } 这段代码可以保证卖票正常: t1 sales ticket_15 t1 sales ticket_14 t1 sales ticket_13 t1 sales ticket_12 t1 sales ticket_11 t1 sales ticket_10 t1 sales ticket_9 t1 sales ticket_8 t1 sales ticket_7 t1 sales ticket_6 t1 sales ticket_5 t3 sales ticket_4 t3 sales ticket_3 t3 sales ticket_2 t3 sales ticket_1 继承Thread 先上一段有问题的代码,应该是比较多初学者不会考虑到的: /** * 继承thread * @author * */ public class Ticket3 extends Thread { // 总票数 private int total = 15; public void run() { while(true) { synchronized (this) { if(total > 0) { try { Thread.sleep(200); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--); } else { break; } } } } public static void main(String[] args) { Ticket3 t1 = new Ticket3(); Ticket3 t2 = new Ticket3(); Ticket3 t3 = new Ticket3(); t1.setName("t1"); t2.setName("t2"); t3.setName("t3"); t1.start(); t2.start(); t3.start(); } } 运行结果大致如下: t2 sales ticket_15 t1 sales ticket_15 t3 sales ticket_15 t3 sales ticket_14 t2 sales ticket_14 t1 sales ticket_14 t2 sales ticket_13 t3 sales ticket_13 t1 sales ticket_13 t3 sales ticket_12 t2 sales ticket_12 t1 sales ticket_12 t2 sales ticket_11 t3 sales ticket_11 t1 sales ticket_11 t2 sales ticket_10 t1 sales ticket_10 t3 sales ticket_10 t1 sales ticket_9 t2 sales ticket_9 t3 sales ticket_9 t1 sales ticket_8 t2 sales ticket_8 t3 sales ticket_8 t2 sales ticket_7 t1 sales ticket_7 t3 sales ticket_7 t2 sales ticket_6 t1 sales ticket_6 t3 sales ticket_6 t2 sales ticket_5 t1 sales ticket_5 t3 sales ticket_5 t2 sales ticket_4 t3 sales ticket_4 t1 sales ticket_4 t2 sales ticket_3 t3 sales ticket_3 t1 sales ticket_3 t2 sales ticket_2 t1 sales ticket_2 t3 sales ticket_2 t2 sales ticket_1 t3 sales ticket_1 t1 sales ticket_1 很明显每个线程都卖了15张票,跟我们预期的不符合,我们在学习面向对象的时候知道了两个概念:类变量和实例变量,不展开细说二者的区别,实例变量就是每new一个实例出来之后,变量都会有一个自己的内存区域,不受他人影响;而类变量就是所有通过该类实例化的对象共享一个变量。Ticket3的问题之处就是将票总数total声明为实例变量了,这样我们每新建一个实例(线程)total都会有一个自己的内存区域,所以每次卖的都是自己那15张票。 OK,发现问题解决问题,将票数声明为类变量问题应该就能解决了,试一下: package com.cmsz.upay.thread; /** * 继承thread,共享变量 * @author * */ public class Ticket4 extends Thread { // 总票数 private static int total = 15; public void run() { while(true) { synchronized (this) { if(total > 0) { try { Thread.sleep(200); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--); } else { break; } } } } public static void main(String[] args) { Ticket4 t1 = new Ticket4(); Ticket4 t2 = new Ticket4(); Ticket4 t3 = new Ticket4(); t1.setName("t1"); t2.setName("t2"); t3.setName("t3"); t1.start(); t2.start(); t3.start(); } } 运行结果如下: t2 sales ticket_15 t1 sales ticket_14 t3 sales ticket_13 t1 sales ticket_12 t2 sales ticket_11 t3 sales ticket_10 t2 sales ticket_9 t1 sales ticket_8 t3 sales ticket_7 t2 sales ticket_5 t1 sales ticket_6 t3 sales ticket_4 t2 sales ticket_3 t1 sales ticket_2 t3 sales ticket_1 t1 sales ticket_0 t2 sales ticket_-1 坑爹的问题出现了,虽然是大家共同卖15张票,但是卖出了0和-1号的票,也就是我们买了17张票,现实是这样的话估计抢座要打起来了…… 分析一下原因,共享变量没问题,那就是同步出现问题了,同步有什么问题呢? 问题出现在synchronized(this),获取锁的对象是this,很明显3个Thread的this对象是不同的,说白了就是这个加锁根本没有锁住共享变量。 知道了问题的原因,我们只要保证synchronized(syncObject)中的syncObject唯一即可,声明一个类变量即可。 /** * 继承thread,共享变量,锁相同对象 * @author * */ public class Ticket5 extends Thread { private static Object syncObject = new Object(); // 总票数 private static int total = 15; public void run() { while(true) { synchronized (syncObject) { if(total > 0) { try { Thread.sleep(200); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " sales ticket_" + total--); } else { break; } } } } public static void main(String[] args) { Ticket5 t1 = new Ticket5(); Ticket5 t2 = new Ticket5(); Ticket5 t3 = new Ticket5(); t1.setName("t1"); t2.setName("t2"); t3.setName("t3"); t1.start(); t2.start(); t3.start(); } } 运行即可保证正常。 本文总结主要针对编写多线程代码过程中共享变量的问题和锁机制的一些细节,初学者容易犯错或者欠考虑,通过几个Demo的总结,可以把一些零散的知识点汇总在一起,保证看问题的全面性。
之前写了一篇支付宝支付开发,现在总结一下支付宝退款的开发。 总体上来说,支付宝的退款接口分为即时到账批量有密退款接口(refund_fastpay_by_platform_pwd)和即时到账批量无密退款接口(refund_fastpay_by_platform_nopwd)两种,分别对应前台请求和后台请求。 通俗说明一下前台请求和后台请求的区别: 前台请求:通过html的form表单方式提交的。 后台请求:直接通过后台交互,利用http请求发送到请求系统。 一、有密退款接口 及时到账有密退款接口比较简单,易于理解,相关字段到支付宝网站查看一般业务流程是:用户发起退款请求->运营人员审批退款请求->同意退款->后台组装退款报文(html)->重定向到支付宝退款页面->运营人员输入密码->支付宝将退款结果异步通知到后台->后台完成剩余后台逻辑。 ** DEMO如下: ** 用户发起退款后,运营人员打开退款审核页面: Paste_Image.png 确认可以退款后,后台组装一个重定向的html到支付宝: Paste_Image.png 输入密码后支付宝向notify_url发送退款结果通知。 注: 组装发送到支付宝的html报文样例如下: <html> <head></head> <body> <form id="alipaysubmit" name="alipaysubmit" action="https://mapi.alipay.com/gateway.do?_input_charset=utf-8" method="post"> <input type="hidden" name="sign" value="签名值" /> <input type="hidden" name="_input_charset" value="utf-8" /> <input type="hidden" name="sign_type" value="MD5" /> <input type="hidden" name="detail_data" value="201606132100100479^0.01^我要退款" /> <input type="hidden" name="service" value="refund_fastpay_by_platform_pwd" /> <input type="hidden" name="notify_url" value="退款通知地址" /> <input type="hidden" name="seller_user_id" value="值同partner" /> <input type="hidden" name="partner" value="合作商户ID,需跟支付宝申请" /> <input type="hidden" name="batch_num" value="1" /> <input type="hidden" name="batch_no" value="20160613236648747861707090426379" /> <input type="hidden" name="refund_date" value="2016-06-13 07:57:12" /> <input type="submit" value="确认" style="display:none;" /> </form> <script>document.forms['alipaysubmit'].submit();</script> </body> </html> 其中的detail_data为单笔数据集,规则见支付宝文档。 Paste_Image.png 需要说明的是:原付款支付宝交易号对应的是支付结果通知时的trade_no字段。 二、无密退款接口 无密退款接口的权限是不开放的,如果你需要的话,需要重新与支付宝签约,有密接口是跟着支付一起的,你具备了支付的接口权限,就能用有密退款接口。 无密退款接口的流程比较简单:用户发起退款->后台组装退款数据->通过http请求到支付宝后台->接收退款结果异步通知->后台完成剩余后台逻辑。 无密接口有几个概念说明如下: 充退:支付宝将退款退到买家支付宝账号后,将钱再退到买家银行卡的过程,也就是这里是两个过程:退到买家支付宝账号,退到买家银行卡。一般我们不关心这个,因为钱退到买家支付宝账号后,钱已经退给买家了,至于能不能从买家的支付宝退到买家的银行卡,一般不关心。 充退结果通知:充退到买家银行卡成功的异步通知。 三、说明(遇到的坑) 无密接口流程比较简单,一开始就开发了无密接口,结果发现没签约,没权限。 无密接口请求地址:https://mapi.alipay.com/gateway.do?_input_charset=UTF-8 注意到gateway.do后面的_input_charset的值为UTF-8,我们在请求报文中可能也会有_input_charset这个key,注意其value也应该为UTF-8,不能为小写utf-8,否则验签不过,被坑了一天。 最后 Paste_Image.png
接着上一篇,总结一下HttpClient发送https请求相关的内容。 先简单介绍连接工厂(interface org.apache.http.conn.socket.ConnectionSocketFactory),连接工厂主要用于创建、初始化、连接socket。org.apache.http.conn.socket.PlainConnectionSocketFactory是默认的socket工厂,用于创建无加密(unencrypted)socket对象。创建https需要使用org.apache.http.conn.ssl.SSLConnectionSocketFactory,PlainConnectionSocketFactory和SSLConnectionSocketFactory都实现了ConnectionSocketFactory。 好了,直接上代码,代码实现的功能是,组装一个发往银联的查询报文(查询交易结果)。 import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.apache.http.HttpEntity; import org.apache.http.NameValuePair; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; /** * This example demonstrates how to create secure connections with a custom SSL * context. */ public class ClientCustomSSL { private static String reqStr = "txnType=00&signMethod=01&certId=68759663125&encoding=UTF-8&merId=777290058110048&bizType=000201&txnSubType=00&signature=k0lrWgeLK%2Fx%2B8ajj15QCYfmdQxZSKBjXUJN0bLt17rp87ptogxWgHAAq7EUt8RlEbxD6GaRngwtdLGiy6are45Gj1dBLJBtW2841WIq4Ywzx3oK6538Kfh9ll91GJcZJGYz8LuJoZfii7HFPlpl1ZsPZbbdKP6WFVHNMnGnL9nk9QSa%2BihXGpyK%2Fy1FA42AJpfc%2FTT3BV6C%2FxpoEhXzVckHnniVnCpLdGnPfZOd76wK%2Fa%2BALNmniwUZmMj9uNPwnONIIwL%2FFqrqQinQArolW%2FrcIt9NL7qKvQujM%2BdRvd1fboAHI5bZC3ktVPB0s5QFfsRhSRFghVi4RHOzL8ZG%2FVQ%3D%3D&orderId=20160309145206&version=5.0.0&txnTime=20160309145206&accessType=0"; private static String url = "https://101.231.204.80:5000/gateway/api/queryTrans.do"; // 信任管理器 private static X509TrustManager tm = new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return null; } }; public final static void main(String[] args) throws Exception { long starttime = System.currentTimeMillis(); SSLContext sslContext = SSLContext.getInstance("TLS"); // 初始化SSL上下文 sslContext.init(null, new TrustManager[] { tm }, null); // SSL套接字连接工厂,NoopHostnameVerifier为信任所有服务器 SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,NoopHostnameVerifier.INSTANCE); /** * 通过setSSLSocketFactory(sslsf)保证httpclient实例能发送Https请求 */ CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).setMaxConnTotal(50) .setMaxConnPerRoute(50).setDefaultRequestConfig(RequestConfig.custom() .setConnectionRequestTimeout(60000).setConnectTimeout(60000).setSocketTimeout(60000).build()) .build(); try { HttpPost httppost = new HttpPost(url); // 设置参数,参数含义不需要理解 Map<String, String> map = new HashMap<String, String>(); map.put("txnType","00"); map.put("signMethod","01"); map.put("certId","68759663125"); map.put("encoding","UTF-8"); map.put("merId","777290058110048"); map.put("bizType","000201"); map.put("txnSubType","00"); map.put("signature","k0lrWgeLK%2Fx%2B8ajj15QCYfmdQxZSKBjXUJN0bLt17rp87ptogxWgHAAq7EUt8RlEbxD6GaRngwtdLGiy6are45Gj1dBLJBtW2841WIq4Ywzx3oK6538Kfh9ll91GJcZJGYz8LuJoZfii7HFPlpl1ZsPZbbdKP6WFVHNMnGnL9nk9QSa%2BihXGpyK%2Fy1FA42AJpfc%2FTT3BV6C%2FxpoEhXzVckHnniVnCpLdGnPfZOd76wK%2Fa%2BALNmniwUZmMj9uNPwnONIIwL%2FFqrqQinQArolW%2FrcIt9NL7qKvQujM%2BdRvd1fboAHI5bZC3ktVPB0s5QFfsRhSRFghVi4RHOzL8ZG%2FVQ%3D%3D"); map.put("orderId","20160309145206"); map.put("version","5.0.0"); map.put("txnTime","20160309145206"); map.put("accessType","0"); List<NameValuePair> list = new ArrayList<NameValuePair>(); Iterator<Entry<String, String>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Entry<String, String> elem = (Entry<String, String>) iterator.next(); list.add(new BasicNameValuePair(elem.getKey(), elem.getValue())); } if (list.size() > 0) { UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); httppost.setEntity(entity); } System.out.println("executing request " + httppost.getRequestLine()); CloseableHttpResponse response = httpclient.execute(httppost); try { HttpEntity entity = response.getEntity(); System.out.println("----------------------------------------"); System.out.println(response.getStatusLine()); if (entity != null) { System.out.println("Response content length: " + entity.getContentLength()); } String s = EntityUtils.toString(entity,"UTF-8"); System.out.println("应答内容:" + s); EntityUtils.consume(entity); } finally { response.close(); } } finally { httpclient.close(); } long endtime = System.currentTimeMillis(); System.out.println("耗时:" + (endtime-starttime) + "ms"); } } 使用注册器可以保证既能发送http请求也能发送httpsclient请求,代码块如下: int httpReqTimeOut = 60000;//60秒 SSLContext sslContext = SSLContext.getInstance("TLS"); // 初始化SSL上下文 sslContext.init(null, new TrustManager[] { tm }, null); // SSL套接字连接工厂,NoopHostnameVerifier为信任所有服务器 SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,NoopHostnameVerifier.INSTANCE); // 注册http套接字工厂和https套接字工厂 Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.INSTANCE) .register("https", sslsf) .build(); // 连接池管理器 PoolingHttpClientConnectionManager pcm = new PoolingHttpClientConnectionManager(r); pcm.setMaxTotal(maxConnTotal);//连接池最大连接数 pcm.setDefaultMaxPerRoute(maxConnPerRoute);//每个路由最大连接数 /** * 请求参数配置 * connectionRequestTimeout: * 从连接池中获取连接的超时时间,超过该时间未拿到可用连接, * 会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool * connectTimeout: * 连接上服务器(握手成功)的时间,超出该时间抛出connect timeout * socketTimeout: * 服务器返回数据(response)的时间,超过该时间抛出read timeout */ RequestConfig requestConfig = RequestConfig.custom() .setConnectionRequestTimeout(httpReqTimeOut) .setConnectTimeout(httpReqTimeOut) .setSocketTimeout(httpReqTimeOut) .build(); /** * 构造closeableHttpClient对象 */ closeableHttpClient = HttpClients.custom() .setDefaultRequestConfig(requestConfig) .setConnectionManager(pcm) .setRetryHandler(retryHandler) .build(); 关键代码为: // 注册http套接字工厂和https套接字工厂 Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.INSTANCE) .register("https", sslsf) .build();
Base64编码是基于64个字符(字符分别为:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxzy0123456789+/)的编码方式,因为2的6次方正好为64,所以我们用6bit就可以表示出64个字符,eg:000000对应'A',000001对应'B',111111对应'/'。 转换表如下: QQ截图20160411092432.png 按我们的习惯,8bit是1个字节,所以我们正常使用的时候,一般都是1Byte=8bit(字符'a'=97=01100001)来使用。所以我们处理字符串的时候会遇到8bit/16bit/24bit/32bit……的情况,而Base64按照6bit为一个单元,处理的时候会遇到6bit/12bit/18bit/24bit/30bit……的情况,8和6的最小公倍数是24,所以我们用1、2、3个正常字符这三种情况就可以把所有需要转换成Base64的字符串概括了。 被3整除个字符(3/6/9/12……):abc=01100001 01100010 01100011 分成Base64分组后为:011000 010110 001001 100011 即24 22 9 35,对应Base64编码的 YWJj 除3余1个字符(2/5/8/11……):ab=01100001 01100010,分成Base64分组后为:011000 010110 0010,0010不够6bit,需要补0为:001000,得到YWI,因为4个Base编码为一组,最后再补上'='补齐一组,即:YWI= 除3余2个字符(1/4/7/10……):a=011000010,分成Base64分组后为:011000 01,01不够6bit,需要补0为:010000,得到YQ,因为4个Base编码为一组,最后再补上'='补齐一组,即:YQ== 上面的文字归结为下图: QQ截图20160411094646.png 可以看出,所有转换后的Base64编码都是4个字符的倍数(4/8/12/16……),如果不够4个字符的,都用'='填充了。 /** * <p>Base64编码是基于64个字符(字符分别为:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxzy0123456789+/)的编码方式, * 因为2的6次方正好为64,所以我们用6bit就可以表示出64个字符,eg:000000对应A,000001对应B</p> * <p>按我们的习惯,一般都是1Byte=8bit(字符'a'=97=01100001)的使用,所以我们可以用24bit(6和8的最小公倍数)来进行分组,24正好事3BYTE,可以分出4组Base64的分组,需要编码的字符串有如下情况: * <li>被3整除:abc=01100001 01100010 01100011 分成Base64分组后为:011000 010110 001001 100011 即24 22 9 35,对应Base64编码的 YWJj</li> * <li>除3余1:ab=01100001 01100010,分成Base64分组后为:011000 010110 0010,0010不够6bit,需要补0为:001000,得到YWI,因为4个Base编码为一组,最后再补上'='补齐一组,即:YWI=</li> * <li>除3余2:a=011000010,分成Base64分组后为:011000 01,01不够6bit,需要补0为:010000,得到YQ,因为4个Base编码为一组,最后再补上'='补齐一组,即:YQ==</li> * </p> * @author chmod400 * */ public class Base64Utils { private static String codeStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxzy0123456789+/"; private static char[] code = codeStr.toCharArray(); /** * 对字符串进行Base64编码 * @param str * @return */ public static String encode(String str) { if(str == null) { throw new NullPointerException(); } StringBuffer result = new StringBuffer(); // 字符串转为二进制 String binStr = str2Bin(str); // 6bit 为一个单元,需要补0的位数 int tail = binStr.length() % 6; if(tail != 0) {//最后剩2bit,需要补4位,剩4位需要补2bit tail = 6 - tail; } for(int i = 0; i < tail; i++) { binStr += "0"; } for(int i = 0; i < binStr.length() / 6; i++) { int beginIndex = i * 6; String s = binStr.substring(beginIndex, beginIndex+6); // 二进制转十进制 int codePoint = Integer.valueOf(s, 2); // 对应的字符 char c = code[codePoint]; result.append(c); } // 需要补=的位数 int groupNum = binStr.length() / 6;// 6bit为一组 if((groupNum % 4) != 0) { tail = 4 - groupNum % 4; } for(int i = 0; i < tail; i++) { result.append("="); } return result.toString(); } /** * base64解码 * @param str * @return */ public static String decode(String str) { if(str == null) { throw new NullPointerException(); } StringBuffer result = new StringBuffer(); // 去除末尾的'=' int index = str.indexOf("="); if (index >= 0) { str = str.substring(0, index); } // base64字符串转换为二进制 String binStr = base64Str2Bin(str); // 将二进制按8bit一组还原成原字符 for(int i = 0; i < binStr.length() / 8; i++) { int beginIndex = i * 8; String s = binStr.substring(beginIndex, beginIndex+8); String c = bin2Str(s); result.append(c); } return result.toString(); } /** * 字符串转换为二进制字符串 * @param str * @return */ private static String str2Bin(String str) { StringBuffer sb = new StringBuffer(); // 字符串转为字符数组 char[] c = str.toCharArray(); for(int i = 0; i < c.length; i++) { // 将每个字符转换为二进制 String s = Integer.toBinaryString(c[i]); // 需要补0的长度 int len = 8 - s.length(); for(int j = 0; j < len; j++) { s = "0" + s; } sb.append(s); } return sb.toString(); } /** * Base64字符串转换为二进制字符串 * @param str * @return */ private static String base64Str2Bin(String str) { StringBuffer sb = new StringBuffer(); // 字符串转为字符数组 char[] c = str.toCharArray(); for(int i = 0; i < c.length; i++) { // 将每个字符转换为二进制 int index = codeStr.indexOf(c[i]); String s = Integer.toBinaryString(index); // 需要补0的长度 int len = 6 - s.length(); for(int j = 0; j < len; j++) { s = "0" + s; } sb.append(s); } return sb.toString(); } /** * 二进制转换为字符串 * @param binStr * @return */ private static String bin2Str(String binStr) { StringBuffer sb = new StringBuffer(); for(int i = 0; i < binStr.length() / 8; i++) { int beginIndex = i * 8; String s = binStr.substring(beginIndex, beginIndex+8); // 二进制转十进制 int codePoint = Integer.valueOf(s, 2); // 对应的字符 char c = Character.toChars(codePoint)[0]; sb.append(c); } return sb.toString(); } public static void main(String[] args) { System.out.println(str2Bin("ab")); // System.out.println(bin2Str("000001000001000001000000")); /*System.out.println(encode("a")); System.out.println(encode("ab")); System.out.println(encode("abc")); System.out.println(encode("")); System.out.println(encode(null));*/ // System.out.println(encode(codeStr)); System.out.println(decode("YQ==")); System.out.println(decode("YWI=")); System.out.println(decode("YWJj")); System.out.println(decode("QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJydHV2d3h6eTAxMjM0NTY3ODkrLw==")); System.out.println(decode("")); System.out.println(decode(null)); } }
接着上一篇,我们在使用HttpClient的时候更多的是需要自己根据业务特点创建自己定制化的HttpClient实例,而不是像之前那样使用 // 使用默认配置创建httpclient的实例 CloseableHttpClient client = HttpClients.createDefault(); 废话不多说,直接上代码(Talk is cheap, show me the code!): import org.apache.http.HttpEntity; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; /** * 使用httpclient-4.5.2发送请求,配置请求定制化参数 * @author chmod400 * 2016.3.28 */ public class RequestConfigDemo { public static void main(String[] args) { try { String url = "http://www.baidu.com"; /** * 请求参数配置 * connectionRequestTimeout: * 从连接池中获取连接的超时时间,超过该时间未拿到可用连接, * 会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool * connectTimeout: * 连接上服务器(握手成功)的时间,超出该时间抛出connect timeout * socketTimeout: * 服务器返回数据(response)的时间,超过该时间抛出read timeout */ CloseableHttpClient client = HttpClients.custom().setDefaultRequestConfig(RequestConfig.custom() .setConnectionRequestTimeout(2000).setConnectTimeout(2000).setSocketTimeout(2000).build()).build(); HttpPost post = new HttpPost(url); // HttpGet get = new HttpGet(url); CloseableHttpResponse response = client.execute(post); // CloseableHttpResponse response = client.execute(get); // 服务器返回码 int status_code = response.getStatusLine().getStatusCode(); System.out.println("status_code = " + status_code); // 服务器返回内容 String respStr = null; HttpEntity entity = response.getEntity(); if(entity != null) { respStr = EntityUtils.toString(entity, "UTF-8"); } System.out.println("respStr = " + respStr); // 释放资源 EntityUtils.consume(entity); } catch (Exception e) { e.printStackTrace(); } } } 需要说明的是,需要自己定制HttpClient客户端的话,我们使用HttpClients.custom(),然后调用各种set方法即可,一般建议使用HttpClients.custom().setDefaultRequestConfig(),org.apache.http.client.config.RequestConfig类提供了很多可定制的参数,我们可以根据自己的配置来使用相关配置。有几个参数我们自己必须设置一下 connectionRequestTimeout:从连接池中获取连接的超时时间,超过该时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool connectTimeout:连接上服务器(握手成功)的时间,超出该时间抛出connect timeout socketTimeout:服务器返回数据(response)的时间,超过该时间抛出read timeout 通过打断点的方式我们知道,HttpClients在我们没有指定连接工厂的时候默认使用的是连接池工厂org.apache.http.impl.conn.PoolingHttpClientConnectionManager.PoolingHttpClientConnectionManager(Registry<ConnectionSocketFactory>),所以我们需要配置一下从连接池获取连接池的超时时间。 以上3个超时相关的参数如果未配置,默认为-1,意味着无限大,就是一直阻塞等待! 官方提供了一个demo,里面有一些最常用的配置代码,仅供参考: /* * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package org.apache.http.examples.client; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.charset.CodingErrorAction; import java.util.Arrays; import javax.net.ssl.SSLContext; import org.apache.http.Consts; import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.ParseException; import org.apache.http.client.CookieStore; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.AuthSchemes; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.ConnectionConfig; import org.apache.http.config.MessageConstraints; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.config.SocketConfig; import org.apache.http.conn.DnsResolver; import org.apache.http.conn.HttpConnectionFactory; import org.apache.http.conn.ManagedHttpClientConnection; import org.apache.http.conn.routing.HttpRoute; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.DefaultHttpResponseFactory; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.DefaultHttpResponseParser; import org.apache.http.impl.conn.DefaultHttpResponseParserFactory; import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.conn.SystemDefaultDnsResolver; import org.apache.http.impl.io.DefaultHttpRequestWriterFactory; import org.apache.http.io.HttpMessageParser; import org.apache.http.io.HttpMessageParserFactory; import org.apache.http.io.HttpMessageWriterFactory; import org.apache.http.io.SessionInputBuffer; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicLineParser; import org.apache.http.message.LineParser; import org.apache.http.ssl.SSLContexts; import org.apache.http.util.CharArrayBuffer; import org.apache.http.util.EntityUtils; /** * This example demonstrates how to customize and configure the most common aspects * of HTTP request execution and connection management. */ public class ClientConfiguration { public final static void main(String[] args) throws Exception { // Use custom message parser / writer to customize the way HTTP // messages are parsed from and written out to the data stream. HttpMessageParserFactory<HttpResponse> responseParserFactory = new DefaultHttpResponseParserFactory() { @Override public HttpMessageParser<HttpResponse> create( SessionInputBuffer buffer, MessageConstraints constraints) { LineParser lineParser = new BasicLineParser() { @Override public Header parseHeader(final CharArrayBuffer buffer) { try { return super.parseHeader(buffer); } catch (ParseException ex) { return new BasicHeader(buffer.toString(), null); } } }; return new DefaultHttpResponseParser( buffer, lineParser, DefaultHttpResponseFactory.INSTANCE, constraints) { @Override protected boolean reject(final CharArrayBuffer line, int count) { // try to ignore all garbage preceding a status line infinitely return false; } }; } }; HttpMessageWriterFactory<HttpRequest> requestWriterFactory = new DefaultHttpRequestWriterFactory(); // Use a custom connection factory to customize the process of // initialization of outgoing HTTP connections. Beside standard connection // configuration parameters HTTP connection factory can define message // parser / writer routines to be employed by individual connections. HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory = new ManagedHttpClientConnectionFactory( requestWriterFactory, responseParserFactory); // Client HTTP connection objects when fully initialized can be bound to // an arbitrary network socket. The process of network socket initialization, // its connection to a remote address and binding to a local one is controlled // by a connection socket factory. // SSL context for secure connections can be created either based on // system or application specific properties. SSLContext sslcontext = SSLContexts.createSystemDefault(); // Create a registry of custom connection socket factories for supported // protocol schemes. Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.INSTANCE) .register("https", new SSLConnectionSocketFactory(sslcontext)) .build(); // Use custom DNS resolver to override the system DNS resolution. DnsResolver dnsResolver = new SystemDefaultDnsResolver() { @Override public InetAddress[] resolve(final String host) throws UnknownHostException { if (host.equalsIgnoreCase("myhost")) { return new InetAddress[] { InetAddress.getByAddress(new byte[] {127, 0, 0, 1}) }; } else { return super.resolve(host); } } }; // Create a connection manager with custom configuration. PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager( socketFactoryRegistry, connFactory, dnsResolver); // Create socket configuration SocketConfig socketConfig = SocketConfig.custom() .setTcpNoDelay(true) .build(); // Configure the connection manager to use socket configuration either // by default or for a specific host. connManager.setDefaultSocketConfig(socketConfig); connManager.setSocketConfig(new HttpHost("somehost", 80), socketConfig); // Validate connections after 1 sec of inactivity connManager.setValidateAfterInactivity(1000); // Create message constraints MessageConstraints messageConstraints = MessageConstraints.custom() .setMaxHeaderCount(200) .setMaxLineLength(2000) .build(); // Create connection configuration ConnectionConfig connectionConfig = ConnectionConfig.custom() .setMalformedInputAction(CodingErrorAction.IGNORE) .setUnmappableInputAction(CodingErrorAction.IGNORE) .setCharset(Consts.UTF_8) .setMessageConstraints(messageConstraints) .build(); // Configure the connection manager to use connection configuration either // by default or for a specific host. connManager.setDefaultConnectionConfig(connectionConfig); connManager.setConnectionConfig(new HttpHost("somehost", 80), ConnectionConfig.DEFAULT); // Configure total max or per route limits for persistent connections // that can be kept in the pool or leased by the connection manager. connManager.setMaxTotal(100); connManager.setDefaultMaxPerRoute(10); connManager.setMaxPerRoute(new HttpRoute(new HttpHost("somehost", 80)), 20); // Use custom cookie store if necessary. CookieStore cookieStore = new BasicCookieStore(); // Use custom credentials provider if necessary. CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); // Create global request configuration RequestConfig defaultRequestConfig = RequestConfig.custom() .setCookieSpec(CookieSpecs.DEFAULT) .setExpectContinueEnabled(true) .setTargetPreferredAuthSchemes(Arrays.asList(AuthSchemes.NTLM, AuthSchemes.DIGEST)) .setProxyPreferredAuthSchemes(Arrays.asList(AuthSchemes.BASIC)) .build(); // Create an HttpClient with the given custom dependencies and configuration. CloseableHttpClient httpclient = HttpClients.custom() .setConnectionManager(connManager) .setDefaultCookieStore(cookieStore) .setDefaultCredentialsProvider(credentialsProvider) .setProxy(new HttpHost("myproxy", 8080)) .setDefaultRequestConfig(defaultRequestConfig) .build(); try { HttpGet httpget = new HttpGet("http://httpbin.org/get"); // Request configuration can be overridden at the request level. // They will take precedence over the one set at the client level. RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig) .setSocketTimeout(5000) .setConnectTimeout(5000) .setConnectionRequestTimeout(5000) .setProxy(new HttpHost("myotherproxy", 8080)) .build(); httpget.setConfig(requestConfig); // Execution context can be customized locally. HttpClientContext context = HttpClientContext.create(); // Contextual attributes set the local context level will take // precedence over those set at the client level. context.setCookieStore(cookieStore); context.setCredentialsProvider(credentialsProvider); System.out.println("executing request " + httpget.getURI()); CloseableHttpResponse response = httpclient.execute(httpget, context); try { System.out.println("----------------------------------------"); System.out.println(response.getStatusLine()); System.out.println(EntityUtils.toString(response.getEntity())); System.out.println("----------------------------------------"); // Once the request has been executed the local context can // be used to examine updated state and various objects affected // by the request execution. // Last executed request context.getRequest(); // Execution route context.getHttpRoute(); // Target auth state context.getTargetAuthState(); // Proxy auth state context.getTargetAuthState(); // Cookie origin context.getCookieOrigin(); // Cookie spec used context.getCookieSpec(); // User security token context.getUserToken(); } finally { response.close(); } } finally { httpclient.close(); } } }
跟服务器交互,更多的是发送数据,然后接收到服务器返回的数据,一般我们利用http-client中的实体(Entity),具体在org.apache.http.entity包下面的类来封装我们的请求,从服务器上接收的也是实体,这个在上一篇的response.getEntity()可知。 HttpClient根据内容的出处来区分3种实体: 流式(Stream):内容从流(Stream)中接收,或者在运行中产生(generated on the fly)。 自我包含(self-contained):内容在内存中或通过独立的连接或其他实体中获得。 包装(wrapping):内容是从其他实体(Entity)中获得的。 只有自我包含的实体是可以重复的,重复意味着可以被多次读取,如多次调用EntityUtils.toString(entity)。具体来讲有如:StringEntity、ByteArrayEntity…… 废话不多说,上代码,看看如何向服务器发送请求。 import java.util.ArrayList; import java.util.List; import org.apache.http.HttpEntity; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; /** * 使用httpclient-4.5.2发送请求参数 * @author chmod400 * 2016.3.24 */ public class RequestParameterDemo { public static void main(String[] args) { try { String url = "http://localhost:9090"; // 使用默认配置创建httpclient的实例 CloseableHttpClient client = HttpClients.createDefault(); HttpPost post = new HttpPost(url); /** * 设置参数,常用的有StringEntity,UrlEncodedFormEntity,MultipartEntity * 具体看org.apache.http.entity包 */ List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("username", "张三")); params.add(new BasicNameValuePair("password", "123456")); UrlEncodedFormEntity e = new UrlEncodedFormEntity(params, "UTF-8"); post.setEntity(e); CloseableHttpResponse response = client.execute(post); // 服务器返回码 int status_code = response.getStatusLine().getStatusCode(); System.out.println(status_code); // 服务器返回内容 String respStr = null; HttpEntity entity = response.getEntity(); if(entity != null) { respStr = EntityUtils.toString(entity, "UTF-8"); } System.out.println("respStr = " + respStr); // 释放资源 EntityUtils.consume(entity); } catch (Exception e) { e.printStackTrace(); } } } 代码模拟了客户端向服务器发送一个表单数据,最常用的场景就是发送报文数据/登陆动作了。 这段代码应该不需要过多的解释。 来欣赏一下官方代码是如何完成一个登陆动作的: import java.net.URI; import java.util.List; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.cookie.Cookie; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; /** * A example that demonstrates how HttpClient APIs can be used to perform * form-based logon. */ public class ClientFormLogin { public static void main(String[] args) throws Exception { BasicCookieStore cookieStore = new BasicCookieStore(); CloseableHttpClient httpclient = HttpClients.custom().setDefaultCookieStore(cookieStore).build(); try { HttpGet httpget = new HttpGet("https://someportal/"); CloseableHttpResponse response1 = httpclient.execute(httpget); try { HttpEntity entity = response1.getEntity(); System.out.println("Login form get: " + response1.getStatusLine()); EntityUtils.consume(entity); System.out.println("Initial set of cookies:"); List<Cookie> cookies = cookieStore.getCookies(); if (cookies.isEmpty()) { System.out.println("None"); } else { for (int i = 0; i < cookies.size(); i++) { System.out.println("- " + cookies.get(i).toString()); } } } finally { response1.close(); } HttpUriRequest login = RequestBuilder.post().setUri(new URI("https://someportal/")) .addParameter("IDToken1", "username").addParameter("IDToken2", "password").build(); CloseableHttpResponse response2 = httpclient.execute(login); try { HttpEntity entity = response2.getEntity(); System.out.println("Login form get: " + response2.getStatusLine()); EntityUtils.consume(entity); System.out.println("Post logon cookies:"); List<Cookie> cookies = cookieStore.getCookies(); if (cookies.isEmpty()) { System.out.println("None"); } else { for (int i = 0; i < cookies.size(); i++) { System.out.println("- " + cookies.get(i).toString()); } } } finally { response2.close(); } } finally { httpclient.close(); } } }
apache httpclient不多介绍这个工具是什么,具体请看官网,不赘述。 进行记录的原因一个是把掉过坑的地方记住,另一个是自httpclient-4.4开始,官方对代码进行了很多调整,4.4以前的很多class和method都过时了,而国内之前很多关于httpclient的分享都是4.4之前的。 个人感觉使用Httpclient比较重要的是看它的代码,和官方的一些例子,可能是网络知识比较短板的原因,官方的tutorial其实看起来挺不清晰的,感觉主线不明确,不能引导你很好的学习,建议初学的人同时结合官网、源码、官方例子、tutorial进行学习。 先看第一个demo,把这个东西用起来再说。 /** * 使用httpclient-4.5.2发送请求 * @author chmod400 * 2016.3.24 */ public class FirstHttpClientDemo { public static void main(String[] args) { try { String url = "http://www.baidu.com"; // 使用默认配置创建httpclient的实例 CloseableHttpClient client = HttpClients.createDefault(); HttpPost post = new HttpPost(url); // HttpGet get = new HttpGet(url); CloseableHttpResponse response = client.execute(post); // CloseableHttpResponse response = client.execute(get); // 服务器返回码 int status_code = response.getStatusLine().getStatusCode(); System.out.println("status_code = " + status_code); // 服务器返回内容 String respStr = null; HttpEntity entity = response.getEntity(); if(entity != null) { respStr = EntityUtils.toString(entity, "UTF-8"); } System.out.println("respStr = " + respStr); // 释放资源 EntityUtils.consume(entity); } catch (Exception e) { e.printStackTrace(); } } } 这个demo主要是完成基本的交互过程,发送请求,接收消息,如果只是做小程序或者不是特别大并发量的系统,基本已经够用了。 进行一些说明: 1.需要向服务器发送请求,我们需要一个org.apache.http.client.HttpClient的实例对象,一般使用的都是org.apache.http.impl.client.CloseableHttpClient,创建该对象的最简单方法是CloseableHttpClient client = HttpClients.createDefault();,HttpClients是负责创建CloseableHttpClient的工厂,现在我们用最简单的方法就是使用默认配置去创建实例,后面我们再讨论有参数定制需求的实例创建方法。我们可以通过打断点的方式看到这个默认的实例对象的连接管理器 : org.apache.http.conn.HttpClientConnectionManager、请求配置 : org.apache.http.client.config.RequestConfig等配置的默认参数,这些都是后面需要了解的。 2.构造请求方法HttpPost post = new HttpPost(url);表示我们希望用那种交互方法与服务器交互,HttpClient为每种交互方法都提供了一个类:HttpGet, HttpHead, HttpPost, HttpPut, HttpDelete, HttpTrace, 还有 HttpOptions。 3.向服务器提交请求CloseableHttpResponse response = client.execute(post);,很明显`CloseableHttpResponse就是用了处理返回数据的实体,通过它我们可以拿到返回的状态码、返回实体等等我们需要的东西。 4.EntityUtils是官方提供一个处理返回实体的工具类,toString方法负责将返回实体装换为字符串,官方是不太建议使用这个类的,除非返回数据的服务器绝对可信和返回的内容长度是有限的。官方建议是自己使用HttpEntity#getContent()或者HttpEntity#writeTo(OutputStream),需要提醒的是记得关闭底层资源。 5.EntityUtils.consume(entity);负责释放资源,通过源码可知,是需要把底层的流关闭: InputStream instream = entity.getContent(); if (instream != null) { instream.close(); } 好了,到此已经完成了httpclient的第一个demo。 jar包地址:Apache HttpClient 4.5.2、Apache HttpCore 4.4
最近在接入各种支付机构,鉴于各种产品业务需求不同,具体开发实现不尽相同,不讨论具体业务实现。 前一篇银联支付开发介绍了几种银联支付交互方法和实例报文。本文主要回顾一下支付宝常用支付方式的开发和交互报文,还有在开发时遇到的一些坑。 支付宝支付产品大全,主要介绍: 手机网站支付 即使到账 移动支付 一、手机网站支付 手机网站支付主要应用于手机、掌上电脑等无线设备的网页上,通过网页跳转或浏览器自带的支付宝快捷支付实现买家付款的功能,资金即时到账。 根据请求参数,后台将相关参数值填好,组装成HTML报文之后返回给客户端即可,比较重要的几个参数 参数 参数名称 参数说明 样例 service 接口名称 接口名称 alipay.wap.create.direct.pay.by.user <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <form id="alipaysubmit" name="alipaysubmit" action="https://mapi.alipay.com/gateway.do?_input_charset=utf-8" method="get"> <input type="hidden" name="service" value="alipay.wap.create.direct.pay.by.user" /> <input type="hidden" name="partner" value="合作商户ID,需要跟支付宝申请" /> <input type="hidden" name="_input_charset" value="utf-8" /> <input type="hidden" name="sign_type" value="MD5" /> <input type="hidden" name="sign" value="78ff1f0d498b6bc1302b9fcb96e25edf" /> <input type="hidden" name="notify_url" value="http://127.0.0.1:8080/pay-front/aliPayResultNotify" /> <input type="hidden" name="return_url" value="http://127.0.0.1:8080/Service/toReturn.action" /> <input type="hidden" name="out_trade_no" value="00652016012716580911842412986916" /> <input type="hidden" name="subject" value="1" /> <input type="hidden" name="total_fee" value="0.01" /> <input type="hidden" name="seller_id" value="合作商户ID,需要跟支付宝申请" /> <input type="hidden" name="payment_type" value="1" /> <input type="hidden" name="body" value="20160127|10201601271658060548484552912431|ALIPAY-WAP|1" /> <input type="hidden" name="exter_invoke_ip" value="112.97.57.54" /> </form> <script>document.forms['alipaysubmit'].submit();</script> </body> </html> 表单提交后跳到支付宝页面: QQ截图20160314151617.png 支付成功后支付宝会根据请求参数的notify_url返回到商户后台,然后完成剩余逻辑即可。 二、及时到账 及时到账其实就是手机到账的PC网页版,不知道为什么支付宝没有把这2个整合到一起。及时到账的开发跟手机网站支付区别不大,请求参数的service有区别: 参数 参数名称 参数说明 样例 service 接口名称 接口名称 create_direct_pay_by_user 组装成HTML提交即可。 三、移动支付 移动支付是一种程序式的支付方式,在手机、掌上电脑等无线设备的应用程序内,买家可通过支付宝进行付款购买特定服务或商品,资金即时到账。 移动支付说白了就是你的移动设备安装了支付宝app,付款时在客户端上调起支付宝的支付控件,完成支付过程。 参数 参数名称 参数说明 样例 service 接口名称 接口名称 mobile.securitypay.pay Cache_6e7892108021bc7c.jpg 四、说明(遇到的坑) 关于签名: 支付宝支持的签名方式较多,MD5、RSA都支持,其中移动支付目前仅支持RSA签名方式,其他方式都支持MD5/RSA,需要说明的是,MD5签名的时候是将key按字母顺序排序,然后拼上从支付宝申请的MD5 key,组成一个新的字符串,然后签名,如: key-value如下: _input_charset=utf-8&body=20160128|10201601281559200554074030492283|ALIPAY-WAP|1&exter_invoke_ip=112.97.61.254&notify_url=http://127.0.0.1:8080/&out_trade_no=00652016012815592280050374808082&service=alipay.wap.create.direct.pay.by.user&subject=1&total_fee=0.01 假设你从支付宝申请的MD5 KEY为:aabbccddeeff 拼接上去后是: _input_charset=utf-8&body=20160128|10201601281559200554074030492283|ALIPAY-WAP|1&exter_invoke_ip=112.97.61.254&notify_url=http://127.0.0.1:8080/&out_trade_no=00652016012815592280050374808082&service=alipay.wap.create.direct.pay.by.user&subject=1&total_fee=0.01aabbccddeeff 之前做微信支付的时候定势思维,以为两者的拼接方式相同,下意识的也这么做,结果一直验签不过,微信的拼接方式是,在原来字符串的基础上加上"&key=MD5 KEY",如: _input_charset=utf-8&body=20160128|10201601281559200554074030492283|ALIPAY-WAP|1&exter_invoke_ip=112.97.61.254&notify_url=http://127.0.0.1:8080/&out_trade_no=00652016012815592280050374808082&service=alipay.wap.create.direct.pay.by.user&subject=1&total_fee=0.01&key=aabbccddeeff 然后再计算MD5。 值得注意的是,计算MD5签名难道不大,但是跟支付宝调试的时候容易出现验签不过,比较多的都是签名串与提交表单的input不同、字符编码不同……为了解决编码不同导致的验签不过,我们通常在表单头中加入: <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 移动支付中,需要对签名值进行一次URL编码,但是,在支付结果通知的报文的签名值却不需要对签名值进行URL解密,比较奇怪的地方。 try { /** * 仅需对sign 做URL编码 */ sign = URLEncoder.encode(sign, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); }
最近在接入各种支付机构,鉴于各种产品业务需求不同,具体开发实现不尽相同,不讨论具体业务实现。银联官方商家技术服务网站提供了很多可接入产品,本文仅讨论以下几种收款产品: 网关支付(和手机网页支付(WAP支付)其实一样) 手机控件支付 无跳转支付 一、网关支付&WAP支付 先讨论这两种支付方式,比较简单而且容易理解,这两种支付方式只需要我们将请求参数值设置好,签名,组装成HTML返回给前台界面[java:response.getWriter().write()]即可。HTML报文示例: <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <form id="pay_form" action="https://101.231.204.80:5000/gateway/api/frontTransReq.do" method="post"> <input type="hidden" name="txnType" id="txnType" value="01" /> <input type="hidden" name="frontUrl" id="frontUrl" value="http://127.0.0.1:8080/ACPSample_B2C/frontRcvResponse" /> <input type="hidden" name="channelType" id="channelType" value="07" /> <input type="hidden" name="currencyCode" id="currencyCode" value="156" /> <input type="hidden" name="merId" id="merId" value="777290058110048" /> <input type="hidden" name="txnSubType" id="txnSubType" value="01" /> <input type="hidden" name="txnAmt" id="txnAmt" value="10000" /> <input type="hidden" name="version" id="version" value="5.0.0" /> <input type="hidden" name="signMethod" id="signMethod" value="01" /> <input type="hidden" name="backUrl" id="backUrl" value="http://222.222.222.222:8080/ACPSample_B2C/BackRcvResponse" /> <input type="hidden" name="certId" id="certId" value="68759663125" /> <input type="hidden" name="encoding" id="encoding" value="UTF-8" /> <input type="hidden" name="bizType" id="bizType" value="000201" /> <input type="hidden" name="signature" id="signature" value="q75cUw1E90Z/3zoPLoaWwOsHoiLmw4PvD1xgUIapsxKY3tcQpHmI/Y/4oKsG3lli4DpU63EoZScTEZNjdOvorAd5+DTSmKNLECVSBxy7mRTfTVISX/jYuVuc87ogdro8GpT4sHaY0jwVjp1dWalOSQ/jfoYniAggUuhSgQtz/0dSH//R4GVa3sP22jJjHWeWUVFJi5bMNeYe57qqcdZ5Ga04rnKnGuIpIQC3I3GosKziRtRGjdo+OYFmbl28W3QwB5qohG1QIqPvwpDM6WUlVbStuEVBf/FwpZ8yuai8WXOU+GQ9kZYuRSoSDNrRR9/jmYqkwyJDEMtWsl9pehQ4Og==" /> <input type="hidden" name="orderId" id="orderId" value="20160303100902" /> <input type="hidden" name="txnTime" id="txnTime" value="20160303100902" /> <input type="hidden" name="accessType" id="accessType" value="0" /> </form> <script type="text/javascript">document.all.pay_form.submit();</script> </body> </html> 不清楚的可以试试复制下上面的代码保存为.html格式的文档,然后用浏览器打开,即可跳转到银联页面,付款成功后银联会通过你请求参数中的backUrl通知你。 二、手机控件支付 手机控件支付更简单,不用组装报文,但是流程上与网页支付不同,需要先跟银联交互一次,拿到tn(银联受理订单号),然后客户端就可以调起银联手机控件进行支付了。 三、无跳转支付 介绍无跳转支付之前,需要先了解银联的两个概念: 后台消费 前台消费。 前台消费:通过浏览器提交请求到银联的消费。后台消费:直接通过商户后台提交请求到银联完成支付,相对于前台消费,无需页面跳转。 无跳转支付就是后台支付,主要是为了方便用户完成交互过程(前提是用户银联卡已开通银联全渠道支付),用户选择银联卡/输入卡号后,向银联“获取短信”接口发送获取短信验证码请求,银联会将短信验证码发送到卡号(accNo)对应的绑定手机上,用户输入短信验证码后,后台将卡号(accNo)和短信验证码(smsCode)提交到银联,即可完成消费过程。 所以后台消费的整个过程可以理解为这样(假设卡已经开通了银联全渠道消费): 客户端/前台界面选择银行卡/输入卡号-->点击获取短信-->后台发送报文到银联获取短信验证码-->银联将短信验证码发送到卡号绑定的手机-->用户输入验证码-->点击“消费/购买”-->后台将包含卡号(accNo)和短信验证码(smsCode)的报文发送到银联-->消费完成。 ```sequence 客户端->客户端: 选择卡/输入卡号 客户端->后台:请求短信验证码 后台->银联:调用发送短信接口 note right of 银联:发送验证码到\n卡号绑定的手机 note right of 客户端:用户输入验证码 客户端->后台:消费 后台->银联:发送消费报文\n包含accNo和smsCode ``` 以上代码在stackedit可以生成此图
一级标题 语法:#一级标题 二级标题: 语法:##二级标题 三级标题: 语法:###三级标题 四级标题: 语法:###四级标题 五级标题: 语法:#####五级标题 六级标题: 语法:######六级标题 分割线: 语法:---- 斜体: 语法:*斜体* 粗体: 语法:**粗体** 无序列表一: 无序 无序 无序 语法: - 无序 - 无序 - 无序 无序列表二: 无序 无序 无序 语法: * 无序 * 无序 * 无序 有序列表: 有序1 有序2 有序3 语法: 1. 有序1 2. 有序2 3. 有序3 这里是引用,注意尖括号与文字的空格: 语法:> 这里是引用,注意尖括号与文字的空格 表格: Tables Are Cool col 3 is :----:表示居中 --: 表示居右 col 2 is centered $12 zebra stripes are neat $1 语法: | Tables | Are | Cool | | ------------ |:--------------:| -----:| | col 3 is | :----:表示居中 | --: 表示居右 | | col 2 is | centered | $12 | | zebra stripes| are neat | $1 | 链接: 百度 语法:[百度](www.baidu.com) 图片: 图片 语法: 代码框: public class TestMarkdown { public static void main(String[] args) { System.out.println("Hello Markdown!"); } } 语法: `public class TestMarkdown { public static void main(String[] args) { System.out.println("Hello Markdown!"); } }` 特殊符号: Markdown 支持以下这些符号前面加上反斜杠来帮助插入普通的符号: \ 反斜线 ` 反引号 * 星号 _ 底线 {} 花括号 [] 方括号 () 括弧 # 井字号 + 加号 - 减号 . 英文句点 ! 惊叹号 Markdown 语法说明 (简体中文版)创始人 John Gruber 的 Markdown 语法说明推荐一款客户端软件,开源绿色版,只有一个exe文件 在线编辑器mahua、stackedit(stackedit还具备图表功能,非常强大)、作业部落(中文支持较好)、dillinger(可以导出pdf,但中文支持一般)、CMD Markdown、马克飞象。 当然,直接用简书也行!