数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

导出word我们常用的是通过POI实现导出。POI最擅长的是EXCEL的操作。word操作起来样式控制还是太繁琐了。今天我们介绍下通过FREEMARK来实现word模板导出。
[TOC]
开发准备 本文实现基于springboot,所以项目中采用的都是springboot衍生的产品。首先我们在maven项目中引入freemark坐标。 org.springframework.boot spring-boot-starter-freemarker 只需要引入上面的jar包 。 前提是继承springboot坐标。就可以通过freemark进行word的导出了。
模板准备
上面是我们导出的一份模板。填写规则也很简单。只需要我们提前准备一份样本文档,然后将需要动态修改的通过 ${} 进行占位就行了。我们导出的时候提供相应的数据就行了。这里注意一下 ${c.no} 这种格式的其实是我们后期为了做集合遍历的。这里先忽略掉。后面我们会着重介绍。
开发测试 到了这一步说明我们的前期准备就已经完成了。剩下我们就通过freemark就行方法调用导出就可以了。 首先我们构建freemark加载路径。就是设置一下freemark模板路径。模板路径中存放的就是我们上面编写好的模板。只不过这里的模板不是严格意义的word.而是通过word另存为xml格式的文件。
配置加载路径 //创建配置实例 Configuration configuration = new Configuration(); //设置编码 configuration.setDefaultEncoding("UTF-8"); //ftl模板文件 configuration.setClassForTemplateLoading(OfficeUtils.class, "/template"); 获取模板类 Template template = configuration.getTemplate(templateName); 构建输出对象 Writer out = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")); 导出数据到out template.process(dataMap, out); 就上面四步骤我们就可以实现导出了。我们可以将加载配置路径的放到全局做一次。剩下也就是我们三行代码就可以搞定导出了。当然我们该做的异常捕获这些还是需要的。 点我获取源码
结果检测
功能通用化思考 上面我们只是简单介绍一下freemark导出word的流程。关于细节方面我们都没有进行深究。 细心的朋友会发现上面的图片并没有进行动态的设置。这样子功能上肯定是说不过去的。图片我们想生成我们自己设置的图片。 还有一个细节就是复选框的问题。仔细观察会发现复选框也没有字段去控制。肯定也是没有办法进行动态勾选的。 最后就是我们上面提到的就是主要安全措施那块。那块是我们的集合数据。通过模板我们是没法控制的。 上面的问题我们freemark的word模板是无法实现的。有问题其实是好事。这样我们才能进步。实际上freemark导出真正是基于ftl格式的文件的。只不过xml和ftl语法很像所以上面我们才说导出模板是xml的。实际上我们需要的ftl文件。如果是ftl文件那么上面的问题的复选框和集合都很好解决了。一个通过if标签一个通过list标签就可以解决了。图片我们还是需要通过人为去替换 <#if checkbox ??&& checkbox?seq_contains('窒息;')?string('true','false')=='true'>0052<#else>00A3 <#list c as c> dosomethings() 上面两段代码就是if 和 list语法
Dom4j实现智能化 上面ftl虽然解决了导出的功能问题。但是还是不能实现智能化。我们想做的其实想通过程序自动根据我们word的配置去进行生成ftl文件。经过百度终究还是找到了对应的方法。Dom4j就是我们最终方法。我们可以通过在word进行特殊编写。然后程序通过dom4j进行节点修改。通过dom4j我们的图片问题也就迎刃而解了。下面主要说说针对以上三个问题的具体处理细节
复选框
首先我们约定同一类型的复选框前需要 #{} 格式编写。里面就是控制复选框的字段名。 然后我们通过dom4j解析xml。我们再看看复选框原本的格式在xml中 那么我们只需要通过dom4j获取到w:sym标签。在获取到该标签后对应的文本内容即#{zhuyaoweihaiyinsu}窒息;这个内容。 匹配出字段名zhuyaoweihaiyinsu进行if标签控制内容 <#if checkbox ??&& checkbox?seq_contains('窒息')?string('true',false')=='true'>0052<#else>00A3
部分源码 Element root = document.getRootElement(); List checkList = root.selectNodes("//w:sym"); List nameList = new ArrayList<>(); Integer indext = 1; for (Element element : checkList) { Attribute aChar = element.attribute("char"); String checkBoxName = selectCheckBoxNameBySymElement(element.getParent()); aChar.setData(chooicedCheckBox(checkBoxName)); }
集合
同样的操作我们通过获取到需要改变的标签就可以了。集合和复选框不一样。集合其实是我们认为规定出来的一种格式。在word中并没有特殊标签标示。所以我们约定的格式是 ${a_b} 。首先我们通过遍历word中所以文本通过正则验证是否符合集合规范。符合我们获取到当前的行然后在行标签前添加#list标签。 然后将${a_b}修改成${a.b} 至于为什么一开始不设置a.b格式的。我这里只想说是公司文化导致的。我建议搭建如果是自己实现这一套功能的话采用a.b格式最好。
部分源码 Element root = document.getRootElement(); //需要获取所有标签内容,判断是否符合 List trList = root.selectNodes("//w:t"); //rowlist用来处理整行数据,因为符合标准的会有多列, 多列在同一行只需要处理一次。 List rowList = new ArrayList<>(); if (CollectionUtils.isEmpty(trList)) { return; } for (Element element : trList) { boolean matches = Pattern.matches(REGEX, element.getTextTrim()); if (!matches) { continue; } //符合约定的集合格式的才会走到这里 //提取出tableId 和columnId Pattern compile = Pattern.compile(REGEX); Matcher matcher = compile.matcher(element.getTextTrim()); String tableName = ""; String colName = ""; while (matcher.find()) { tableName = matcher.group(1); colName = matcher.group(2); } //此时获取的是w:t中的内容,真正需要循环的是w:t所在的w:tr,这个时候我们需要获取到当前的w:tr List ancestorTrList = element.selectNodes("ancestor::w:tr[1]"); /*List tableList = element.selectNodes("ancestor::w:tbl[1]"); System.out.println(tableList);*/ Element ancestorTr = null; if (!ancestorTrList.isEmpty()) { ancestorTr = ancestorTrList.get(0); //获取表头信息 Element titleAncestorTr = DomUtils.getInstance().selectPreElement(ancestorTr); if (!rowList.contains(ancestorTr)) { rowList.add(ancestorTr); List foreachList = ancestorTr.getParent().elements(); if (!foreachList.isEmpty()) { Integer ino = 0; Element foreach = null; for (Element elemento : foreachList) { if (ancestorTr.equals(elemento)) { //此时ancestorTr就是需要遍历的行 , 因为我们需要将此标签扩容到循环标签汇中 foreach = DocumentHelper.createElement("#list"); foreach.addAttribute("name", tableName+" as "+tableName); Element copy = ancestorTr.createCopy(); replaceLineWithPointForeach(copy); mergeCellBaseOnTableNameMap(titleAncestorTr,copy,tableName); foreach.add(copy); break; } ino++; } if (foreach != null) { foreachList.set(ino, foreach); } } } else { continue; } } }
图片
图片和复选框类似。因为在word的xml中是通过特殊标签处理的。但是我们的占位符不能通过以上占位符占位了。需要一张真实的图片进行占位。因为只有是一张图片word才会有图片标签。我们可以在图片后通过 @{imgField} 进行占位。然后通过dom4j将图片的base64字节码用${imgField}占位。
部分源码 //图片索引下表 Integer index = 1; //获取根路径 Element root = document.getRootElement(); //获取图片标签 List imgTagList = root.selectNodes("//w:binData"); for (Element element : imgTagList) { element.setText(String.format("${img%s}",index++)); //获取当前图片所在的wp标签 List wpList = element.selectNodes("ancestor::w:p"); if (CollectionUtils.isEmpty(wpList)) { throw new DomException("未知异常"); } Element imgWpElement = wpList.get(0); while (imgWpElement != null) { try { imgWpElement = DomUtils.getInstance().selectNextElement(imgWpElement); } catch (DomException de) { break; } //获取对应图片字段 List imgFiledList = imgWpElement.selectNodes("w:r/w:t"); if (CollectionUtils.isEmpty(imgFiledList)) { continue; } String imgFiled = getImgFiledTrimStr(imgFiledList); Pattern compile = Pattern.compile(REGEX); Matcher matcher = compile.matcher(imgFiled); String imgFiledStr = ""; while (matcher.find()) { imgFiledStr = matcher.group(1); boolean remove = imgWpElement.getParent().elements().remove(imgWpElement); System.out.println(remove); } if (StringUtils.isNotEmpty(imgFiledStr)) { element.setText(String.format("${%s}",imgFiledStr)); break; } } }
基于word自动化导出(含源码) 以上就是我们实现导出的流程。通过上面的逻辑我们最终可以一套代码复用了。源码下载地址: https://gitee.com/zxhTom/office-multip.git
参考网络文章
dom操作xml dom生成xml httpclient获取反应流 获取jar路径 itext实现套打 ftl常见语法 freemark官网 ftl判断非空 freemark自定义函数 freemark自定义函数java freemark特殊字符转义 java实现word转xml各种格式
加入战队
# 加入战队
微信公众号
软件开发
2020-05-25 09:49:00
SpringMVC的运行流程
1、用户发送请求至前端控制器DispatcherServlet
2、DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
4、DispatcherServlet通过HandlerAdapter处理器适配器调用处理器
5、执行处理器(Controller,也叫后端控制器)。
6、Controller执行完成返回ModelAndView
7、HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
8、DispatcherServlet将ModelAndView传给ViewReslover视图解析器
9、ViewReslover解析后返回具体View
10、DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)。
11、DispatcherServlet响应用户。

Servlet 生命周期 :Servlet 加载—>实例化—>服务—>销毁。
init():在Servlet的生命周期中,仅执行一次init()方法。它是在服务器装入Servlet时执行的,负责初始化Servlet对象。可以配置服务器,以在启动服务器或客户机首次访问Servlet时装入Servlet。无论有多少客户机访问Servlet,都不会重复执行init()。
service():它是Servlet的核心,负责响应客户的请求。每当一个客户请求一个HttpServlet对象,该对象的Service()方法就要调用,而且传递给这个方法一个“请求”(ServletRequest)对象和一个“响应”(ServletResponse)对象作为参数。在HttpServlet中已存在Service()方法。默认的服务功能是调用与HTTP请求的方法相应的do功能。
destroy():仅执行一次,在服务器端停止且卸载Servlet时执行该方法。当Servlet对象退出生命周期时,负责释放占用的资源。一个Servlet在运行service()方法时可能会产生其他的线程,因此需要确认在调用destroy()方法时,这些线程已经终止或完成。


手写springmvc的大致方向:
1、首先定义一个DispatcherServlet,拦截所有的请求。
2、搞一个初始化的方法,只执行一次,用于获取前包下所有的类,将类的信息存放到集合中去,也就是打一些注解啥的。
3、初始化一个HandlerMapping,将url和类的方法关联上去,类的方法就是步骤2上面扫包获取到的信息。
4、将拦截到的请求,解析url然后对应到对应的方法进行执行操作就完事了。

软件开发
2020-05-24 16:46:00
前期回顾:
JDK1.8新特性(一):JDK1.8究竟有哪些新特性呢
JDK1.8新特性(二):为什么要关注JDK1.8
JDK1.8新特性(三):Lambda表达式,让你爱不释手

上一篇 JDK1.8新特性(三):Lambda表达式,让你爱不释手 ,只是简单的讲到Lambda表达式的语法、使用,使得你对它产生了好感,而Lambda表达式是如何实现、定义,你可能不太清楚。本篇将会详细介绍 函数式接口 ,让你在使用JDK新特性时,做到心中有数,自信满满。

一、函数式接口 函数式接口( functional Interface ), 有且仅有一个抽象方法的接口 ,但可以有多个非抽象的方法。 适用于Lambda表达式使用的接口。如创建线程: new Thread (() - > System .out .println ( Thread .currentThread () .getName ())) .start (); 其中,Lambda表达式代替了 new Runnable() ,这里的 Runable 接口就属于函数式接口,最直观的体现是使用了 @FunctionalInterface 注解,而且使用了一个抽象方法(有且仅有一个),如下:
package java.lang;
/** * The Runnable interface should be implemented by any * class whose instances are intended to be executed by a thread. The * class must define a method of no arguments called run. *

* This interface is designed to provide a common protocol for objects that * wish to execute code while they are active. For example, * Runnable is implemented by class Thread. * Being active simply means that a thread has been started and has not * yet been stopped. *

* In addition, Runnable provides the means for a class to be * active while not subclassing Thread. A class that implements * Runnable can run without subclassing Thread * by instantiating a Thread instance and passing itself in * as the target. In most cases, the Runnable interface should * be used if you are only planning to override the run() * method and no other Thread methods. * This is important because classes should not be subclassed * unless the programmer intends on modifying or enhancing the fundamental * behavior of the class. * * @author Arthur van Hoff * @see java.lang.Thread * @see java.util.concurrent.Callable * @since JDK1.0 */ @FunctionalInterface public interface Runnable { /** * When an object implementing interface Runnable is used * to create a thread, starting the thread causes the object's * run method to be called in that separately executing * thread. *

* The general contract of the method run is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run () ; }
1. 格式
修饰符 interface 接口名 {
public abstract 返回值类型 方法名 (可选参数列表);
} 注: public abstract 可以省略(因为默认修饰为 public abstract ) 如: public interface MyFunctionalInterface { public abstract void method ( ) ; }
2. 注解@FunctionalInterface
@FunctionalInterface ,是JDK1.8中新引入的一个注解,专门指代函数式接口,用于一个接口的定义上。 和 @Override 注解的作用类似, @FunctionalInterface 注解可以用来检测接口是否是函数式接口。如果是函数式接口,则编译成功,否则编译失败(接口中没有抽象方法或者抽象方法的个数多余1个)。 package com.xcbeyond.study.jdk8.functional;
/** * 函数式接口 * @Auther : xcbeyond * @Date : 2020/5/17 0017 0:26 */ @FunctionalInterface public interface MyFunctionalInterface { public abstract void method () ;
// 如果存在多个抽象方法,则编译失败,即:@FunctionalInterface飘红 // public abstract void method1(); }
3. 实例 函数式接口: package com.xcbeyond.study.jdk8.functional;
/** * 函数式接口 * @Auther : xcbeyond * @Date : 2020/5/17 0017 0:26 */ @FunctionalInterface public interface MyFunctionalInterface { public abstract void method () ;
// 如果存在多个抽象方法,则编译失败,即:@FunctionalInterface飘红 // public abstract void method1(); }
测试:
package com.xcbeyond.study.jdk8.functional;
/** * 测试函数式接口 * @Auther : xcbeyond * @Date : 2020/5/17 0017 0:47 */ public class MyFunctionalInterfaceTest {
public static void main (String[] args) { // 调用show方法,参数中有函数式接口MyFunctionalInterface,所以可以使用Lambda表达式,来完成接口的实现 show( "hello xcbeyond!" , msg -> System.out.printf(msg)); }
/** * 定义一个方法,参数使用函数式接口MyFunctionalInterface * @param myFunctionalInterface */ public static void show (String message, MyFunctionalInterface myFunctionalInterface) { myFunctionalInterface.method(message); } } 函数式接口,用起来是不是更加的灵活,可以在具体调用处进行接口的实现。
函数式接口,可以很友好地支持Lambda表达式。

二、常用的函数式接口 在JDK1.8之前已经有了大量的函数式接口,最熟悉的就是 java.lang.Runnable 接口了。 JDK 1.8 之前已有的函数式接口: java.lang.Runnable java.util.concurrent.Callable java.security.PrivilegedAction java.util.Comparator java.io.FileFilter java.nio.file.PathMatcher java.lang.reflect.InvocationHandler java.beans.PropertyChangeListener java.awt.event.ActionListener javax.swing.event.ChangeListener 而在JDK1.8新增了 java.util.function 包下的很多函数式接口,用来支持Java的函数式编程,从而丰富了Lambda表达式的使用场景。 这里主要介绍四大核心函数式接口: java.util.function.Consumer :消费型接口 java.util.function.Supplier :供给型接口 java.util.function.Predicate :断定型接口 java.util.function.Function :函数型接口
1. Consumer接口 java.util.function.Consumer 接口,是一个消费型的接口,消费数据类型由泛型决定。 package java.util.function;
import java.util.Objects;
/** * Represents an operation that accepts a single input argument and returns no * result. Unlike most other functional interfaces, { @code Consumer} is expected * to operate via side-effects. * *

This is a functional interface * whose functional method is { @link #accept(Object)}. * * @param the type of the input to the operation * * @since 1.8 */ @FunctionalInterface public interface Consumer < T > {
/** * Performs this operation on the given argument. * * @param t the input argument */ void accept (T t) ;
/** * Returns a composed { @code Consumer} that performs, in sequence, this * operation followed by the { @code after} operation. If performing either * operation throws an exception, it is relayed to the caller of the * composed operation. If performing this operation throws an exception, * the { @code after} operation will not be performed. * * @param after the operation to perform after this operation * @return a composed { @code Consumer} that performs in sequence this * operation followed by the { @code after} operation * @throws NullPointerException if { @code after} is null */ default Consumer andThen (Consumer after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } }
(1)抽象方法:accept
Consumer 接口中的抽象方法 void accept(T t) ,用于消费一个指定泛型T的数据。 举例如下: /** * 测试void accept(T t) */ @Test public void acceptMethodTest() { acceptMethod( "xcbeyond" , message -> { // 完成字符串的处理,即:通过Consumer接口的accept方法进行对应数据类型(泛型)的消费 String reverse = new StringBuffer(message).reverse().toString(); System.out.printf(reverse); }); }
/** * 定义一个方法,用于消费message字符串 * @param message * @param consumer */ public void acceptMethod( String message, Consumer< String > consumer) { consumer.accept(message); }
(2)方法:andThen
方法 andThen ,可以用来将多个 Consumer 接口连接到一起,完成数据消费。 /** * Returns a composed { @code Consumer} that performs, in sequence, this * operation followed by the { @code after} operation. If performing either * operation throws an exception, it is relayed to the caller of the * composed operation. If performing this operation throws an exception, * the { @code after} operation will not be performed. * * @param after the operation to perform after this operation * @return a composed { @code Consumer} that performs in sequence this * operation followed by the { @code after} operation * @throws NullPointerException if { @code after} is null */ default Consumer andThen (Consumer after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } 举例如下:
/** * 测试Consumer andThen(Consumer after) * 输出结果: * XCBEYOND * xcbeyond */ @ Test public void andThenMethodTest ( ) { andThenMethod( "XCbeyond" , t -> { // 转换为大小输出 System. out .println(t.toUpperCase()); }, t -> { // 转换为小写输出 System. out .println(t.toLowerCase()); }); }
/** * 定义一个方法,将两个Consumer接口连接到一起,进行消费 * @param message * @param consumer1 * @param consumer2 */ public void andThenMethod ( String message, Consumer consumer1, Consumer consumer2 ) { consumer1.andThen(consumer2).accept(message); }
2. Supplier接口 java.util.function.Supplier 接口,是一个供给型接口,即:生产型接口。只包含一个无参方法: T get() ,用来获取一个泛型参数指定类型的数据。 package java.util.function;
/** * Represents a supplier of results. * *

There is no requirement that a new or distinct result be returned each * time the supplier is invoked. * *

This is a functional interface * whose functional method is { @link #get()}. * * @param the type of results supplied by this supplier * * @since 1.8 */ @FunctionalInterface public interface Supplier < T > {
/** * Gets a result. * * @return a result */ T get () ; } 举例如下:
@ Test public void test ( ) { String str = getMethod(() -> "hello world!" ); System. out .println(str); }
public String getMethod ( Supplier supplier ) { return supplier. get (); }
3. Predicate接口 java.util.function.Predicate 接口,是一个断定型接口,用于对指定类型的数据进行判断,从而得到一个判断结果( boolean 类型的值)。 package java.util.function;
import java.util.Objects;
/** * Represents a predicate (boolean-valued function) of one argument. * *

This is a functional interface * whose functional method is { @link #test(Object)}. * * @param the type of the input to the predicate * * @since 1.8 */ @FunctionalInterface public interface Predicate < T > {
/** * Evaluates this predicate on the given argument. * * @param t the input argument * @return { @code true} if the input argument matches the predicate, * otherwise { @code false} */ boolean test (T t) ;
/** * Returns a composed predicate that represents a short-circuiting logical * AND of this predicate and another. When evaluating the composed * predicate, if this predicate is { @code false}, then the { @code other} * predicate is not evaluated. * *

Any exceptions thrown during evaluation of either predicate are relayed * to the caller; if evaluation of this predicate throws an exception, the * { @code other} predicate will not be evaluated. * * @param other a predicate that will be logically-ANDed with this * predicate * @return a composed predicate that represents the short-circuiting logical * AND of this predicate and the { @code other} predicate * @throws NullPointerException if other is null */ default Predicate and (Predicate other) { Objects.requireNonNull(other); return (t) -> test(t) && other.test(t); }
/** * Returns a predicate that represents the logical negation of this * predicate. * * @return a predicate that represents the logical negation of this * predicate */ default Predicate negate () { return (t) -> !test(t); }
/** * Returns a composed predicate that represents a short-circuiting logical * OR of this predicate and another. When evaluating the composed * predicate, if this predicate is { @code true}, then the { @code other} * predicate is not evaluated. * *

Any exceptions thrown during evaluation of either predicate are relayed * to the caller; if evaluation of this predicate throws an exception, the * { @code other} predicate will not be evaluated. * * @param other a predicate that will be logically-ORed with this * predicate * @return a composed predicate that represents the short-circuiting logical * OR of this predicate and the { @code other} predicate * @throws NullPointerException if other is null */ default Predicate or (Predicate other) { Objects.requireNonNull(other); return (t) -> test(t) || other.test(t); }
/** * Returns a predicate that tests if two arguments are equal according * to { @link Objects#equals(Object, Object)}. * * @param the type of arguments to the predicate * @param targetRef the object reference with which to compare for equality, * which may be { @code null} * @return a predicate that tests if two arguments are equal according * to { @link Objects#equals(Object, Object)} */ static Predicate isEqual (Object targetRef) { return ( null == targetRef) ? Objects::isNull : object -> targetRef.equals(object); } }
(1)抽象方法:test
抽象方法 boolean test(T t) ,用于条件判断。 /** * Evaluates this predicate on the given argument. * * @param t the input argument * @return { @code true} if the input argument matches the predicate, * otherwise { @code false} */ boolean test (T t) ; 举例如下:
/** * 测试boolean test(T t); */ @Test public void testMethodTest () { String str = "xcbey0nd" ; boolean result = testMethod(str, s -> s.equals( "xcbeyond" )); System.out.println(result); }
/** * 定义一个方法,用于字符串的判断。 * @param str * @param predicate * @return */ public boolean testMethod (String str, Predicate predicate) { return predicate.test(str); }
(2)方法:and
方法 Predicate and(Predicate other) ,用于将两个 Predicate 进行逻辑”与“判断。 /** * Returns a composed predicate that represents a short-circuiting logical * AND of this predicate and another. When evaluating the composed * predicate, if this predicate is { @code false}, then the { @code other} * predicate is not evaluated. * *

Any exceptions thrown during evaluation of either predicate are relayed * to the caller; if evaluation of this predicate throws an exception, the * { @code other} predicate will not be evaluated. * * @param other a predicate that will be logically-ANDed with this * predicate * @return a composed predicate that represents the short-circuiting logical * AND of this predicate and the { @code other} predicate * @throws NullPointerException if other is null */ default Predicate and (Predicate other) { Objects.requireNonNull(other); return (t) -> test(t) && other.test(t); }
(3)方法:negate
方法 Predicate negate() ,用于取反判断。 /** * Returns a predicate that represents the logical negation of this * predicate. * * @return a predicate that represents the logical negation of this * predicate */ default Predicate negate ( ) { return (t) -> !test(t); }
(4)方法:or 方法 Predicate or(Predicate other) ,用于两个Predicate的逻辑”或“判断。 /** * Returns a composed predicate that represents a short-circuiting logical * OR of this predicate and another. When evaluating the composed * predicate, if this predicate is { @code true}, then the { @code other} * predicate is not evaluated. * *

Any exceptions thrown during evaluation of either predicate are relayed * to the caller; if evaluation of this predicate throws an exception, the * { @code other} predicate will not be evaluated. * * @param other a predicate that will be logically-ORed with this * predicate * @return a composed predicate that represents the short-circuiting logical * OR of this predicate and the { @code other} predicate * @throws NullPointerException if other is null */ default Predicate or (Predicate other) { Objects.requireNonNull(other); return (t) -> test(t) || other.test(t); }
4. Function接口 java.util.function.Function 接口,是一个函数型接口,用来根据一个类型的数据得到另外一个类型的数据。 package java.util.function;
import java.util.Objects;
/** * Represents a function that accepts one argument and produces a result. * *

This is a functional interface * whose functional method is { @link #apply(Object)}. * * @param the type of the input to the function * @param the type of the result of the function * * @since 1.8 */ @FunctionalInterface public interface Function < T , R > {
/** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply(T t);
/** * Returns a composed function that first applies the { @code before} * function to its input, and then applies this function to the result. * If evaluation of either function throws an exception, it is relayed to * the caller of the composed function. * * @param the type of input to the { @code before} function, and to the * composed function * @param before the function to apply before this function is applied * @return a composed function that first applies the { @code before} * function and then applies this function * @throws NullPointerException if before is null * * @see #andThen(Function) */ default Function < V , R > compose (Function before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); }
/** * Returns a composed function that first applies this function to * its input, and then applies the { @code after} function to the result. * If evaluation of either function throws an exception, it is relayed to * the caller of the composed function. * * @param the type of output of the { @code after} function, and of the * composed function * @param after the function to apply after this function is applied * @return a composed function that first applies this function and then * applies the { @code after} function * @throws NullPointerException if after is null * * @see #compose(Function) */ default Function < T , V > andThen (Function after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); }
/** * Returns a function that always returns its input argument. * * @param the type of the input and output objects to the function * @return a function that always returns its input argument */ static Function < T , T > identity () { return t -> t; } }
(1)抽象方法:apply
抽象方法 R apply(T t) ,根据类型T的参数获取类型R的结果。 /** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply (T t) ; 举例如下: /** * 测试R apply(T t),完成字符串整数的转换 */ @Test public void applyMethodTest() { // 字符串类型的整数 String numStr = "123456" ; Integer num = applyMethod(numStr, n -> Integer.parseInt(n)); System.out.println(num); }
public Integer applyMethod( String str, Function < String , Integer> function ) { return function . apply ( str ) ; }
(2)方法:compose 方法 Function compose(Function before) ,获取 apply 的 function 。 /** * Returns a composed function that first applies the { @code before} * function to its input, and then applies this function to the result. * If evaluation of either function throws an exception, it is relayed to * the caller of the composed function. * * @param the type of input to the { @code before} function, and to the * composed function * @param before the function to apply before this function is applied * @return a composed function that first applies the { @code before} * function and then applies this function * @throws NullPointerException if before is null * * @see #andThen(Function) */ default Function < V , R > compose (Function before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); }
(3)方法:andThen
方法 Function andThen(Function after) ,用来进行组合操作,即:”先做什么,再做什么“的场景。 /** * Returns a composed function that first applies this function to * its input, and then applies the { @code after} function to the result. * If evaluation of either function throws an exception, it is relayed to * the caller of the composed function. * * @param the type of output of the { @code after} function, and of the * composed function * @param after the function to apply after this function is applied * @return a composed function that first applies this function and then * applies the { @code after} function * @throws NullPointerException if after is null * * @see #compose(Function) */ default Function < T , V > andThen (Function after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); }

三、函数式编程 函数式编程并不是Java提出的新概念,它将计算机运算看作是函数的计算。 函数式编程最重要的基础是λ演算,而且λ演算的函数是可以接受函数当作输入(参数)和输出(返回值)的。 和指令式编程相比,函数式编程强调函数的计算比指令的执行重要。 和过程化编程相比,函数式编程里函数的计算可随时调用。 当然,Java大家都知道是面向对象的编程语言,一切都是基于对象的特性(抽象、封装、继承、多态)。在JDK1.8出现之前,我们关注的往往是某一对象应该具有什么样的属性,当然这也就是面向对象的核心——对数据进行抽象。但JDK1.8出现以后,这一点开始出现变化,似乎在某种场景下,更加关注某一类共有的行为(有点类似接口),这也就是JDK1.8提出函数式编程的目的。如下图所示,展示了面向对象编程到函数式编程的变化。 Lambda表达式就是更好的体现了函数式编程,而为了支持Lambda表达式,才有了函数式接口。 另外,为了在面对大型数据集合时,为了能够更加高效的开发,编写的代码更加易于维护,更加容易运行在多核CPU上,java在语言层面增加了Lambda表达式。在上一节 JDK1.8新特性(三):Lambda表达式,让你爱不释手 中,我们已经知道Lambda表达式是多么的好用了 。 在JDK1.8中,函数式编程随处可见,在你使用过程中简直很爽,例如:Stream流。 函数式编程的优点,也很多,如下:
1. 代码简洁,开发快速
函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。
2. 接近自然语言,易于理解 函数式编程的自由度很高,可以写出很接近自然语言的代码。 例如,两数只差,可以写成 (x, y) -> x – y
3. 更方便的代码管理 函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。
4. 易于"并发编程" 函数式编程不需要考虑"死锁",因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"。
5. 代码的热升级 函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。

四、总结 在JDK1.8中,函数式接口/编程将会随处可见,也有有助于你更好的理解JDK1.8中的一些新特性。关于函数式接口,在接下来具体特性、用法中将会体现的淋漓尽致。 JDK1.8提出的函数式接口,你是否赞同呢?

参考资料: 1. https://www.cnblogs.com/Dorae/p/7769868.html
2. https://blog.csdn.net/stormkai/article/details/94364233
3. https://baike.baidu.com/item/ 函数式编程

MySQL性能优化(一):MySQL架构与核心问题 MySQL性能优化(二):选择优化的数据类型 MySQL性能优化(三):深入理解索引的这点事 MySQL性能优化(四):如何高效正确的使用索引 MySQL性能优化(五):为什么查询速度这么慢 MySQL性能优化(六):常见优化SQL的技巧 MySQL性能优化(七):MySQL执行计划,真的很重要,来一起学习吧
微服务架构下的核心话题 (一):微服务架构下各类项目的顺势崛起 微服务架构下的核心话题 (二):微服务架构的设计原则和核心话题 微服务架构下的核心话题 (三):微服务架构的技术选型
喜欢就点个 "在看" 呗,留言、转发朋友圈
本文分享自微信公众号 - 程序猿技术大咖(cxyjsdk)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“ OSC源创计划 ”,欢迎正在阅读的你也加入,一起分享。

软件开发
2020-05-24 00:52:00
Fecmall【 免费商用 】在线出售电子书籍PDF以及实物书籍的B2C商城
Fecmall开源免费公告:【 免费商用】在线出售电子书籍PDF以及实物书籍的B2C商城
Fecbvc PDF电子书籍商城系统, 是针对PDF电子书籍等虚拟产品,在线出售,在fecyo单商户商城系统的基础上,以扩展形式开发的PDF电子书籍系统。
应用市场地址: http://addons.fecmall.com/84991566
详细参看: http://www.fecmall.com/fecbvc
Fecbvc PC演示地址 http://fecbvc.fecshop.com/cn/
Fecbvc html5演示地址 http://fecbvch5.fecshop.com/cn/
Fecbvc文档地址: http://www.fecmall.com/doc/fecmall-guide/fecbvc/cn-1.0/guide-README.html
软件开发
2020-05-23 12:10:00
简介
Maven是java中非常有用和常用的构建工具,基本上现在大型的java项目都是Maven和gradle的天下了。
因为JDK的版本现在以每半年一次的速度在飞速发展。不同的JDK版本就有不同的java路径,我们在使用Maven的过程中,可能经常会需要切换JDK的版本。
一般来说我们可以在maven-compiler-plugin中配置好executable的路径。如下所示: 更多内容请访问 www.flydean.com maven-compiler-plugin true /usr/bin/javac14
看起来还不错,但是如果想切换executable的路径可能就比较麻烦。更有问题的是,如果你是团队来发,一个人在mac环境一个人在windows环境,两边的executable的路径完全是不同的,这会导致代码冲突,和代码难以维护。
Toolchains的介绍
为了解决这个问题,Maven为我们推出了Toolchains。使用Toolchains,我们可以将这些可执行文件的路径,版本号,还有类型都定义在一个toolchains.xml文件里面。
而在pom.xml文件中只需要引用toolchains.xml中定义的别名就可以了。
针对上面的windows和linux路径不一致的问题,我们可以保证pom.xml是完全一致的,大家只需要适配自己的toolchains.xml文件即可。
Toolchains的例子
Toolchains是和pom中其他的plugin结合起来使用的,比如最常用的maven-compiler-plugin。
下面我们举一个例子来说明。首先定义toolchains.xml文件,这个文件最好放在${user.home}/.m2/中。 jdk 14 oracle /path/to/jdk/14 jdk 11 oracle /path/to/jdk/11
上面的例子中,我们定义了2个JDK的toolchains。一个JDK14,一个JDK11。下面看下怎么在pom文件中使用。 ... org.apache.maven.plugins maven-compiler-plugin 3.1 org.apache.maven.plugins maven-toolchains-plugin 1.1 toolchain 14 oracle ...
上面的pom配置文件中,我们通过简单的引用toolchains中的定义,即可无缝的进行JDK版本的切换。
Toolchains支持
Toolchains需要Maven 2.0.9以上版本的支持。
Toolchains是需要和pom中的plugin一起使用的,下面的图中列出了toolchains支持的plugin名字和最低的版本要求。
总结
本文介绍了Apache Maven中toolchain的使用,希望大家能够在实际工作中用起来。
更多精彩内容且看: 区块链从入门到放弃系列教程-涵盖密码学,超级账本,以太坊,Libra,比特币等持续更新 Spring Boot 2.X系列教程:七天从无到有掌握Spring Boot-持续更新 Spring 5.X系列教程:满足你对Spring5的一切想象-持续更新 java程序员从小工到专家成神之路(2020版)-持续更新中,附详细文章教程 本文作者:flydean程序那些事
本文链接: http://www.flydean.com/apache-maven-toolchains/
本文来源:flydean的博客
欢迎关注我的公众号:程序那些事,更多精彩等着您!
软件开发
2020-05-23 06:51:00
一、责任链模式的概念 责任链模式是一种对象的行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。
二、什么时候使用责任链模式
根据概念理解,责任链模式是一种可以处理一次请求流的一种模式,类似业务工作流程,而且责任链可以动态的调整流程中的各个环节。
也就是说,如果业务里有这种需要逐级处理的业务,可是尝试使用责任链模式。
我在项目中,一般需要逐级做业务判断的时候会使用工作流模式,例如:
http请求拦截器,先判断HttpHeader的协议是否正确,在判断登录信息是否正确,在判断请求内容是否正确等,这几步判断就可以用责任链模式实现;
从代码结构角度,可以解决嵌套if的问题,例如: if (验证协议) { .... if (验证鉴权) { .... if (验证数据) { .... } } }
可以避免这种嵌套if结构,提升代码的可读性。下面来试着用Java代码实现责任链模式。
三、怎么使用责任链模式
3.1 实现方式
就以请求拦截器为例,模拟做一个责任链模式,一共三步验证: 验证协议是否正确; 验证登录信息是否正确; 验证请求数据是否正确;
下面是类图和代码:
// 责任链模式的抽象类 public abstract class Validator { // 设置责任链的下一个环节 private Validator next; public Validator setNext(Validator next) { this.next = next; // 返回下一个环节,做到链式设置 return next; } // 开始责任链的处理 public final void handler(List context) { if (!resolve(context)) { // 判断处理结果是否失败,如果失败则不向下进行 fail(context); } else if (next != null) { // 如果本环节处理成功,判断是否还有下一环节,如果就,就执行下一个环节的handler next.handler(context); } else { // 如果处理成功,切没有下一个环节了,责任链结束 success(context); } } // 具体处理业务的方法,定义一个抽象方法等待被子类实现(类似模板方法模式) protected abstract boolean resolve(List context); // 成功通过的处理 protected void success(List context) { System.out.println("验证通过。"); } // 未成功通过的处理 protected void fail(List context) { System.out.println("验证失败。"); } } // 验证鉴权的处理类 public class AuthValidator extends Validator { public boolean resolve(List context) { return context.get(1).equals("admin"); } } // 验证协议的处理类 public class ProtocolValidator extends Validator { public boolean resolve(List context) { return context.get(0).equals("http"); } } // 验证数据的处理类 public class DataSecurityValidator extends Validator { public boolean resolve(List context) { return context.get(2).equals("hello"); } }
上面是责任链模式的类图和代码,从代码上看,抽象类定义了责任链的创建方法,和循环调用方法,但具体的处理,需要有子类的实现(类似模板方法),然后在成功或者失败的时候,进行相关处理,责任链模式的使用方式如下: public class Client { public static void main(String[] args) { // 因为是测试,所以在此处配置了责任链的关系,正常情况下,Client是不需要知道责任链的配置关系的 Validator protocolValidator = new ProtocolValidator(); Validator authValidator = new AuthValidator(); Validator dataSecurityValidator = new DataSecurityValidator(); protocolValidator.setNext(authValidator).setNext(dataSecurityValidator); // 处理,模拟验证通过 List params1 = new ArrayList(); params1.add("http"); params1.add("admin"); params1.add("hello"); protocolValidator.handler(params1); // 处理,模拟验证失败 List params2 = new ArrayList(); params2.add("http"); params2.add("ss"); params2.add("hello"); protocolValidator.handler(params2); } }
先将几个环节通过setNext建立起关联,然后从某个节点(通常是第一个节点)开始调用handler,进行业务的处理。
以上就是责任链模式的Java代码实现。
3.2 责任链模式的好处
责任链模式的好处如下: 可读性高:将嵌套判断的代码块用面向对象的方式去优化,通过良好的命名起到语义化的效果; 灵活性强:可以随意通过setNext配置各个判断环节的前后关系,而不需要修改代码,嵌套if是做不到的; 扩展性强:如后续有需求变更,可以快读的加入新的责任链环节,而无需修改原有代码,符合开闭原则;
四、总结
责任链模式,面相的业务场景相对比较单一,比较好确定在什么业务场景下使用。但是实现的代码比起其他的设计模式,稍微复杂了一点,但也比较好理解。
还有就是,责任链模式的一个隐藏的优点是,它弱化了调用者(Client)和处理者(Validator)之前的关联,调用者其实无需知道责任链的每个环节的关系,只是去调用责任链的hanlder方法即可,这种解除耦合的优点很重要。
以上就是我对责任链模式的一些理解,有不足之处请大家指出,谢谢。
软件开发
2020-05-23 00:30:00
1 Stream简介
Stream是数据渠道,用于操作数据源(集合,数组等)所生成得元素序列。而集合讲得是数据,流讲得是计算。
注意: Stream 自己不会存储元素。 Stream 不会改变源对象。相反,它会返回一个持有结果得新Stream Stream 操作时延迟执行得,这意味着它们会等到需要结果时才执行。(延迟加载)
Stream 操作步骤 Stream 创建: 一个数据源(集合,数组),获取一个流。 Stream 中间操作: 一个中间操作链,对数据源的数据进行处理。 Stream 终止操作: 一个终止操作,执行中间操作链,并产生结果。
2 Stream 用法
2.1 创建Stream //1. 通过 Collection.stream() / parallelStream() 创建Stream List list = new ArrayList(); Stream stream11 = list.stream(); // 串行流 Stream stream12 = list.parallelStream(); // 并行流 //2. 通过 Arrays.stream() 获取数组流 IntStream stream2 = Arrays.stream(new int[]{1,2}); // 串行流 //3. 通过 Stream.of() 获取流 Stream stream3 = Stream.of("123", "456"); // 串行流 //4. 创建无限流,需要配合 limit() 截断,不然无限制下去 Stream stream41 = Stream.iterate(2, (x) -> x * 2); // 串行流 Stream stream42 = Stream.generate(Math::random); // 串行流
2.2 Stream 中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何得处理。而终止操作时一次性全部处理,称为‘延迟加载’
中间操作(例举部分) 说明 limit(long maxSize) 截断,使其元素不超过给定数量
filter(Predicate predicate) 过滤,从流中过滤出想要的元素
skip(long n) 忽略,跳过前n个元素,若流中元素不足n个,则返回空
distinct() 去重,通过元素 hashCode() 和 equals() 去除重复元素
map(Funcation mapper) 映射,函数会被应用到每个元素上,并将其映射成一个新的元素
flatMap(Function mapper) 映射,将流中的每个值都换成一个流,然后把所有流连接成一个流
sorted()
sorted(Comparator comparator)
排序,自然排序
排序,定制排序
// 中间操作:不会执行任何操作 Stream stream = Stream.generate(Math::random) // double 无限流 .limit(20) // 截断,取前 20 个 .filter(x -> x > 0.3) // 过滤,取大于 0.3 的元素 .skip(1) // 忽略,丢弃第一个元素 .distinct() // 去重 .map(x -> x * 10) // 映射,将每个元素扩大 10 倍 .sorted(); // 对 double 流进行排序 // 终止操作,只有执行终止操作才会执行全部。即:延迟加载 stream.forEach(System.out::println); // 中间操作:flatMap 接收一个函数作为参数,将流中的每个值都换成一个流,然后把所有流连接成一个流 List list = Arrays.asList("aaa", "bbb", "ccc", "ddd"); list.stream().flatMap((e) -> filterCharacter(e)).forEach(System.out::println); //如果使用map则需要这样写 list.stream().map((e) -> filterCharacter(e)).forEach((e) -> { e.forEach(System.out::println); }); public Stream filterCharacter(String str){ List list = new ArrayList<>(); for (Character ch : str.toCharArray()) { list.add(ch); } return list.stream(); }
2.3 Stream 终止操作
2.3.1 查找与匹配
操作(例举部分) 说明
allMatch(Predicate predicate) 检查是否匹配所有元素
anyMatch(Predicate predicate) 检查是否至少匹配所有元素
noneMatch(Predicate predicate) 检查是否没有匹配所有元素
findFirst() 返回第一个元素
findAny() 返回当前流中任意元素
count() 返回流中元素总个数
max(Comparator comparator)
min(Comparator comparator)
返回流中最大值
返回流中最小值
2.3.2 规约 - 将流中元素结合在一起,返回一个值
操作(例举部分) 说明
reduce(T identitty,BinaryOperator) reduce(BinaryOperator
需要传一个起始值,然后,传入的是一个二元运算 没有起始值,有可能结果为空,所以返回的值会被封装到Optional中
// 求和 List list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Integer sum = list.stream().reduce(0, (x, y) -> x + y); // 求和,没有起始值,则有可能结果为空,所以返回的值会被封装到Optional中 List list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Optional sum = list.stream().reduce(Integer :: sum);
2.3.3 收集
将流转换为其他形式。接收一个Collector接口的实现,用于给Stream中元素做汇总的方法。Collector接口方法的实现决定了如何对流执行收集操作(如收集到List,Set,Map)。但是Collectors实用类提供了很多静态方法,可以方便地创建常见得收集器实例。
操作(例举部分) 说明 Collectors.toList() 将流转换成List
Collectors.toSet() 将流转换为Set
Collectors.toCollection(Supplier supplier) 将流转换为其他类型的集合
Collectors.counting() 元素个数
Collectors.averagingInt/Long/Double(Function function) 平均数,不同之处在于传入得参数类型不同,返回值都为Double
Collectors.summingInt/Long/Double(Function function) 求和,不同之处在于传入得参数类型不同,返回值为Integer, Double, Long
Collectors.maxBy(Comparator comparator) 最大值
Collectors.minBy(Comparator comparator) 最小值
Collectors.groupingBy(Function function)
Collectors.partitioningBy(Predicate predicate)
分组,返回Map
分区,传入函数返回true和false 分成两个区,返回Map
3 并行流
并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。Java8中将并行流进行了优化,我们可以很容易的对数据进行并行操作。Stream API可以声明性地通过parallel()与scqucntial()在并行流与顺序流之间进行切换。
3.1 Fork-Join 框架
Fork—Join框架:是java7提供得一个用于执行任务得框架,就是在必要得情况下,将一个大任务,进行拆分(Fork)成若干个小任务(拆分到不能再拆分),再将一个个的小任务运算得结果进行join汇总。
Fork—Join框架时ExecutorService接口得一种具体实现,目的是为了帮助更好地利用多处理器带来得好处。它是为那些能够被递归地拆分成子任务的工作类型量身设计的。起目的在于能够使用所有有可用的运算能力来提升你的应用的性能。
关于 Fork-Join 实现原理请看这篇:图解Fork/Join https://mp.weixin.qq.com/s/OzZFGW_8GBYHUa0Ef10WVg /** * 要想使用Fark—Join,类必须继承RecursiveAction(无返回值)或者 RecursiveTask(有返回值) * * 计算从 start 到 end 的数字累加 */ public class ForkJoin extends RecursiveTask { private long start; // 起始数字 private long end; // 结束数字 public ForkJoin(long start, long end) { this.start = start; this.end = end; } // 拆分的最小区间 private static final long THRESHOLD = 10000L; @Override protected Long compute() { // 当区间小于最小区间时,直接计算累加 if (end - start <= THRESHOLD) { long sum = 0; for (long i = start; i < end; i++) { sum += i; } return sum; } else { // 否则,将区间一分为二,分给两个不同的线程去计算 // 注意这里,如果有问题,会抛出java.lang.NoClassDefFoundError: Could not initialize class java.util.concurrent.locks.AbstractQueuedSynchronizer$Node 异常 long middle = start + (end - start) / 2; ForkJoin left = new ForkJoin(start, middle); // 递归,直到分解到最小区间后,开始计算 left.fork(); // 拆分子任务,压入线程队列 ForkJoin right = new ForkJoin(middle, end); // 递归,直到分解到最小区间后,开始计算 right.fork(); // 拆分子任务,压入线程队列 // 合并两部分计算的值 return left.join() + right.join(); } } public static void main(String[] args) { // 开始时间 Instant start = Instant.now(); // 这里需要一个线程池的支持 ForkJoinPool pool = new ForkJoinPool(); // 累加到 1 亿 ForkJoinTask task = new ForkJoin(0, 100000000L); long sum = pool.invoke(task); // 结束时间 Instant end = Instant.now(); System.out.println(String.format("累加到1亿的计算时间为:%s 毫秒,值:%s", Duration.between(start, end).toMillis(), sum)); } }
3.2 并行流对 Fork-Join 的简化 //开始时间 Instant start = Instant.now(); long sum = LongStream.rangeClosed(0, 1000000000L) // 创建0-1亿的数字串行流 .parallel() // 转换为并行流,使用Fort-Join框架,缺省使用ForkJoinPool.commonPool()线程池 .reduce(0, Long :: sum); // 规约计算所有元素累加 //结束时间 Instant end = Instant.now(); System.out.println(String.format("累加到1亿的计算时间为:%s 毫秒,值:%s", Duration.between(start, end).toMillis(), sum));
3.3 并行流的性能
性能测试请看:Stream Performance https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/8-Stream%20Performance.md 此处引用结论: * 对于简单操作,比如最简单的遍历,Stream串行API性能明显差于显示迭代,但并行的Stream API能够发挥多核特性。 * 对于复杂操作,Stream串行API性能可以和手动实现的效果匹敌,在并行执行时Stream API效果远超手动实现。 所以,如果出于性能考虑, 1. 对于简单操作推荐使用外部迭代手动实现, 2. 对于复杂操作,推荐使用Stream API, 3. 在多核情况下,推荐使用并行Stream API来发挥多核优势, 4. 单核情况下不建议使用并行Stream API。 如果出于代码简洁性考虑,使用Stream API能够写出更短的代码。 即使是从性能方面说,尽可能的使用Stream API也另外一个优势, 那就是只要Java Stream类库做了升级优化,代码不用做任何修改就能享受到升级带来的好处。
软件开发
2020-05-22 21:16:00
MapReduce 是 Google 大数据处理的三驾马车之一,另外两个是 GFS 和 Bigtable。它在倒排索引、PageRank 计算、网页分析等搜索引擎相关的计数中都有大量的应用。
MapReduce 的本质就是分治算法。
如何理解分治算法?
分支算法(divide and conquer)的核心思想就是,分而治之,也就是将原问题划分成 n 个规模较小,并且结构与原问题相似的子问题,递归地解决这些子问题,然后在合并其结果,就得到原问题的解了。
这个定义看起来有点类似递归的定义。关于分支与递归的区别,分支算法是一种处理问题的思想,递归是一种编程技巧。实际上,分治算法一般都比较适合用帝归来实现的。
分治算法的递归实现中,每一层递归都会涉及这样三个操作: 分解:将原问题分解成一系列子问题; 解决:递归地求解各个子问题,若子问题足够小,则直接求解; 合并:将子问题的结果合并成原问题。
分治算法能解决的问题,一般需要满足下面这几个条件: 原问题与分解成的小问题有相同的模式; 原问题分解成的子问题可以独立求解,子问题之间没有相关性,这一点是分治算法跟动态规划的明显区别; 具有分解终止条件,也就是说,当问题足够小时,可以直接求解; 可以将子问题合并成原问题,而这个合并操作的复杂度不能太高,否则就起不到减小算法总体复杂度的效果了。
分支算法应用举例分析
我们通过用分治算法来解决我们在学习排序的时候涉及的一个问题,加深你对分治算法的理解。
排序算法中的数据有有序度、逆序度的概念。我们用有序度来表示一组数据的有序程度,用逆序度来表示一组数据的无序程度。
假设我们有 n 个数据,我们期望数据从小到大排列,那完全有序的数据的有序度就是 n(n-1)/2,逆序度等于 0;相反,倒序排列的数据的有序度就是 0,逆序度是 n(n-1)/2。除了这两种极端情况,我们通过计算有序对或者逆序对的个数,来表示数据的有序度或逆序度。
那如何用编程求出一组数据的有序对个数或者逆序对个数呢?因为有序对个数和逆序对个数的求解方式是类似的,所以我们只思考逆序对个数的求解方法。
最笨的方法,拿每个数字跟它后面的数组比较,看有几个比它小的。我们把它小的数字个数记作 k,通过这样的方式,把每个数字都考察一遍之后,然后对每个数字对应的 k 值求和,最后得到的总和就是逆序对的个数。不过,这样的操作时间复杂度是 O(n^2)。我们希望用更加高效的处理方法。
我们用分治算法来试试。套用分治的思想求数组 A 的逆序对个数。我们可以将数组分成前后两半 A1 和 A2,分别计算 A1 和 A2 的逆序对个数 K1 和 K2,然后再计算 A1 与 A2 之间的逆序对个数 K3。那数组 A 的逆序对个数就等于 K1 + K2 + K3。
使用分治算法的其中一个要求是,子问题合并的代价不能太大,否则就起不了降低时间复杂度的效果了。那回到这个问题,如何快速计算出两个子问题 A1 与 A2 之间的逆序对个数呢?
这就要借助归并排序算法了。
归并排序中有一个非常关键的操作,就是将两个有序的小数组,合并成一个有序的数组。实际上,这个个合并的过程中,我们就可以计算这两个小数组的逆序对个数了。每次合并操作,我们都计算逆序对个数,把这些计算出来的逆序对个数求和,就是这个数组的逆序对个数。
代码如下。 // 全局变量或者成员变量 private int num = 0; public int count(int[] a, int n){ num = 0; mergeSortCounting(a, 0, n-1); return num; } private void mergeSortCounting(int[] a, int p, int r){ if(p >= r){ return; } int q = (p + r) / 2; mergeSortCount(a, p, q); mergeSortCount(a, q+1, r); merge(a, p, q, r); } private void merge(int[] a, int p, int q, int r){ int i = p; int j = q + 1; int k = 0; int[] tmp = new int[r-p+1]; while(i <= q && j <= r){ if(a[i] <= a[j]){ tmp[k++] = a[i++]; }else{ // 统计 p-q 之间,比 a[j] 大的元素个数 num += (q - i + 1); tmp[k++] = a[j++]; } } // 处理剩下的 while(i <= q){ tmp[k++] = a[i++]; } while(j <= r){ tmp[k++] = a[j++]; } for(i = 0; i <= r - q; i++){ a[p+i] = tmp[i]; } }
关于分支算法,还有两个比较经典的问题,可以练习下。 二维平面上有 n 个点,如何快速计算出两个距离最近的点对? 有两个 n * n 的矩阵 A,B,如何快速求解两个矩阵的乘积 C = A * B ?
分支思想的海量数据处理中的应用
分治算法思想的应用非常广泛,并不仅限于指导编程和算法设计。它还经常用在海量数据处理的场景中。
我们前面学习的数据结构和算法,大部分都是基于内存存储和单机处理。但是,如果要处理的数据量非常大,没法一次性放到内存中,这时候,这些数据结构就无法工作了。
比如,给 10GB 的订单文件按照金额排序这样一个需求,看似是简单的排序问题,但是因为数据量太大,有 10GB,而我们的机器的内存可能只有 2、3GB,无法一次性加载到内存,也发通过单纯的使用快排、归并等算法来解决。
要解决这种数据量大到内存装不下的问题,我们就可以利用分治思想。我们可以将海量的数据集合根据某种方法(比如哈希分片取模),划分为几个小的数据集合,每个小的数据集合单独加载到内存中解决,然后再将小数据集合合并成大数据集合。实际上,利用这种分支的处理思想,不仅仅能克服内存的限制,还能利用多线程或者多级处理,加快处理的速度。
比如刚才讲的例子,给 10GB 的订单排序,我们可以先扫描一遍订单,根据订单的金额,将 10GB 的文件划分为几个金额区间。比如订单金额为 1 到 100 元的放到一个小文件,101 到 200 之间的放到另一个文件,以此类推。每个小文件都可以单独加载到内存中进行排序,最后将这些有序的小文件合并,就是最终有序的 10GB 订单数据了(合并多个有序小文件,参考第 29 节堆的应用)。
如果订单数据存储在类似 GFS 这样的分布式系统上,当 10GB 的订单被划分成多个小文件的时候,每个文件可以并行加载到多台机器上处理,最后再将结果合并在一起,这样并行处理的速度也加快了很多。不过,这里有一点需要注意,就是数据的存储与计算所在的机器是同一个网络或者网络中靠的近的(比如一个局域网内,数据存储速度很快),否则就会因为数据访问的速度,导致整个处理过程不但不会变快,反而有可能变慢。
在学习第 23 节哈希算在分布式系统中的应用和第29 节堆的应用中,有很多例子都用到了分治思想来解决问题。
解决开篇
为什么说 MapReduce 的本质就是分治思想呢?
刚才举的订单排序的例子,数据有 10GB 大小,可能给你感受还不够强烈。那如果处理的数据是 1T、10T、100T 这样的,那一台机器处理的效率肯定是非常低的。而对于谷歌搜索引擎来说,网页爬取、清洗、分析、分词、计算权重、倒排索引等等各个环节中,都会面临如此海量的数据(比如网页)。所以,利用集群并行处理显然是大势所趋。
一台机器过于低效,那我们就把任务拆分到多台机器上来处理。如果拆分之后的小任务之间互不干扰,独立计算,最后再将结果合并,这就是分治思想!
实际上,MapReduce 框架只是一个任务调度器,底层依赖 GFS 来存储数据,依赖 Borg 管理机器。它从 GFS 中拿数据,交给 Borg 中的机器执行,并且时刻监控机器执行的进度,一旦出现机器宕机、进度卡壳等,就重新从 Borg 中调度一台机器执行。
尽管 MapReduce 的模型非常简单,但是在 Google 内存应用非常广泛。它除了可以用来处理这种数据与数据之间存在关系的任务,比如 MapReduce 的经典例子,统计文件中单词出现的频率。除此之外,它还可以用来处理数据与数据之间没有关系的任务,比如对网页分析、分词等,每个网页可以独立的分析、分词,而这两个网页之间并没有关系。网页几十亿、上百亿,如果单机处理,效率低下。我们就可以利用 MapReduce 提供的高可靠、高性能、高容错的并行计算框架,并行地吹着几十亿、上百亿的网页。
内容小结
今天我们学习一种应用非常广泛的算法思想,分治算法。
分治算法用四个字概括就是“分而治之”,将原问题划分成 n 个规模较小而结构与原问题相似的问题,递归地解决这些问题,然后再合并其结果,就得到原问题的解。
还学习了两种分治算法的典型的应用场景,一个是用来指导编码,降低问题求解的时间复杂度,另一个是解决海量数据处理问题。比如 MapReduce 本质就是利用了分治思想。
我们有时常感叹 Google 的创新能力如之强,总是在引领技术的发展。实际上, 创新并非离我们很远,创新的源泉来自对事物本质的认识。无数优秀架构设计的思想来源都是基础的数据结构与算法,这本身就是算法的一个魅力所在。
课后思考
我们前面学习的数据结构、算法、解决思路,以及举的例子中,有哪些采用了分治算法的思想呢?除此之外,生活中、工作中,还有没有其他用到分治思想的地方呢?
答: 快速排序、归并排序、桶排序、基数排序算法; 二分查找算法; 利用递归树求解算法复杂度; 哈希算法在分布式系统中的应用章节中,应用六:数据分片,统计搜索关键词出现的次数;快速判断图片是否在图库中的例子; 堆的应用中章节中,获取热门 Top 10 搜索关键词;应用一:优先级队列,合并有序小文件。 分布式数据库数据分片等等。
代码
Github
软件开发
2020-05-22 17:23:00
1. 简介
函数式接口(Functional Interface)是Java 8对一类特殊类型的接口的称呼。 这类接口只定义了唯一的抽象方法的接口,并且这类接口使用了 @FunctionalInterface 进行注解。在jdk8中,引入了一个新的包 java.util.function , 可以使java 8 的函数式编程变得更加简便。
这个package中的接口大致分为了以下四类: Function: 接收参数,并返回结果,主要方法 R apply(T t) UnaryOperation 接收与返回类型相同,主要方法 T apply(T t) BiFunction 接收两个参数,并返回结果,主要方法 R apply(T t, U u) BinaryOperation 接收的两个参数与返回类型均相同,主要方法 T apply(T t, T u) Consumer: 接收参数,无返回结果,主要方法 void accept(T t) BiConsumer 接收两个参数,无返回结果,主要方法 void accept(T t, U u) Supplier: 不接收参数,但返回结构,主要方法 T get() Predicate: 接收参数,返回boolean值,主要方法 boolean test(T t) BiPredicate 接收两个参数,返回boolean值,主要方法 boolean test(T t, U u)
2. 总览
软件开发
2020-05-22 12:11:00
说一说关于synchronized关键字的理解
synchronized关键字解决的是多个线程之间访问资源的同步性。synchronized关键字可以保证被它修饰过的方法或者代码块在任意时刻只能被一个线程执行
在Java早期版本中,synchronized属于重量级锁,效率低下。而JDK1.6版本后对锁的实现做了大量优化,比如自旋锁、适应性锁、锁消除、偏向锁、轻量级锁等技术来减少锁的开销。
说说自己是怎么使用synchronized关键字,在项目使用到了吗
一般有三种使用场景 修饰 实例 方法,作用于当前 对象实例 加锁,进入同步代码前要获得当前 对象实例 的锁 修饰 静态 方法,作用于当前 类对象 加锁,进入同步代码前要获得当前 类对象 的锁 修饰 代码块 ,指定加锁对象,对给定对象加锁
讲一下synchronized关键字的底层原理
synchronized关键字底层原理属于JVM层面
synchronized 同步语句块的实现 使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同 步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图 获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么 Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设 为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
synchronized 修饰的方法 并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用
说说JDK1.6之后的synchronized关键字底层做了哪些优化,可以详细介绍一下这些优化吗
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 锁主要存在 四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态 ,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
谈谈 synchronized和ReenTrantLock 的区别
① 两者都是可重入锁 “可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
**② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API **
synchronized 是依赖于 JVM 实现的,ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成)
③ ReenTrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。
主要有三点: ①等待可中断;②可实现公平锁; ③可实现选择性通知(锁可以绑定多个条件) ReenTrantLock提供了一种能够中断等待锁的线程的机制 ,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,但可以通过 ReenTrantLock类的》 ReentrantLock(boolean fair) 构造方法来制定是否是公平的。 synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也 可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很 好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器), 线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵 活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合 Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而 synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果 执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的 signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。
讲一下Java内存模型
在 JDK1.2 之前,Java的内存模型实现总是从 主存(即共享内存)读取变量 ,不需要特别注意的。
而在当前的Java 内存模型下,线程可以把变量保存**本地内存(比如机器的寄存器)**中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝, 造成数据的不一致 。
要解决这个问题,就需要把变量声明成 volatile ,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取
说白了, volatile 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。
说说 synchronized 关键字和 volatile 关键字的区别 volatile关键字 是线程同步的 轻量级实现 ,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块 。synchronized关键字在JavaSE1.6之后引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升, 实际开发中使用 synchronized 关键字的场景还是更多一些 。 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞 volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。 volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
为什么要用线程池?
使用线程池的好处: 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性, 使用线程池可以进行统一的分配,调优和监控。
实现Runnable接口和Callable接口的区别
两者的区别在于 Runnable 接口不会返回结果,但是 Callable 接口可以返回结果。
备注: 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
Executors.callable(Runnable task)

Executors.callable(Runnable task,Object resule) 。
执行execute()方法和submit()方法的区别是什么呢
1) execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功; 2) submit() 方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断 任务是否执行成功 ,并且可以通过future的 get() 方法来获取返回值, get() 方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
如何创建线程池
方法一:通过构造方法实现
方法二:通过Executor框架的工具类Executors来实现 我们可以创建三种类型的ThreadPoolExecutor FixedThreadPool:该方法返回一个固定线程数量的线程池。 SingleThreadExecutor: 方法返回一个只有一个线程的线程池。 CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但 若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新 的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
介绍一下Atomic 原子类
原子类说简单点就是具有原子特征的类。
指一个操作不可中断。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
AQS介绍
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面
AQS:也就是队列同步器,是实现 ReentrantLock 的基础。
AQS 有一个 state 标记位,值为1 时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是一个双向链表。
当获得锁的线程需要等待某个条件时,会进入condition 的等待队列,等待队列可以有多个。
当 condition 条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争。
ReentrantLock 就是基于 AQS 实现的,如下图所示,ReentrantLock 内部有公平锁和非公平锁两种实现,差别就在于新来的线程是否比已经在同步队列中的等待线程更早获得锁。
和 ReentrantLock 实现方式类似,Semaphore 也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,Semaphore 是共享锁。
从图中可以看到,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。
它有公平锁FairSync和非公平锁NonfairSync两个子类。
ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
跟我聊一下CAS
CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。
CAS 是怎么实现线程安全的?
线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。
举个栗子:现在一个线程要修改数据库的name,修改前我会先去数据库查name的值,发现name=“张三”,拿到值了,我们准备修改成name=“李四”,在修改之前我们判断一下,原来的name是不是等于“张三”,如果被其他线程修改就会发现name不等于“张三”,我们就不进行操作,如果原来的值还是张三,我们就把name修改为“李四”,至此,一个流程就结束了。 Tip:比较+更新 整体是一个原子操作
他是乐观锁的一种实现,就是说认为数据总是不会被更改,我是乐观的仔,每次我都觉得你不会渣我,差不多是这个意思。
CAS存在什么问题呢?
要是结果一直循环了, CUP开销 是个问题,还有 ABA问题 和只能保证一个 共享变量原子操作 的问题。
你能分别介绍一下么?
好的,我先介绍一下 ABA 这个问题,直接口述可能有点抽象,我画图解释一下:
看到问题所在没,我说一下顺序: 线程1读取了数据A 线程2读取了数据A 线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B 线程3读取了数据B 线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A 线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值
在这个过程中任何线程都没做错什么,但是值被改变了,线程1却没有办法发现,其实这样的情况出现对结果本身是没有什么影响的,但是我们还是要防范,怎么防范我下面会提到。
循环时间长开销大的问题:
是因为CAS操作长时间不成功的话,会导致一直自旋,相当于死循环了,CPU的压力会很大。
只能保证一个共享变量的原子操作:
CAS操作单个共享变量的时候可以保证原子的操作,多个变量就不行了,JDK 5之后 AtomicReference可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作。
我还记得你之前说在JUC包下的原子类也是通过这个实现的,能举个栗子么?
那我就拿AtomicInteger举例,他的自增函数incrementAndGet()就是这样实现的,其中就有大量循环判断的过程,直到符合条件才成功。
大概意思就是循环判断给定偏移量是否等于内存中的偏移量,直到成功才退出,看到do while的循环没。
乐观锁在项目开发中的实践,有么?
有的就比如我们在很多订单表,流水表,为了防止并发问题,就会加入CAS的校验过程,保证了线程的安全,但是看场景使用,并不是适用所有场景,他的优点缺点都很明显。
那开发过程中ABA你们是怎么保证的?
加标志位,例如搞个自增的字段,操作一次就自增加一,或者搞个时间戳,比较时间戳的值。
举个栗子:现在我们去要求操作数据库,根据CAS的原则我们本来只需要查询原本的值就好了,现在我们一同查出他的标志位版本字段vision。
聊一下悲观锁?
悲观锁从宏观的角度讲就是,他是个渣男,你认为他每次都会渣你,所以你每次都提防着他。
我们先聊下JVM层面的synchronized:
synchronized加锁,synchronized 是最常用的线程同步手段之一,上面提到的CAS是乐观锁的实现,synchronized就是悲观锁了。
它是如何保证同一时刻只有一个线程可以进入临界区呢?
synchronized,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
在对象级使用锁通常是一种比较粗糙的方法,为什么要将整个对象都上锁,而不允许其他线程短暂地使用对象中其他同步方法来访问共享资源?
如果一个对象拥有多个资源,就不需要只为了让一个线程使用其中一部分资源,就将所有线程都锁在外面。
由于每个对象都有锁,可以如使用虚拟对象
synchronized 应用在方法上时,在字节码中是通过方法的 ACC_SYNCHRONIZED 标志来实现的。
反正其他线程进这个方法就看看是否有这个标志位,有就代表有别的仔拥有了他,你就别碰了。
synchronized 应用在同步块上时,在字节码中是通过 monitorenter 和 monitorexit 实现的。
每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。
小结: 同步方法和同步代码块底层都是通过monitor来实现同步的。
两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现,同步代码块是通过monitorenter和monitorexit来实现。
我们知道了每个对象都与一个monitor相关联,而monitor可以被线程拥有或释放。
以前我们一直锁synchronized是重量级的锁,为啥现在都不提了?
在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。
但是,随着 Java SE 1.6 对 synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。
针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。锁只能升级,不能降级。 Tip:锁升级的过程
软件开发
2020-06-06 01:12:00
最左边是原始图片,中间是canvas内容,右边是将canvas内容导出到img标签中

canvas绘图时,确定图片的原始尺寸,不是显示的dom大小,需要创建元素后获得
如果使用dom大小的话,会在绘制时只能绘制出一部分
canvas目前的感觉是分为绘图层和展示层
旋转和移动的是绘图层的中心
展示层呈现内容,大小也是展示层的大小 Title
软件开发
2020-06-05 21:57:00
在开发过程中,通常会对一段业务代码不断地修改测试,在修改之后往往需要重启服务,有些服务需要加载很久才能启动成功,这种不必要的重复操作极大的降低了程序开发效率。为此,Spring Boot框架专门提供了进行热部署的依赖启动器,用于进行项目热部署,而无需手动重启项目 演示: 1.添加spring-boot-devtools热部署依赖启动器** 在Spring Boot项目进行热部署测试之前,需要先在项目的pom.xml文件中添加spring-boot-devtools热部署依赖启动器: ```xml org.springframework.boot spring-boot-devtools ``` 由于使用的是IDEA开发工具,添加热部署依赖后可能没有任何效果,接下来还需要针对IDEA开发工具进行热部署相关的功能设置 **2. IDEA工具热部署设置** 选择IDEA工具界面的【File】->【Settings】选项,打开Compiler面板设置页面选择Build下的Compiler选项,在右侧勾选“Build project automatically”选项将项目设置为自动编译,单击【Apply】→【OK】按钮保存设置 在项目任意页面中使用组合快捷键“Ctrl+Shift+Alt+/”打开Maintenance选项框,选中并打开Registry页面,具体如图1-17所示 列表中找到“compiler.automake.allow.when.app.running”,将该选项后的Value值勾选,用于指定IDEA工具在程序运行过程中自动编译,最后单击【Close】按钮完成设置 这些内容,是从拉勾教育的《Java工程师高薪训练营》里学到的,课程内容非常全面,还有拉勾的内推大厂服务,推荐你也看看。
软件开发
2020-06-05 17:30:00
将mybatis全局配置文件对应的DOM转换为XNODE对象
在上文中我们完成了 XmlConfigBuilder 对象的构建工作,准备好了解析 XML 文件的基础环境。
所以接下来就是调用 XmlConfigBuilder 暴露的 parse() 方法来完成mybatis配置文件的解析工作了。 public Configuration parse() { if (parsed) { // 第二次调用XMLConfigBuilder throw new BuilderException("Each XMLConfigBuilder can only be used once."); } // 重置XMLConfigBuilder的解析标志,防止重复解析 parsed = true; // 此处开始进行Mybatis配置文件的解析流程 // 解析 configuration 配置文件,读取【configuration】节点下的内容 parseConfiguration(parser.evalNode("/configuration")); // 返回Mybatis的配置实例 return configuration; }
在没有解析过的前提下,mybatis会调用 parseConfiguration(XNode root) 方法来完成 Configuration 对象的构建操作。
parseConfiguration(XNode root) 方法的入参是一个 XNode 类型的对象实例,该对象的产生是通过调用我们上文创建的 XPathParser 的 XNode evalNode(String expression) 方法来完成的。 parseConfiguration(parser.evalNode("/configuration"));
evalNode 方法接收的是一个XPath地址表达式,字符串 "/configuration" 中的 / 表示从根节点获取元素,所以 "/configuration" 则表示获取配置文件的根元素 configuration . configuration 是Mybaits主配置文件的根节点,我们通常这样使用: ... /** * 根据表达式解析Document对象获取对应的节点 * * @param expression Xpath地址表达式 * @return XNode */ public XNode evalNode(String expression) { // 从document对象解析出指定的节点 return evalNode(document, expression); }
在 evalNode 方法中,将上文获取到的 XPathParser 的类属性 document 作为参数,传递给他的重载方法: /** * 根据表达式获取节点 * @param root 根节点 * @param expression xpath表达式 * @return XNode */ public XNode evalNode(Object root, String expression) { // 获取DOM节点 Node node = (Node) evaluate(expression, root, XPathConstants.NODE); if (node == null) { return null; } // 包装成XNode节点 return new XNode(this, node, variables); }
在重载的 evalNode 方法内部,将获取表达式对应的DOM节点的工作委托给了 evaluate 方法来完成,如果解析出了对应的DOM节点,将会以 XPathParser 对象本身和解析出来的DOM节点对象,以及用户传入的变量作为参数构造出一个 XNode 对象实例返回给方法的调用方。
被委托的 evaluate 方法利用 XPath 解析器来完成将表达式解析成指定对象的工作。 private Object evaluate(String expression, Object root, QName returnType) { try { // 在指定的上下文中计算XPath表达式,并将结果作为指定的类型返回。 return xpath.evaluate(expression, root, returnType); } catch (Exception e) { throw new BuilderException("Error evaluating XPath. Cause: " + e, e); } }
在 XNode 类中定义了六个常量,这六个常量的初始化赋值操作都是在 XNode 节点的构造方法中完成的。 /** * XNode * * @param xpathParser XPath解析器 * @param node 被包装的节点 * @param variables 用户传入的变量 */ public XNode(XPathParser xpathParser, Node node, Properties variables) { // 初始化节点对应的解析器 this.xpathParser = xpathParser; // 初始化DOM 节点 this.node = node; // 初始化节点名称 this.name = node.getNodeName(); // 初始化用户定义的变量 this.variables = variables; // 解析节点中的属性配置 this.attributes = parseAttributes(node); // 解析节点包含的内容 this.body = parseBody(node); }
其中 attributes 和 body 属性的取值操作需要分别通过 parseAttributes 和 parseBody 方法来完成。 /** * 解析节点中的属性值 * * @param n 节点 * @return 属性集合 */ private Properties parseAttributes(Node n) { // 定义 Properties对象 Properties attributes = new Properties(); // 获取属性节点 NamedNodeMap attributeNodes = n.getAttributes(); if (attributeNodes != null) { for (int i = 0; i < attributeNodes.getLength(); i++) { Node attribute = attributeNodes.item(i); // 针对每个属性的值进行一次占位符解析替换的操作 String value = PropertyParser.parse(attribute.getNodeValue(), variables); // 保存 attributes.put(attribute.getNodeName(), value); } } return attributes; } /** * 解析节点中的内容 * * @param node 节点 * @return 节点中内容 */ private String parseBody(Node node) { String data = getBodyData(node); if (data == null) { NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); data = getBodyData(child); if (data != null) { break; } } } return data; } /** * 获取CDATA节点和TEXT节点中的内容 * * @param child 节点 */ private String getBodyData(Node child) { if (child.getNodeType() == Node.CDATA_SECTION_NODE || child.getNodeType() == Node.TEXT_NODE) { // 获取CDATA节点和TEXT节点中的内容 String data = ((CharacterData) child).getData(); // 执行占位符解析操作 data = PropertyParser.parse(data, variables); return data; } return null; }
这两个方法比较简单,唯一需要注意的就是在处理属性值和body内容的时候,调用了 PropertyParser.parse(String string, Properties variables) 方法对属性值和body体中的占位符进行了替换操作。 关于PropertyParser > PropertyParser在mybatis中担任着一个替换变量占位符的角色。主要作用就是将 ${变量名} 类型的占位符替换成对应的实际值。
PropertyParser 只对外暴露了一个 String parse(String string, Properties variables) 方法,该方法的作用是替换指定的占位符为变量上下文中对应的值,该方法有两个入参:一个是 String 类型的可能包含了占位符的文本内容,一个是 Properties 类型的变量上下文。 /** * 替换占位符 * @param string 文本内容 * @param variables 变量上下文 */ public static String parse(String string, Properties variables) { // 占位符变量处理器 VariableTokenHandler handler = new VariableTokenHandler(variables); // 占位符解析器 GenericTokenParser parser = new GenericTokenParser("${", "}", handler); // 返回闭合标签内的内容 return parser.parse(string); }
在 parse 方法中涉及到了两个类, VariableTokenHandler 和 GenericTokenParser .
VariableTokenHandler 是 TokenHandler 接口的一个实现类, TokenHandler 定义了一个 String handleToken(String content); 方法,该方法主要用来对客户端传入的内容进行一些额外的处理。
TokenHandler 也是策略模式的一种体现,它定义了文本统一处理的接口,其子类负责提供不同的处理策略。
具体到 VariableTokenHandler 中,该方法的作用就是替换传入的文本内容中的占位符。
VariableTokenHandler 的构造方法需要一个 Properties 类型的 variables 参数,该参数中定义的变量将用于替换占位符。
VariableTokenHandler 的占位符解析操作允许用户以 ${key:defaultValue} 的形式为指定的 key 提供默认值,即如果变量上下文中没有匹配 key 的变量值,则以 defaultValue 作为 key 的值。
占位符中取默认值时使用分隔符默认是 : ,如果需要修改,可以通过在 variables 参数中添加 org.apache.ibatis.parsing.PropertyParser.default-value-separator="自定义分隔符" 进行配置。
在占位符中使用默认值的操作默认是关闭的,如果需要开启,可以在 variables 参数中添加 org.apache.ibatis.parsing.PropertyParser.enable-default-value=true 进行配置。
下面是 VariableTokenHandler 的构造方法: private VariableTokenHandler(Properties variables) { this.variables = variables; // 是否允许使用默认值比如${key:aaaa} this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE)); // 默认值分隔符 this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR); }
GenericTokenParser 是一个通用的占位符解析器,他的构造方法有三个入参,分别是占位符的开始标签,结束标签,以及针对占位符内容的处理策略对象。 /** * GenericTokenParser * @param openToken 开始标签 * @param closeToken 结束标签 * @param handler 内容处理器 */ public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) { this.openToken = openToken; this.closeToken = closeToken; this.handler = handler; }
GenericTokenParser 对外提供了一个 parse(String text) 方法,该方法将会寻找匹配占位符的内容并调用 TokenHandler 对其进行处理,如果未匹配到占位符对应的内容,则返回始原内容。
在完成 XNode 对象的创建工作之后,就可以使用该对象调用 parseConfiguration(XNode root) 方法来进行真正的配置文件解析操作了: parseConfiguration(parser.evalNode("/configuration"));
在解析配置文件之前,我们先简单了解一下mybatis全局配置文件的DTD定义:
参考上面的DTD文件,我们可以发现 configuration 节点下允许出现11种类型的子节点,这些节点都是可选的,这就意味着Mybatis的全局配置文件可以不配置任何子节点(参考单元测试: org.apache.ibatis.builder.XmlConfigBuilderTest#shouldSuccessfullyLoadMinimalXMLConfigFile )。
回头继续看方法 parseConfiguration ,在该方法中,对应着 configuration 的子节点,解析配置的工作被拆分成了多个子方法来完成: /** * 解析Configuration节点 */ private void parseConfiguration(XNode root) { try { //issue #117 read properties first // 加载资源配置文件,并覆盖对应的属性[properties节点] propertiesElement(root.evalNode("properties")); // 将settings标签内的内容转换为Property,并校验。 Properties settings = settingsAsProperties(root.evalNode("settings")); // 根据settings的配置确定访问资源文件的方式 loadCustomVfs(settings); // 根据settings的配置确定日志处理的方式 loadCustomLogImpl(settings); // 别名解析 typeAliasesElement(root.evalNode("typeAliases")); // 插件配置 pluginElement(root.evalNode("plugins")); // 配置对象创建工厂 objectFactoryElement(root.evalNode("objectFactory")); // 配置对象包装工厂 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 配置反射工厂 reflectorFactoryElement(root.evalNode("reflectorFactory")); // 通过settings配置初始化全局配置 settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 // 加载多环境源配置,寻找当前环境(默认为default)对应的事务管理器和数据源 environmentsElement(root.evalNode("environments")); // 数据库类型标志创建类,Mybatis会加载不带databaseId以及当前数据库的databaseId属性的所有语句,有databaseId的 // 语句优先级大于没有databaseId的语句 databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 注册类型转换器 typeHandlerElement(root.evalNode("typeHandlers")); // !!注册解析Dao对应的MapperXml文件 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
在上面代码中多次出现了 root.evalNode(String) 方法,该方法的作用是:根据传入的表达式,获取到对应的 XNode 节点对象。 public XNode evalNode(String expression) { return xpathParser.evalNode(node, expression); }
具体的实现实际上是委托给了 xpathParser 解析器的 XNode evalNode(Object root, String expression) 方法来完成。 /** * 根据表达式获取节点 * @param root 根节点 * @param expression xpath表达式 * @return XNode */ public XNode evalNode(Object root, String expression) { // 获取DOM节点 Node node = (Node) evaluate(expression, root, XPathConstants.NODE); if (node == null) { return null; } // 包装成XNode节点 return new XNode(this, node, variables); }
该方法我们在上文中已经看过了,这里不再赘述,现在以 propertiesElement(root.evalNode("properties")); 为例,解释一下 xpath 表达式 "properties" 的作用: 该表达式表示获取 properties 元素及其所有子元素。
关注我,一起学习更多知识
软件开发
2020-06-05 13:16:00
消费者也期待小型跨境电商零售商提供与大型跨境电商零售商(如亚马逊)相同的服务,这些服务包括加急运输,当日交货,退货简单,实时库存可见性以及清单。一套好的ERP系统能够让小型跨境电商卖家为客户提供更好的服务。那么跨境电商ERP系统能够带来哪些好处呢?


跨境电商ERP系统的好处:
提高效率
ERP解决方案与跨境电商商店的无缝集成有助于将订单信息传输到ERP,减少了手动数据输入过程,就算是订单数量快速增加也能迅速完成数据的输入。ERP系统和跨境电商解决方案的集成还将确保在单个位置管理产品目录,从而提高了准确性并提高了效率。
更好的决策权
传统上,跨境电商后台操作涉操作:会计,采购/库存管理,供应商管理,仓库管理,运输系统,付款处理,CRM系统和其他各个领域,通常,公司将拥有许多不同的软件解决方案来解决所有这些领域中的问题,这意味着每个区域都在独立运行。如果没有通过集成流程进行非常严格的控制,则企业将冒着基于不准确数据做出决策的风险。例如,如果没有来自销售的准确信息,库存管理将受到影响。没有准确的库存,营销团队提供的定价和促销活动将受到影响,进而影响现金流量和盈利能力。
一个完善的、基于云的ERP系统通常包括针对每个领域的解决方案/模块,以确保组织内的每个业务领域都依赖单一的事实来源,从而使员工能够更快地做出更好的决策。白码ERP具有深度数据关联的特点可以追源多层数据关系,为公司决策提供数据支持。
节约成本
了解销售趋势可以更好地预测库存,确定人员需求,从而提高客户满意度,并减少货架上库存占用的资金量。
会计实务
一个好的ERP将确保企业所有的财务文件都符合公认会计原则和国际财务报告准则。通常,会计团队会根据对可见性和报告的需求来提出对ERP的需求。
全渠道支持
现在很多跨境电商都是通过多平台销售商品的,想要自己的网站在诸如Amazon或eBay的市场上列出商品,则需要具有跨多个位置和渠道管理库存的功能,并且改善这些渠道的客户体验。与跨境电商商店集成的ERP将使您显着改善客户体验,这是通过利用ERP系统作为所有渠道的解决方案所连接的枢纽来完成的。


选择跨境电商ERP系统时需要考虑的问题:
是否无缝连接到我的跨境电商解决方案
数据是否能够同步
是否有开放的API
是否允许自定义和可配置的工作流程
系统是基于云的吗
是否支持所有类型的产品(简单,可定制,工具包或捆绑包,虚拟产品等)
是否允许在跨境电商系统中授权付款并在发货时捕获交易
如何处理税金和运费
平台和公司是否稳定,并且可以随着业务扩展吗
是否有强大的经销商和产品支持网络
ERP系统能够提高跨境电商卖家的工作效率,节约经营成本,将多渠道的数据集成在一次为决策的时候提供数据支持避免决策失误。现在市场上的ERP系统分为通用型和定制型两种,ERP系统定制化从企业需求出发,开发出来的系统能够更好的解决企业经营中的问题。
软件开发
2020-06-04 12:05:00
public class TestThreadlocal { private static ThreadLocal local = new ThreadLocal(){ @Override protected Integer initialValue() { return 1; } }; public static void main(String[] args) { for (int j = 0; j < 5; j++) { new Thread(() -> { Integer i = local.get(); local.set(i+=5); System.out.println(Thread.currentThread().getName() + "-" + local.get()); }).start(); } } }
一、ThreadLocal结构
二、内存模型
这里table数组的每个元素存放一个entry,一个线程中可能有多个ThreadLocal,就会有多个entry
ThreadLocal-ref是一个弱引用,如果ThreadLocal-ref = null时,堆里的threadLocal就会被GC回收,此时key的指向就是一个null。 value是强引用,此时如果thread-ref(线程池等场景)一直存在,则value的是不会清空,这样就会产生脏数据,造成内存泄漏。为了解决这种情况,在set的时候会清理部分脏数据。
三、set方法
1.set public void set(T value) { //1.获取当前线程 Thread t = Thread.currentThread(); //2.根据当前线程获取到一个ThreadLocalMap ThreadLocalMap map = getMap(t); //3.如果ThreadLocalMap不为空,则取值,否则创建一个ThreadLocalMap if (map != null) map.set(this, value); else createMap(t, value); }
2.createMap(t, value) 逻辑 初始化数组table,里面存放的是entry实力,默认16个长度 通过当前ThreadLocal对象的hash值 与上 15 计算出一个数组下标(hash值使用斐波拉契散列值保证key不重复,避免hash碰撞) 创建一个entry,key = ThreadLocal实例,value = set方法传过来的值。并发entry实力放到table数组对应的下标中。 图示
代码 //createMap void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } //new ThreadLocalMap(this, firstValue); ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } //firstKey.threadLocalHashCode private final int threadLocalHashCode = nextHashCode(); private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } //new Entry(firstKey, firstValue) static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }
3.map.set(this, value) 逻辑 同样的算法计算出table数组的下标 从当前下标开始向右探测(线性探测(开放式寻址,防止hash冲突)),此时在i位置有四种情况 若table[i].key != this & table[i].key != null,则继续向右 若table[i].key = this ,则直接填充值value 若table[i].key = null,则替换脏entry(该情况是因为threadLocal被GC释放掉了) replaceStaleEntry(key, value, i) 若table[i]= null,则直接创建一个新的entry后填充到 i 位置 图示
代码 private void set(ThreadLocal key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //1.向后寻址,找到!= null的entry for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal k = e.get(); //1.1.找到正确的填充位置,替换值 if (k == key) { e.value = value; return; } //1.2.key=null,则认为附近很大的可能有其他的脏数据,所以进行寻址清除 //key = null,说明被GC回收了,这个时候在做填值的动作的时候还做了一次替换,主要为了填入重复hash的entry的问题,具体详见后面的图 if (k == null) { replaceStaleEntry(key, value, i); return; } } //2.向后寻址找个一个null的节点 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
4.replaceStaleEntry(key, value, i)
​ 这里,说明第一次寻址,走到table[i].key = null的逻辑,【i】位置已经是一个脏节点 逻辑 从【i-1】向左寻址 entry != null & entry.key != this,继续 entry != null & entry.key = null,标记slotToExpunge = i(待清除的位置) entry == null 结束
**备注:**向前寻址的作用是,当第一次寻址时发现脏节点,这个脏节点的附近很大可能还存在其他的脏节点,所有向前寻址,找出附近的脏节点(即entry != null & entry.key = null的节点),遇到entry = null结束寻址。 从【i+1】向右寻址 entry != null & entry.key != this,继续 entry != null & entry.key = this,填充值,并交换staleSlot 和 当前位置的entry。(一) 交互的目的:使有效节点左移 交换的原因:寻址方式为线性探测,防止下次set方法寻址时提前遇到null直接填充entry导致key相同的问题 同样这里也增加判断if (slotToExpunge == staleSlot) slotToExpunge = i; 清除脏节点 entry != null & entry.key = null。(二) 这里只是有可能后移清除标记:if (slotToExpunge == staleSlot) slotToExpunge = i; entry = null,直接填充new Entry(key, value)。(三) 最后清除脏节点 图示
向左寻址
向右寻址
(一)
(三)
​ 代码 private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
四、get方法
线性探测 写入:找到发生冲突最近的空闲单元 查找:从发生冲突的位置,向后查找
软件开发
2020-06-03 23:44:00
1.bean copy package com.qimh.springbootfiledemo.utils; import org.apache.poi.ss.formula.functions.T; import java.lang.reflect.Field; /** * @author * bean 复制 */ public class BeanCopy { /** * bean 的拷贝 * @param srcBean--原始bean * @param destClazz--目标class对象 * 这个 T 表示的是返回值T是泛型 * T是一个占位符,用来告诉编译器,这个东西先给我留着,等我编译的时候,告诉你。 * https://www.cnblogs.com/jpfss/p/9929108.html */ public static T beanCopy(Object srcBean,Class destClazz) throws IllegalAccessException, InstantiationException, NoSuchFieldException { if (null != srcBean && null != destClazz){ //获取目标bean的实例 T destBean = destClazz.newInstance(); //获取目标bean所有字段 Field[] fields = destClazz.getDeclaredFields(); for (int i = 0;i < fields.length;i++){ Field field = fields[i]; Object value = BeanEquals.getClassValue(srcBean,field.getName()); //获取目标类的字段 Field destField = destClazz.getDeclaredField(field.getName()); //可访问私有属性 destField.setAccessible(true); //给目标类属性设置值 destField.set(destBean,value); } return destBean; } return null; } }
2.拷贝工具类 package com.qimh.springbootfiledemo.utils; import com.alibaba.fastjson.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.Map; /** * @author ex-qiminhui001 * 判断两个bean是否相等 */ public class BeanEquals { /** * 日志操作类 */ private static Logger logger = LoggerFactory.getLogger(BeanEquals.class); /** * 比较两个BEAN或MAP对象的值是否相等 * 如果是BEAN与MAP对象比较时MAP中的key值应与BEAN的属性值名称相同且字段数目要一致 * @param source * @param target * @return */ public static boolean domainEquals(Object source, Object target) { if (source == null || target == null) { return false; } boolean rv = true; if (source instanceof Map) { rv = mapOfSrc(source, target, rv); } else { rv = classOfSrc(source, target, rv); } logger.info("THE EQUALS RESULT IS " + rv); return rv; } /** * 源目标为MAP类型时 * @param source * @param target * @param rv * @return */ private static boolean mapOfSrc(Object source, Object target, boolean rv) { HashMap map = new HashMap(); map = (HashMap) source; for (String key : map.keySet()) { if (target instanceof Map) { HashMap tarMap = new HashMap(); tarMap = (HashMap) target; if(tarMap.get(key)==null){ rv = false; break; } if (!map.get(key).equals(tarMap.get(key))) { rv = false; break; } } else { String tarValue = getClassValue(target, key) == null ? "" : getClassValue(target, key).toString(); if (!tarValue.equals(map.get(key))) { rv = false; break; } } } return rv; } /** * 源目标为非MAP类型时 * @param source * @param target * @param rv * @return */ private static boolean classOfSrc(Object source, Object target, boolean rv) { Class srcClass = source.getClass(); Field[] fields = srcClass.getDeclaredFields(); for (Field field : fields) { String nameKey = field.getName(); if (target instanceof Map) { HashMap tarMap = new HashMap(); tarMap = (HashMap) target; String srcValue = getClassValue(source, nameKey) == null ? "" : getClassValue(source, nameKey) .toString(); if(tarMap.get(nameKey)==null){ rv = false; break; } if (!tarMap.get(nameKey).equals(srcValue)) { rv = false; break; } } else { if (nameKey.equals("carts")){ System.out.println("field:" + nameKey); } if (nameKey.equals("friends")){ System.out.println("field:" + nameKey); } //数组类型 if (!StringUtils.isEmpty(getClassValue(source, nameKey)) && getClassValue(source, nameKey) instanceof Object[]){ rv = Arrays.equals((Object[])getClassValue(source, nameKey),(Object[])getClassValue(target, nameKey)); break; } String srcValue = getClassValue(source, nameKey) == null ? "" : getClassValue(source, nameKey) .toString(); String tarValue = getClassValue(target, nameKey) == null ? "" : getClassValue(target, nameKey) .toString(); if (!srcValue.equals(tarValue)) { rv = false; break; } } } return rv; } /** * 根据字段名称取值 * @param obj * @param fieldName * @return */ public static Object getClassValue(Object obj, String fieldName) { if (obj == null) { return null; } try { Class beanClass = obj.getClass(); Method[] ms = beanClass.getMethods(); for (int i = 0; i < ms.length; i++) { // 非get方法不取 if (!ms[i].getName().startsWith("get")) { continue; } Object objValue = null; try { objValue = ms[i].invoke(obj, new Object[] {}); } catch (Exception e) { logger.info("反射取值出错:" + e.toString()); continue; } if (objValue == null) { continue; } if (ms[i].getName().toUpperCase().equals(fieldName.toUpperCase()) || ms[i].getName().substring(3).toUpperCase().equals(fieldName.toUpperCase())) { return objValue; } else if (fieldName.toUpperCase().equals("SID") && (ms[i].getName().toUpperCase().equals("ID") || ms[i].getName().substring(3).toUpperCase() .equals("ID"))) { return objValue; } } } catch (Exception e) { // logger.info("取方法出错!" + e.toString()); } return null; } public static void main(String args[]) { Person person1 = new Person(); person1.setName("张三"); person1.setAge(20); JSONObject carts = new JSONObject(); carts.put("bmw","宝马"); carts.put("audi","奥迪"); person1.setCarts(carts); person1.setFriends(new String[]{"tom","jim"}); Person person2 = new Person(); person2.setName("张三"); person2.setAge(20); JSONObject carts2 = new JSONObject(); carts2.put("bmw","宝马"); carts2.put("audi","奥迪"); person2.setCarts(carts2); person2.setFriends(new String[]{"tom","jim2"}); System.out.println(domainEquals(person1,person2)); } }
3.目标bean package com.qimh.springbootfiledemo.bean; public class JimPerson { private Integer age; private String name; public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "JimPerson{" + "age=" + age + ", name='" + name + '\'' + '}'; } }
4.原始bean package com.qimh.springbootfiledemo.bean; public class Person { private Integer age; private String name; public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Person{" + "age=" + age + ", name='" + name + '\'' + '}'; } }
5.测试类 package com.qimh.springbootfiledemo; import com.qimh.springbootfiledemo.bean.JimPerson; import com.qimh.springbootfiledemo.bean.Person; import com.qimh.springbootfiledemo.utils.BeanCopy; public class BeanCopyMain { public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, InstantiationException { Person person = new Person(); person.setName("张三"); person.setAge(20); JimPerson jimPerson = BeanCopy.beanCopy(person, JimPerson.class); System.out.println("jimPerson:" + jimPerson); } }
软件开发
2020-06-03 23:26:00
敏捷软件开发是从1990年代开始逐渐引起广泛关注的一种新型软件开发方法,是能够应对快速变化的需求的一种软件开发能力,它作为一种新型的开发模式,被越来越多地应用到软件项目中。
敏捷软件测试指的是在敏捷软件开发过程中跟质量相关的一系列活动,和传统意义上的软件测试有很多区别,因为敏捷软件测试的概念一直比较模糊,所以经常会有人走入误区,我曾经在瀑布型的软件开发模式下做过几年的测试人员,所以在刚刚接触敏捷项目的时候也曾有过一些误解,但是在敏捷软件开发团队工作将近5年后,对很多问题有了新的认识,以下针对几个常见的误区和大家分享一下我的理解。
不需要测试策略
测试策略关注的是目标和方法,即怎样在限定的时间内有效利用有限的资源达到提前制定的目标,一般制定测试策略时会首先明确测试目标,然后确定需要哪些测试类型,各种测试类型所占的大概比例,选择测试框架,最后规划一下软件发布前需要经历哪些测试阶段。
很多人认为,敏捷软件开发以用户故事为单元,是不是集中精力在用户故事测试就足够了?是不是根本不需要考虑测试策略?
其实这是一个很大的误解,因为敏捷软件开发通常都是迭代式的发布,周期比较短,资源非常有限,这就更需要我们统筹规划,小到一个用户故事,大到一个完整的用户特性,都需要考虑怎么合理利用测试资源,所以敏捷项目是非常需要测试策略的。
具体到实际项目中,通常团队会在项目初期(迭代0)制定测试策略,明确目标(包括功能性需求的目标以及非功能性需求的目标),然后确定在开发阶段需要添加哪些自动化测试(包括单元测试,接口测试,契约测试,集成测试,系统级别的UI的用户场景测试),并规定这些测试的大概比率(符合测试金字塔),选择自动化测试框架(比如XUnit)以及需要哪些手动测试(包括探索性测试,可用性测试等),还要规划每个发布周期需要进行的测试阶段(比如新功能测试,回归测试等),之后测试策略会对敏捷团队的开发及测试起到非常重要的指导作用,当然,每个团队因为项目的不同策略也会不同。
下图就是一个简单的敏捷测试策略介绍:
不需要测试文档
测试文档通常包括测试计划,测试用例,测试报告,测试缺陷等文档以及相对应的可以指导测试的一部分需求文档。
很多人会认为,敏捷软件测试是不需要文档的,敏捷宣言中有一句“工作的软件 高于 详尽的文档”,尽管敏捷宣言最后提到了“右项也有价值,我们更重视左项的价值”,但人们往往会忽视右项的内容,导致在很多刚开始实施敏捷开发的团队中完全否定了测试文档的作用。
首先不可否认,在实际的敏捷项目中,确实很少见传统意义上的正式的专门的需求文档和测试文档,但这并不代表敏捷项目没有文档,比如用户故事本身就是需求的载体,用户故事中的验收条件就是敏捷测试文档的一部分, 另外很多敏捷软件项目都会采用BDD的方式进行开发,将测试用例用业务人员能够看懂的自然语言描述,并结合自动化实现,形成一个融需求和测试为一体的文档,而且为了应对敏捷软件测试变化快文档更新不及时导致的问题,很多敏捷项目都在使用Living document。
纯自动化测试 or 纯手动测试
有些刚接触敏捷的人认为敏捷软件开发发布周期很短, 测试人员根本没有时间做手动测试, 所以应该采用纯自动化测试。
也有一些人认为,敏捷开发强调快速响应变化,如果投入成本在自动化测试上,那么肯定会导致维护自动化测试带来的资源浪费,所以应该采用纯手动测试。
这是两种极端的误解,虽然这两种观点所考虑到的难点确实存在, 因为在敏捷软件开发过程中, 迭代通常比较短,确实不会预留足够多的时间来做手动测试, 所以必须要有足够多的自动化测试来保障。
然而因为测试代码本身可能存在缺陷,而且有很多部分难以被自动化测试覆盖(比如界面的测试,可用性测试,探索性测试等),所以敏捷测试也同样离不开手动测试。
至于关于自动化测试维护成本的顾虑,敏捷项目也确实存在变化比较多的特点,但通常变的都是比较接近用户的部分,所以应该尽量减少用户级别的依赖界面的自动化测试,而多增加一些不容易变化的底层的单元测试接口测试等。
推荐敏捷测试以自动化测试为主,手动测试为辅。
敏捷QA = 敏捷Tester
在很多刚接触敏捷实践的团队中,大家对敏捷QA的认识还停留在Tester的阶段,认为只要用户故事开发完成之后,QA去专职地做测试,发现缺陷就够了。
这是一个很大的误解,首先QA(Quality Assurance/Analyst),不是单纯意义的测试人员,通过这个角色的名称我们可以看的出来敏捷QA强调的是质量保障和分析,而不单纯是测试产品。
在实际的项目中,敏捷QA通常会从需求分析阶段就开始参与整个软件开发过程,通过在不同阶段和团队中的不同角色合作,帮助整个团队对质量达成共识,并通过在不同阶段的确认和验证做到缺陷预防,而不是等到软件开发完成后再去发现缺陷,所以对于敏捷QA来说,其目标是软件开发完成后能够发现的缺陷越少越好,而对于Tester来说,发现越多的缺陷证明工作做得越优秀。
非功能性测试不重要
非功能性测试指的是针对非功能性需求(软件本身满足用户需求所必需的功能性需求以外的一些特性,比如安全,性能,可用性,兼容性等)的测试,通常包括安全测试,性能测试,可用性测试,兼容性测试等。
在敏捷软件项目中,需求被切割成了很小的单元,在切割的过程中,非功能性需求是最容易被人忽略的一部分,而这导致的问题就是非功能性测试经常被团队忽略,久而久之,就会形成这样一个误解,认为非功能性测试是不重要的。
这个观点非常不对,首先非功能性测试的重要性并不会因为软件开发模式的不同而有所不同,尤其安全测试和性能测试的重要性正越来越多地被重视起来,因为很多产品必须考虑到用户敏感信息的安全以及性能导致的用户满意度,在敏捷项目中由于软件会尽早发布,如果这些非功能性需求出现问题,就会更早地造成影响,很可能在软件刚步入市场就损失掉大多数的用户。
所以非功能性的测试和功能性测试同等重要,在实际的项目中,比较好的做法是将这些非功能性需求也加入到用户故事的验收条件中,在整个敏捷开发流程中对这些非功能性需求进行验证。
质量是QA的事儿
受传统观念的影响,很多人还是会认为质量是QA的事儿,如果产品发布后质量不好是QA的问题,其他角色和质量没有太大的关系。
首先这种认识太高估了QA对质量的作用,软件的质量是在软件开发过程中逐步形成的, 从需求分析阶段是否真正的了解到了客户想要的功能,到开发阶段是否增加了足够多的自动化测试保障,是否写了足够健壮的产品代码,到最后测试阶段是否测试了功能引入后整个系统的可用性,不同用户路径是否能正常工作等等,这些都是软件质量的组成部分。
可以看得出来,在整个过程中,软件的质量离不开敏捷团队各种角色的付出,其中有业务分析人员对需求的准确把握,有开发人员对产品代码的高标准实现,对自动化测试覆盖率的保障,还有QA在整个过程中对质量相关活动的实施和保障,包括需求分析阶段从QA的视角对业务的补充,开发阶段对自动化测试的审查,以及探索性测试可用性测试等对产品质量的进一步保障。
所以在敏捷测试中更多时候我们会淡化角色的概念,强调团队人人都为质量负责,这样更有助于团队的每一位成员都把质量作为非常重要的一部分,而不是依赖于某个人或者某个角色。
开发可以写测试,不再需要QA了
因为敏捷团队强调人人都为质量负责,开发人员会采用TDD等方式写大量的自动化测试,那么是不是就不需要QA了?
对于这个观点,在社区有过很多激烈的讨论,比如这篇文章《我们需要专职的QA吗?》就曾经引起了很大的争议,其实个人认为这篇文章里提到的QA指的是Tester,具体两者的区别可参考前面的观点;抛开这个,作者的某些观点其实是很有价值的,比如作者最后提到了质量不是测出来的,要通过软件生命周期各个阶段相关活动的保障,而这些活动都离不开QA的参与。
首先需求分析阶段,QA可以从不同的视角对于需求提出疑问,补充,修改,因为QA特有的技术背景,对于软件的可用性等有更深入的理解,所以往往可以提出不同于业务分析师和开发人员的观点;开发阶段,QA也会审查开发人员写的自动化测试,通过QA的专业测试背景帮助开发人员写更有价值的测试,比如我们在项目中曾经发现开发人员写了很多没有业务价值的测试;测试阶段,探索性测试,可用性测试,安全测试,性能测试等都是QA们在做的事情。
当然,如果业务分析师从各种视角把业务分析的透彻完美,开发人员可以写非常有价值的测试,也可以做各种类型的手动测试,那么去掉专职QA也不是不可以,那样的话不是不需要QA,而是人人都是QA。
结论
以上列出来的七点是刚刚接触敏捷测试时很容易进入的误区,甚至有的观点在一些已经施行敏捷很长时间的团队中仍然存在,这些观点很容易导致敏捷测试走上弯路,以上是结合实际项目经验个人的一些思考,希望对大家有所帮助。
软件开发
2020-06-03 11:54:00
内存管理可以说是任何基于 C/C++ 的系统程序的基石,需要谨慎对待。
我们知道原生的 Python 解释权就是用 C 编写的,它的内部有一套自己的内存管理方案。
这篇文章就是要来探究这套管理方案的实现细节,我们将会看到 Python 运行时内存是如何组织的,创建一个对象需要的内存又是如何分配的以及不再需要的垃圾内存又是如何被回收的。
本文基于 Python 2.7。
概览
如果把一台计算机中所有的内存看做一块蛋糕的话,要从这块蛋糕成功切下一小块给一个 Python 对象,需要经过一个个层级的申请。
最底层,也就是图中的 -2 层,就是原始的物理存储,包含主存与二级存储;在这之上也就是 -1 层是我们的操作系统层,系统内核负责对物理存储进行管理和分配。
再上一层来到 0 层,这一层负责向操作系统申请内存,典型的如 C 语言的 malloc 库,Python 运行时需要的内存最终要通过这一层从操作系统申请;0 层往上才是 Python 运行时的内存管理层级。
第 1 层中提供的PyMem 相关 API 主要是为了屏蔽不同平台 malloc/free 的差异,比如对于 malloc(0),有的系统返回 NULL,有的系统返回一个指针但是指向的地方没有内存。
解决这个问题的方法简单粗暴,就是不允许分配 0 内存,至少分配 1,PyMem_MALLOC(0) 会被转化成 malloc(1)。 #define PyMem_MALLOC(n) ((size_t)(n) > (size_t)PY_SSIZE_T_MAX ? NULL \ : malloc((n) ? (n) : 1)) #define PyMem_REALLOC(p, n) ((size_t)(n) > (size_t)PY_SSIZE_T_MAX ? NULL \ : realloc((p), (n) ? (n) : 1)) #define PyMem_FREE free
第 2 层从第 1 层获取内存,这些内存如何组织,如何分配给第 3 层的各种对象,以及垃圾内存是怎么被回收的就是这篇文章要关注的点。
pool
一个 pool 可以理解为一块大小为 4k(一个系统内存页的大小) 的蛋糕。当要为一个对象分配内存时,会从某个 pool 切下来一块分给这个对象。这里存在的限制是: 每个 pool 能被切下来的蛋糕大小是固定的,依次是 8,16,32,…,256 bytes,每个切下来的蛋糕块就是一个 block; 超过 256 bytes 的蛋糕块不再通过 pool 切,而是由 malloc 直接从操作系统申请; 请求 0 bytes 的蛋糕块也不通过 pool 切,0 会被改成 1 由 malloc 直接从操作系统申请;
这样的话,对于 [1, 256] bytes 这个范围内存请求与实际分配的内存(block)的关系如下: Request in bytes Size of allocated block Size class idx ---------------------------------------------------------------- 1-8 8 0 9-16 16 1 17-24 24 2 25-32 32 3 33-40 40 4 41-48 48 5 49-56 56 6 57-64 64 7 65-72 72 8 ... ... ... 241-248 248 30 249-256 256 31
现在我们回过头来聚焦到单个 pool ,可以想象,在任意时刻,这个 pool 会有三种状态: used:已经切分了部分小蛋糕块(block)出去,但还有剩余; full:所有的蛋糕块都被切分出去了; empty:还没有切分蛋糕块出去;
Python 会在每个 pool 的开始位置存放这个 pool 能切分蛋糕块大小以及当前状态等相关信息,它们存放在 pool_header 结构体中,其定义如下: struct pool_header { union { block *_padding; uint count; } ref; /* number of allocated blocks */ block *freeblock; /* pool's free list head */ struct pool_header *nextpool; /* next pool of this size class */ struct pool_header *prevpool; /* previous pool */ uint arenaindex; /* index into arenas of base adr */ uint szidx; /* block size class index */ uint nextoffset; /* bytes to virgin block */ uint maxnextoffset; /* largest valid nextoffset */ };
假设现在有一块 4K 内存要被初始化为一个 pool,这个 pool 只能切割出 16 bytes 大小的 block ,下面是它的初始化过程: /* * Initialize the pool header, set up the free list to * contain just the second block. */ pool->szidx = size; size = INDEX2SIZE(size); bp = (block *)pool + POOL_OVERHEAD; pool->nextoffset = POOL_OVERHEAD + (size << 1); pool->maxnextoffset = POOL_SIZE - size; pool->freeblock = bp + size; *(block **)(pool->freeblock) = NULL; UNLOCK(); return (void *)bp;
pool_header 中 szidx 存放的就是 size class index,这里只能切割 16 bytes ,那么传过来的 size 就是 size class index,对应的值必定是 1。
INDEX2SIZE 是一个宏,将 size class index 转化成对应的 block 的大小,这里将 1 转化成 16 又赋给了 size,size 变成了 16。 #define ALIGNMENT_SHIFT 3 #define INDEX2SIZE(I) (((uint)(I) + 1) << ALIGNMENT_SHIFT)
POOL_OVERHEAD 也是一个宏,它的作用是将 pool_header 占用的内存对齐为 8 的倍数。对齐后的位置才是可分配 block 的开始位置,这个位置值存放在了 bp中,最后作为当前内存分配请求的结果被返回。 #define ALIGNMENT 8 #define ALIGNMENT_MASK (ALIGNMENT - 1) #define ROUNDUP(x) (((x) + ALIGNMENT_MASK) & ~ALIGNMENT_MASK) #define POOL_OVERHEAD ROUNDUP(sizeof(struct pool_header))
nextoffset 指向的是这个 pool 中下一个处女 block,也就是还没有被分配给任何对象使用过的 block(被使用后释放的 block 会被加入到 freeblock 链表),maxnextoffset 指向了这个 pool 的最后一个 block,freeblock 被初始化为下一个可用的 block,又通过二级指针将 block 里面的值设置为 NULL,当这个 pool 里面有一个 block 需要被回收时,也会通过类似的二级指针操作加入到 freeblock 中: /* 回收 p 指向的 block */ *(block **)p = lastfree = pool->freeblock; pool->freeblock = (block *)p;
这样 freeblock 就形成了一个特殊的链表结构。
某一时刻一个处于 used 状态的 pool 的可能的内存分布图:
usedpools
所有处于 used 状态的 pool 都是通过 usedpools —— 一个特殊的数组管理的,前面没有介绍的 szidx 在这里排上用场了。
我们已经知道 szidx 的取值范围是 [0, 31],Python 会初始化一个特殊的数组存放所有处于 used 状态的 pool,这个数组的定义如下: #define SMALL_REQUEST_THRESHOLD 256 #define NB_SMALL_SIZE_CLASSES (SMALL_REQUEST_THRESHOLD / ALIGNMENT) /* 256 / 8 = 32 */ ​ #define PTA(x) ((poolp )((uchar *)&(usedpools[2*(x)]) - 2*sizeof(block *))) #define PT(x) PTA(x), PTA(x) ​ static poolp usedpools[2 * ((NB_SMALL_SIZE_CLASSES + 7) / 8) * 8] = { PT(0), PT(1), PT(2), PT(3), PT(4), PT(5), PT(6), PT(7) #if NB_SMALL_SIZE_CLASSES > 8 , PT(8), PT(9), PT(10), PT(11), PT(12), PT(13), PT(14), PT(15) #if NB_SMALL_SIZE_CLASSES > 16 , PT(16), PT(17), PT(18), PT(19), PT(20), PT(21), PT(22), PT(23) #if NB_SMALL_SIZE_CLASSES > 24 , PT(24), PT(25), PT(26), PT(27), PT(28), PT(29), PT(30), PT(31) #endif /* NB_SMALL_SIZE_CLASSES > 24 */ #endif /* NB_SMALL_SIZE_CLASSES > 16 */ #endif /* NB_SMALL_SIZE_CLASSES > 8 */ };
这个数组的设计很巧妙,我们结合图片来看,下面是 usedpools 初始化后的内存布局:
每个元素的初始化值为相应 szidx 在数组中的开始位置向前偏移 2 个 sizeof(block ) 的距离,也就是图中 2 个方块的距离。
如果需要检查是否有 szidx 为 1 处于 used 状态的 pool,我们来到 usedpools[1 + 1],也就是图中的 p,如果把 p 当做 pool_head 指针,那么 nextpool 就是 szidx 为 2 的第一个内存块,因为 p 到 nextpool 之间间隔的俩块内存大小为 2sizeof(block *) ,正好抵消了 union { block *_padding; uint count; } ref; block *freeblock;
占用的内存,这里存放的正是 p !这时候 p->nextpool 与 usedpools[1 + 1] 是相等的,也就是说 szidx 为 1 还没有对应的处于 used 状态的 pool。
假设现在申请到了一个 szidx 为 1 的 pool,在其 header 被初始化之前,Python 首先做的其实是把它放到 usedpools 中: init_pool: /* Frontlink to used pools. */ next = usedpools[size + size]; /* == prev */ pool->nextpool = next; pool->prevpool = next; next->nextpool = pool; next->prevpool = pool; pool->ref.count = 1;
此时 usedpools 的布局:
这时候 usedpools[1 + 1] != pool->nextpool,也就是说存在 szidx 为 1 的可用 pool。
现在再结合 pool 来看请求的内存是如何通过 usedpools 分配出去的,这也是整个内存分配中最常见的情况: /* 请求 nbytes 个字节,转化成 szidx */ size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT; /* 找到对应的 usedpool */ pool = usedpools[size + size]; /* 判断 usedpool 是否可用,不等于说明 usedpool 可用 */ if (pool != pool->nextpool) { ++pool->ref.count; bp = pool->freeblock; /* used 状态的 pool 必定存在 freeblock */ assert(bp != NULL); /* 如果 freeblock 链表元素多余一个,将当前指向的 block 返回并将 freeblock 指向下一个 block */ if ((pool->freeblock = *(block **)bp) != NULL) { UNLOCK(); return (void *)bp; } /* freeblock 的数量只有一个,说明分配出去的都没有释放,因为只有一个,当前的 freeblock * 被分配出去以后需要指向一个新的 block,那么就需要开荒下一块处女地,也就是 nextoffset * 指向的 block,需要先判断是否到达了最后一块 bock */ if (pool->nextoffset <= pool->maxnextoffset) { /* freeblock 指向处女地 */ pool->freeblock = (block*)pool + pool->nextoffset; /* nextoffset 指向下一块处女地 */ pool->nextoffset += INDEX2SIZE(size); *(block **)(pool->freeblock) = NULL; UNLOCK(); return (void *)bp; } /* 走到这里说明当前 freeblock 指向的是最后一块可用的 block 了,分配出去以后 * 这个 pool 也就由 used 状态变成了 full 状态,需要从 usedpools 中移除, * 也即是将 usedpools 中对应的位置恢复到初始化时的状态 */ next = pool->nextpool; pool = pool->prevpool; next->prevpool = pool; pool->nextpool = next; UNLOCK(); return (void *)bp; }
arena
如果 usedpools 对应 szidx 位置里并没有可用 pool ,Python 会申请一块 4k 内存进行初始化,放入到 usedpools 中。
那么又是从哪申请的 4k 内存呢?答案是 arena。
简单来说,如果 pool 是一块 4k 大小的蛋糕,那么 arena 就是一块 256k 大小的蛋糕。当 usedpools 中与某个 szidx 对应的 pool 不存在时就会从 arena 这个更大块的蛋糕上切一块 4k 的小块,初始化为 pool 放入到 usedpools 中。
详细一点来说,arena 只是一个 struct 结构体,里面有一个指针指向 256k 内存,arena 定义如下: /* Record keeping for arenas. */ struct arena_object { /* 与 pool 不同,pool 的 header 是包含在 4k 内存里面的,arena 的 * header 与它管理的 256k 内存是相关独立的,256k 内存的地址存在这里 */ uptr address; ​ /* 下一块 pool 处女地的开始地址,类似于 pool 中的 nextoffset */ block* pool_address; ​ /* 剩余可用 pool 的数量,可以推断其初始值必定是 256k / 4k = 64 */ uint nfreepools; ​ /* 可用 pool 的总数量,可以推断其值必定是 256k / 4k = 64 */ uint ntotalpools; ​ /* 一个单链表,将所有可用的 pool 链接起来,类似于 pool 中的 freeblock */ struct pool_header* freepools; ​ struct arena_object* nextarena; struct arena_object* prevarena; };
arena 并不是系统初始化就存在的,本着不能浪费的原则,系统每次只申请一块 256k 的内存,用完了才会申请第二块,但是系统会一次初始化多个 arena_object 头结构,由 arenas 管理,arenas 是一个全局的 arena_object 指针: /* Array of objects used to track chunks of memory (arenas). */ static struct arena_object* arenas = NULL;
大家看到 arenas 是一个指针,结合 arena_object 定义中的 nextarena 和 prevarena,可能会以为 arenas 指向的是双向链表,但是并不是,arenas 其实是一个数组。
nextarea 是负责将所有还没有使用(包括用完释放的)的 arena_object 连接成一个单链表,头指针是 unused_arena_objects。
此外,next arena 和 prevarena 共同负责将在使用中的 arena_object 连接成一个双链表,头指针是 usable_arenas,这俩个头指针的定义如下: static struct arena_object* unused_arena_objects = NULL; static struct arena_object* usable_arenas = NULL
还有俩个相关的定义: /* 上一次创建的 arena_object 的数量 */ static uint maxarenas = 0; ​ /* 首次创建的 arena_object 的数量 */ #define INITIAL_ARENA_OBJECTS 16 ​
现在可以来看一下 arenas 的初始化了: /* 没有还没有使用的 arena_object 才进行新的初始化 */ if (unused_arena_objects == NULL) { uint i; uint numarenas; size_t nbytes; ​ /* maxarenas 为 0 表示首次创建,创建数量为 16, * 如果不为 0,创建数量为上次创建数量的 2 倍 */ numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS; ​ /* overflow 相关判断略过 */ ​ /* 计算 numarenas 个 arena_object 占用的内存然后申请 */ nbytes = numarenas * sizeof(*arenas); /* 申请内存,注意,用的是 realloc,表示扩大当前的数组 */ arenaobj = (struct arena_object *)realloc(arenas, nbytes); if (arenaobj == NULL) return NULL; arenas = arenaobj; ​ /* 将新创建的 arena_object 链接到 unused_arena_objects */ for (i = maxarenas; i < numarenas; ++i) { arenas[i].address = 0; /* 还没有分配内存 */ arenas[i].nextarena = i < numarenas - 1 ? &arenas[i+1] : NULL; } ​ /* 更新全局变量 */ unused_arena_objects = &arenas[maxarenas]; maxarenas = numarenas; }
假设是首次初始化 arenas,经过上面的步骤后,arenas 现在是包含 16 个 arena_object 的数组,unused_arena_objects 则是包含 16 个 arena_object 的链表,下面才进入到切蛋糕给 arena 的环节: /* 取第一个没有使用的 area_object */ assert(unused_arena_objects != NULL); arenaobj = unused_arena_objects; unused_arena_objects = arenaobj->nextarena; assert(arenaobj->address == 0); ​ /* 切 256k 大小的蛋糕给这个 area_object */ arenaobj->address = (uptr)malloc(ARENA_SIZE); /* 内存分配失败 */ if (arenaobj->address == 0) { ... } /* 更新全局变量 */ ++narenas_currently_allocated; ​ /* 设置 arena_object 的初始化信息 */ /* freepools 指向的是用完释放的 pool,所以初始化的时候是 NULL */ arenaobj->freepools = NULL; arenaobj->pool_address = (block*)arenaobj->address; /* 可以切出来的 pool 的剩余数量 */ arenaobj->nfreepools = ARENA_SIZE / POOL_SIZE; assert(POOL_SIZE * arenaobj->nfreepools == ARENA_SIZE); ​ excess = (uint)(arenaobj->address & POOL_SIZE_MASK); if (excess != 0) { --arenaobj->nfreepools; arenaobj->pool_address += POOL_SIZE - excess; } /* 可以切出来的 pool 的总数量数量 */ arenaobj->ntotalpools = arenaobj->nfreepools; ​ return arenaobj;
上面的俩段代码整合起来就是这个函数的逻辑: static struct arena_object* new_arena(void);
当要切一块蛋糕给 pool 时,如果还没有可以用的 arena,就会调用 new_arena进行初始化: /* 没有可用的 arena */ if (usable_arenas == NULL) { /* 创建新的 arena */ usable_arenas = new_arena(); if (usable_arenas == NULL) { UNLOCK(); goto redirect; } usable_arenas->nextarena = usable_arenas->prevarena = NULL; } assert(usable_arenas->address != 0);
从初始化后的 arena 中切下4k 给然后初始化为 pool: /* pool_address 指向的处女地给 pool */ pool = (poolp)usable_arenas->pool_address; pool->arenaindex = usable_arenas - arenas; assert(&arenas[pool->arenaindex] == usable_arenas); /* pool 的 szidx 初始值为 0xffff */ pool->szidx = DUMMY_SIZE_IDX; /* pool_address 移到下一个位置 */ usable_arenas->pool_address += POOL_SIZE; /* anena 中剩余可用的 pool 数量减 1 */ --usable_arenas->nfreepools; ​ goto init_pool;
在这个切给 pool 以后,如果剩余的可用 pool 的数量 nfreepools 变成 0 了,那么这个 arena 就不再是可用的 arena 了,所有在减少 nfreepools 的操作后,有下面的处理: if (usable_arenas->nfreepools == 0) { assert(usable_arenas->nextarena == NULL || usable_arenas->nextarena->prevarena == usable_arenas); /* 从 usable_arenas 链表中移除 */ usable_arenas = usable_arenas->nextarena; if (usable_arenas != NULL) { usable_arenas->prevarena = NULL; assert(usable_arenas->address != 0); } }
当一个 pool 中的 block 都被释放后,这个 pool 就变成了一个 freepool,多个 freepool 链接成一个链表,链表头正是 arena 中的 freepools,相当于一个缓存,这一点和 freeblock 很相似,从可用的 arena 中申请 pool 是优先从 freepools 获取的: /* 尝试从 freepools 获取一个 pool */ pool = usable_arenas->freepools; if (pool != NULL) { /* freepools 指向下一个 */ usable_arenas->freepools = pool->nextpool; --usable_arenas->nfreepools; if (usable_arenas->nfreepools == 0) { /* 最后一个 pool 被分配出去了,arena 变成了 full 状态,需要从 usable_arenas 移除 */ assert(usable_arenas->freepools == NULL); assert(usable_arenas->nextarena == NULL || usable_arenas->nextarena->prevarena == usable_arenas); ​ usable_arenas = usable_arenas->nextarena; if (usable_arenas != NULL) { usable_arenas->prevarena = NULL; assert(usable_arenas->address != 0); } } else { /* nfreepools > 0: 那么 freepools 必定不为空,或者 arena 中还有未被切出来的 pool */ assert(usable_arenas->freepools != NULL || usable_arenas->pool_address <= (block*)usable_arenas->address + ARENA_SIZE - POOL_SIZE); }
现在可以来看看某一时刻 Python 程序 block,pool 和 arean 组成的内存池的全景了。
内存回收
Python 中一切都是对象,每个对象都有一个 refcnt 变量记录这个对象的引用计数,当引用计数变成零的时候,这个对象就会被回收,内存被释放(这句话不是绝对准确,不过这不是本文重点,我们姑且就这样认为吧)。这里说的内存被释放,并不是立刻还给操作系统,而是还给 pool。
假设要释放 p 指向的内存,最简单的情形是,交还给 pool 后,pool 依然处于 used 状态,这时候只需要将 p 添加到 pool 的 freeblock 链表中即可: assert(pool->ref.count > 0); /* pool 不是 empty 状态 */ /* 获取当前 freeblock 的值给 lastfree,再 p 链接到 freeblock */ *(block **)p = lastfree = pool->freeblock; pool->freeblock = (block *)p; if (lastfree) { struct arena_object* ao; uint nf; /* ao->nfreepools */ ​ /* 释放 p 前,pool 是 used 状态,释放后,ref.count(先减 1)不为 0,说明还是 used * 状态,这就是最简单的情形,不需要额外的改变 */ if (--pool->ref.count != 0) { /* pool isn't empty: leave it in usedpools */ UNLOCK(); return; } ​ ... }
如果释放 p 前,lastfree 是 NULL,那就说明 pool 处于 full 状态,当 p 释放后,pool 由 full 变成了 used 状态,这时候需要将这个 pool 放回到 usedpools 的对应位置中,并和对应位置上的值链接成链表: if (lastfree) { ... } ​ --pool->ref.count; assert(pool->ref.count > 0); size = pool->szidx; next = usedpools[size + size]; /* 得到在 usedpools 中的位置 */ prev = next->prevpool; /* pool 插入到 next 前面: prev <-> pool <-> next */ pool->nextpool = next; pool->prevpool = prev; next->prevpool = pool; prev->nextpool = pool; UNLOCK(); return;
假设 pool 只分配出去了一个 block,就是 p,那么现在 p 释放后,pool 的状态将由 used 变成 empty,这时候 Python 会将 这个 pool 从 usedpools 中移除并链接到所在 arena 的 freepools 中: if (--pool->ref.count != 0) { ... } /* pool 状态变成 empty,从 usedpools 中移除 */ next = pool->nextpool; prev = pool->prevpool; next->prevpool = prev; prev->nextpool = next; ​ /* 加入到所在 arena 的 freepools 中 */ ao = &arenas[pool->arenaindex]; pool->nextpool = ao->freepools; ao->freepools = pool; nf = ++ao->nfreepools;
以上三种情况就是当一个对象的内存被释放后, pool 的状态变化和相应处理。
总的来说就是将 block 归还给 pool,pool 变成 empty 再归还给 arena,Python 2.5 之前的内存释放逻辑到这里就结束了。
但是这里还隐藏了一个问题,那就是从始至终都没有归还 arena 的内存!
假如一个应用程序在开一始由于某种需要申请了大量内存,到内面不需要这么多内存了,释放的内存归还给了 arena,但是 arena 不会再归还给操作系统,这就造成类似内存泄漏的效果!
虽然这种情况很少见,但小概率事件必然发生,还是有人碰到了。后来 Python 紧接着上面的代码后面加入了 arena 管理解决这个问题。
现在考虑当一个 pool 归还给 arena 后, 也将引起 arena 的状态变化: 由 used 状态变成了 free 状态 由 full 状态变成了 used 状态 释放之前是 used 状态,释放后还是 used 状态 不做任何处理
对于第一种情况,Python 会释放整个 arena 占用的内存: /* nfreepools == ntotalpools:说明 arena 变成 empty */ if (nf == ao->ntotalpools) { /* Case 1. First unlink ao from usable_arenas. */ assert(ao->prevarena == NULL || ao->prevarena->address != 0); assert(ao ->nextarena == NULL || ao->nextarena->address != 0); ​ /* Fix the pointer in the prevarena, or the * usable_arenas pointer. */ if (ao->prevarena == NULL) { usable_arenas = ao->nextarena; assert(usable_arenas == NULL || usable_arenas->address != 0); } else { assert(ao->prevarena->nextarena == ao); ao->prevarena->nextarena = ao->nextarena; } /* Fix the pointer in the nextarena. */ if (ao->nextarena != NULL) { assert(ao->nextarena->prevarena == ao); ao->nextarena->prevarena = ao->prevarena; } /* Record that this arena_object slot is * available to be reused. */ ao->nextarena = unused_arena_objects; unused_arena_objects = ao; ​ /* Free the entire arena. */ free((void *)ao->address); ao->address = 0; /* mark unassociated */ --narenas_currently_allocated; ​ UNLOCK(); return; }
第二种情况,将 arena 直接插入到 usable_arenas 头部: /* ntotalpools == 1:表示 arena 由 full 变成了 used */ if (nf == 1) { ao->nextarena = usable_arenas; ao->prevarena = NULL; if (usable_arenas) usable_arenas->prevarena = ao; usable_arenas = ao; assert(usable_arenas->address != 0); ​ UNLOCK(); return; }
第三种情况,由于状态没有变化,arena 还是处于 usable_arenas 中。而 usable_arenas 是按照 arenas 中 nfreepools 的数量从小到大排序的。
为什么要这样做呢?
分配 arena 是优先从 usable_arenas 头获取了,这样的话,nfreepools 越大越靠后被使用的机会也就越小,随着其中 pool 的释放早点归还给操作系统的机会也就越大。
现在我们可以看出来 Python 内存管理总的原则就是:能不占着的内存就不占着,能早点归还给系统就早点归还。
现在一个 pool 释放,nfreepools 的数量增加,如果增加后比它右边 arena 的 nfreepools 要多,就需要重新调整顺序: /* 首先将 ao 从 useable_arenas 中移出来,分来 ao 是头结点和不是头结点俩种情况 */ if (ao->prevarena != NULL) { /* ao 不是头结点 */ assert(ao->prevarena->nextarena == ao); ao->prevarena->nextarena = ao->nextarena; } else { /* ao 是头结点 */ assert(usable_arenas == ao); usable_arenas = ao->nextarena; } ao->nextarena->prevarena = ao->prevarena; ​ /* 遍历 usable_arenas 链表,找到 ao 要插入的位置 */ while (ao->nextarena != NULL && nf > ao->nextarena->nfreepools) { ao->prevarena = ao->nextarena; ao->nextarena = ao->nextarena->nextarena; } ​ /* 插入到新的位置 */ assert(ao->nextarena == NULL || ao->prevarena == ao->nextarena->prevarena); assert(ao->prevarena->nextarena == ao->nextarena); ​ ao->prevarena->nextarena = ao; if (ao->nextarena != NULL) ao->nextarena->prevarena = ao;
第四种情况就是第三种的特例,虽然 nfreepools 数量增加但是并没有超过它右边 arena 的 nfreepools 数量,这时候什么不用做什么: if (ao->nextarena == NULL || nf <= ao->nextarena->nfreepools) { UNLOCK(); return; }
这一节的代码整合起来就是 void PyObject_Free(void *p);
这个函数的逻辑。
总结
本文详细剖析了 Python 运行时的内存组织方式、对象的内存分配以和内存回收过程。核心的概念包括 block、pool、usedpools 和 arenas。
本文的代码片段整合起来就是 obmalloc.c 中最主要的三个函数: void PyObject_Free(void *p); void *PyObject_Malloc(size_t nbytes); static struct arena_object* new_arena(void);
在 Python3 中,block 最大值增加到了 512,对应的 szindex 最大到 63,除此之外,整个内存的组织和分配、回收逻辑并没有什么变化。 原文链接: https://protream.com/2020/how-python-manage-memory-part-one/
文源网络,仅供学习之用,如有侵权请联系删除。
在学习Python的道路上肯定会遇见困难,别慌,我这里有一套学习资料,包含40+本电子书,800+个教学视频,涉及Python基础、爬虫、框架、数据分析、机器学习等,不怕你学不会! https://shimo.im/docs/JWCghr8prjCVCxxK/ 《Python学习资料》
关注公众号【Python圈子】,优质文章每日送达。
软件开发
2020-06-03 15:40:00
今天和大家聊一聊关于如何设计测试用例,以及如何提高测试用例的覆盖度?

首先,写测试用例几乎是每一个测试工程师,无论你是功能测试还是自动化测试乃至测试开发工程师入行首先就要掌握的技能,也是很多测试工程师的日常工作。

可能你看过不少设计测试用例的网课或者帖子,一堆测试用例方法让人一脸懵逼。比如等价类划分法、边界值分析法、场景法、因果图方法、判定表驱动分析法、正交实验设计方法、功能图分析方法、场景设计方法......

但是从应付日常工作来讲,真正具有实用价值并且常用的只有前三种方法等价类划分法,边界值法以及场景法。边界值法和等价类划分法通常配合使用,等价类划分又分为有效等价类和无效等价类,下面举例来讲一下这三种方法:

以一个登录功能账号输入为例,要求只能输入字母,数字和下划线 ,位数要求6到19位。

首先,先说下有效等价类。拿到这个需求你不可能拿6到19位之间的位数去穷举,那是无穷无尽的。运用等价类的划分的思想我们就可以取6位,19位,7位,18位,同时还可以取一个中间位12位更具代表性。然后确定位数之后,就可以设计用例了,字母,数字,下划线,分别排列组合分散开即可。

再说下无效等价类,就是不满足条件的取5位和20位去验证即可。除此之外还要考虑输入为空以及非法字符这两种情况。

边界值法这个比较简单就是取边界去验证,以及验证这个边界附近的数据即可。场景法就是需要考虑到各种各样的场景,比如该账号未注册就去登录,以及各种异常场景都需要考虑。一般,写好测试用例,都会开案例评审会议,就是防止场景遗漏。

当然设计测试用例的方法就那么几种,但是不同水平的测试工程师设计出来的用例覆盖度有很大差别。就好比大家把数学公式都掌握了,但是具体到解题考试,不同人千差万别。

下面以一道大家经常遇到的面试题为例,如何设计用户登录功能的测试用例?看到这里,你可能会说这也太初级了吧,先别急,等把下面的文章看完再说......

如果你是一个刚入行没多久的初级测试,你可能设计出下面的测试用例:
输入已注册的用户名和正确的密码,验证是否登录成功。
输入已注册的用户名和不正确的密码,验证是否登录失败,并且提示信息正确。
输入未注册的用户名和任意密码,验证是否登录失败,并且提示信息正确。
用户名和密码两者都为空,验证是否登录失败,并且提示信息正确。
用户名和密码两者之一为空,验证是否登录失败,并且提示信息正确。
如果登录功能启用了验证码功能,在用户名和密码正确的前提下,输入正确的验证码,验证是否登录成功。
如果登录功能启用了验证码功能,在用户名和密码正确的前提下,输入错误的验证码,验证是否登录失败,并且提示信息正确。

列出这些测试用例后,你可能已经觉得比较满意了,因为你感觉已经把自己的测试知识都用在这些用例设计中了。然而仔细思考一下,这些用例真的覆盖到所有场景了吗?

接下来,再看一下,一个有经验的中级工程师还会增加哪些场景呢?
用户名和密码是否大小写敏感。
页面上的密码框是否加密显示。
后台系统创建的用户第一次登录成功时,是否提示修改密码。
忘记用户名和忘记密码的功能是否可用。
前端页面是否根据设计要求限制用户名和密码长度。
如果登录功能需要验证码,点击验证码图片是否可以更换验证码,更换后的验证码是否可用。
刷新页面是否会刷新验证码。
如果验证码具有时效性,需要分别验证时效内和时效外验证码的有效性。
用户登录成功但是会话超时后,继续操作是否会重定向到用户登录界面。

看到这里,惊不惊喜,意不意外,没想到一个很简单的用户登录居然还能设计出这么多用例,原来自己开始想的,还有那么多场景遗漏。还有更刺激的请接着看,上面那些用例主要从功能层面进行考虑,安全考虑了吗?兼容性考虑了吗?性能考虑了吗?

接下来就是看大佬之所以成为大佬,凭什么他可以定级高级资深测试工程师,每月二三十k的原因......718897738软件测试技术交流,群里还有整理好的资料+工具下载

安全角度考虑测试用例 :

用户密码后台存储是否加密。
用户密码在网络传输过程中是否加密。
密码是否具有有效期,密码有效期到期后,是否提示需要修改密码,
密码输入框是否不支持复制和粘贴。
密码输入框内输入的密码是否都可以在页面源码模式下被查看。
用户名和密码的输入框中分别输入典型的“SQL 注入攻击”字符串,验证系统的返回页面。
同一用户在同一终端的多种浏览器上登录,验证登录功能的互斥性是否符合设计预期。
同一用户先后在多台终端的浏览器上登录,验证登录是否具有互斥性。


性能压力测试角度考虑测试用例 :

单用户登录的响应时间是否小于 3 秒。
单用户登录时,后台请求数量是否过多。
高并发场景下用户登录的响应时间是否小于 5 秒。
高并发场景下服务端的监控指标是否符合预期。
长时间大量用户连续登录和登出,服务器端是否存在内存泄漏。


兼容性角度考虑测试用例:

不同浏览器下,验证登录页面的显示以及功能正确性。
相同浏览器的不同版本下,验证登录页面的显示以及功能正确性。
不同移动设备终端的不同浏览器下,验证登录页面的显示以及功能正确性。
不同分辨率的界面下,验证登录页面的显示以及功能正确性。

看到这里,你还会觉得“用户登录”功能的测试非常简单、不值一提么?下次,面试的时候你如果从这些方面去回答,还怕拿不到offer吗?

当然这些都是需要经验积累的,但是并不妨碍我们在设计测试用例的时候可以从多个角度进行考虑,比如安全,性能,兼容性等等。那么,日积月累,从小牛进阶成为大牛便是指日可待了!
如果对python自动化测试、web自动化、接口自动化、移动端自动化、面试经验交流等等感兴趣的测试人,可以关注我。加入我们免费获取更多软件测试进阶资料!
软件开发
2020-06-03 14:30:00
1. 应用测试的介绍
一般我们在写完代码之后,代码的测试是会给专门的测试人员来测试的,如果一个测试跑到你的工位上对你说,你的代码好像有Bug,你肯定会不爽,反正我就是这样的🙃。所以为了显示自己的代码质量高一点,在功能提交给测试之前,我们会自己测试一下,接下来给大家介绍一下 Spring Boot Test 应用测试框架。
Spring Boot Test 其实就是Spring Test,只是在Spring Boot 中集成变得更简单了,像我们开发自己测试,一般都是单元测试Junit测试,不出bug就谢天谢地啦,Spring Test与JUnit结合起来提供了高效便捷的测试解决方案,而Spring Boot Test是在Spring Test之上增加了切片测试并增强了Mock能力。
Spring Boot Test支持的测试种类,主要分为以下三类: 单元测试,面向方法的测试,常用注解有@Test。(一般都是用这个) 功能测试,面向业务的测试,同时也可以使用切面测试中的Mock能力,常用的注解有@RunWith,@SpringBootTest等。(这个也用得多) 切片测试,面向难于测试的边界功能,介于单元测试和功能测试之间,常用注解有@RunWith,@WebMvcTest等。
测试过程中的关键要素及支撑方式如下: 测试运行环境,通过@RunWith和@SpringBootTest启动Spring容器。 Mock能力,Mockito提供Mock能力。 断言能力,AssertJ、Hamcrest、JsonPath提供断言能力。
接下来我带领大家学习如何简单使用Spring Boot Test框架。
2. Spring Boot Test 的使用 2.1 引入依赖
在Spring Boot中开启测试只需要引入spring-boot-starter-test依赖,使用@RunWith和@SpringBootTest注解就可以开始测试。我们就简单测试一下接口,首先我们引入pom依赖:
pom.xml : org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE org.springframework.boot spring-boot-starter-web 2.2.2.RELEASE org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.0 mysql mysql-connector-java 8.0.18 org.projectlombok lombok 1.18.10 org.springframework.boot spring-boot-test junit junit 4.12 test org.springframework spring-test 5.2.2.RELEASE test org.springframework.boot spring-boot-maven-plugin org.apache.maven.plugins maven-compiler-plugin -parameters 2.2 代码编写
测试代码我们一般都写在与main文件夹平级的test文件夹下,建议文件夹的名称和main文件夹下的文件夹对应好,测试类的名称也要对应好,就像下面这样,当然这只是建议。
Spring Boot 的启动类我就不再写了,没有加什么特别的内容,直接上测试类吧。
StudentServiceTest.java :
对了,这里测试的每一个方法都和 StudentService.java 这个服务类的方法名对应着的,建议大家也是这样。 package com.butterflytri.service; import com.butterflytri.TestApplication; import com.butterflytri.entity.Student; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import javax.annotation.Resource; import java.util.List; /** * @author: WJF * @date: 2020/5/23 * @description: StudentServiceTest */ /** * {@link SpringBootTest}:配置文件属性的读取。读取classes标志的启动类的配置文件和运行环境,并加载。 * {@link RunWith}:'RunWith'注解就是一个运行器,加载value的Class测试环境。 */ @SpringBootTest(classes = TestApplication.class) @RunWith(SpringRunner.class) public class StudentServiceTest { @Resource private StudentService studentService; @Test public void findAllTest() { List list = studentService.findAll(); for (Student student : list) { System.out.println(student); } } @Test public void findOneTest() { Student student = studentService.findOne(1L); System.out.println(student); } @Test public void findByStudentNoTest() { Student student = studentService.findByStudentNo("G030511"); System.out.println(student); } }
每一个测试方法都是可以独立运行的,因为在运行的时候会加载Spring Test的测试环境,同时会将你测试的工程的运行环境的配置加载进来,使用的都是工程的配置和环境,方便我们自己进行测试。
3. 项目地址
本项目传送门: GitHub ---> spring-boot-test Gitee ---> spring-boot-test
此教程会一直更新下去,觉得博主写的可以的话,关注一下,也可以更方便下次来学习。 作者:Butterfly-Tri 出处: Butterfly-Tri个人博客 版权所有,欢迎保留原文链接进行转载🙃
软件开发
2020-06-02 23:34:00
闭包是一个很有意思的东西,理解起来很绕,但是理解后很简单,网上的讲解也朦朦胧胧的.根据自己的理解解释一番:
预备知识:函数中的作用域 python中函数的作用域由def关键字界定,函数内的代码访问变量的方式是从其所在层级由内向外的. 即函数内使用某变量a,但是该变量在函数内部没有定义,那么会去函数外面寻找是否定义该变量a,若没有继续往外寻找. 函数外面定义的变量,函数内部可以读取使用,但是不能更改,会报错. a = 1 def res(): a = a + 1 #报错,a是外面的全局变量,函数内部不能改变 print(a) res() 但是可以返回函数内部的局部变量到外面,更改全局变量! a = 1 def res(): return a+1 a = res() print(a) #输出2,没有在函数内部改变全局变量a,而是在函数外面改变全局变量a
闭包目的: 有一个函数模板(函数中的一个模板参数可以改变),如下函数B,其模板参数a需要根据需要改变,设定这个函数的模板参数a而避免使用全局变量 def B(x): return a * 2 + x
通过类方法实现对一个函数参数的改变 class define_fun: def A(self,a): self.a = a def B(self,x): return 2*self.a + x DE = define_fun() #1.引入类 DE.A(3) #2.通过类方法设定另一个函数的参数 a res = DE.B(5) #3.根据设定的参数,计算结果 print(res) #输出11
通过全局变量,实现对一个函数的参数的改变 def A(a): return a def B(x): return a * 2 + x a = A(3) #1.定义一个变量a res = B(5) #2.函数B中没有设定参数a的值,会自动寻找外层的变量,即全局变量a,然后计算结果 print(res) #输出11
通过闭包,实现对一个函数的参数的改变 def A1(a): def B1(y): return 2*a+y return B1 funB = A1(3) #1.直接设定B1函数的模板参数a,返回的是改变模板参数a后的函数B1 (而A1函数内部的变量a会随该行执行结束而销毁) # 此时的funB应为函数B1(y),且return 2*3 +y res = funB(5) #2.使用设定好参数的B函数,计算结果 print(res) #输出11
总之: 三个方法都会有改变参数后的函数B 但是前面两个方法都有一个额外变量a/self.a一直存在 而使用闭包时,funB = A1(3)执行完毕后,变量a会随着作用域(函数A1)的消失而销毁.比前两者更好
闭包理解: 利用外层函数,设定内层函数的模板参数(内层函数是一个灵活的函数,它的模板参数可以灵活改变) 返回已设定好参数的内层函数供后面使用!
创建闭包函数: 闭包函数必须有内嵌函数 内嵌函数需要引用该嵌套函数上一级函数中的变量 闭包函数必须返回内嵌函数
此外:模板参数,为自创的,哈哈,即函数内部使用却没有在函数内部定义的参数
软件开发
2020-06-02 21:29:00
当你想查询一下火车票信息的时候,你还在上12306官网吗?或是打开你手机里的APP?
下面让我们来用Python写一个命令行版的火车票查看器, 只要在命令行敲一行命令就能获得你想要的火车票信息!如果你刚掌握了Python基础,这将是个不错的小练习。
接口设计
一个应用写出来最终是要给人使用的,哪怕只是给你自己使用。所以,首先应该想想你希望怎么使用它?
让我们先给这个小应用起个名字吧,既然及查询票务信息,那就叫它tickets好了。我们希望用户只要输入出发站,到达站以及日期就让就能获得想要的信息,所以tickets应该这样被使用: $ tickets from to date
另外,火车有各种类型,高铁、动车、特快、快速和直达,我们希望可以提供选项只查询特定的一种或几种的火车,所以,我们应该有下面这些选项:
-g 高铁
-d 动车
-t 特快
-k 快速
-z 直达
这几个选项应该能被组合使用,所以,最终我们的接口应该是这个样子的: $ tickets [-gdtkz] from to date
接口已经确定好了,剩下的就是实现它了。
开发环境
写Python程序的一个良好实践是使用virtualenv这个工具建一个虚拟的环境。我们的程序使用Python3开发,下面在你的工作目录下建一个文件夹tickets,进去创建一个虚拟环境: $ virtualenv -p /usr/bin/python3 venv
通过下面的命令激活它: $ . venv/bin/activate
解析参数
Python有很多写命令行应用的工具,如argparse, docopt, options...这里,我们选用docopt这个简单易用的工具,我们先安装它: $ pip3 install docopt
docopt可以按我们在文档字符串中定义的格式来解析参数,在tickets.py中: # coding: utf-8 ​ """Train tickets query via command-line. ​ Usage: tickets [-gdtkz] ​ Options: -h,--help 显示帮助菜单 -g 高铁 -d 动车 -t 特快 -k 快速 -z 直达 ​ Example: tickets 南京 北京 2016-07-01 tickets -dg 南京 北京 2016-07-01 """ from docopt import docopt ​ def cli(): """command-line interface""" arguments = docopt(__doc__) print(arguments) ​ if __name__ == '__main__': cli()
下面我们运行一下这个程序: $ python3 tickets.py 上海 北京 2016-07-01
我们得到下面的参数解析结果: {'-d': False, '-g': False, '-k': False, '-t': False, '-z': False, '': '2016-07-01', '': '上海', '': '北京'}
获取数据
参数已经解析好了,下面就是如何获取数据了,这也是最主要的部分。
首先我们打开12306,进入余票查询页面,如果你使用chrome,那么按F12打开开发者工具,选中Network一栏,在查询框钟我们输入上海到北京,日期2016-07-01, 点击查询,我们在调试工具发现,查询系统实际上请求了这个URL
https://kyfw.12306.cn/otn/lcxxcx/query?purpose_codes=ADULT&queryDate=2016-07-01&from_station=SHH&to_station=BJP
并且返回的是JSON格式的数据!接下来问题就简单了,我们只需要构建请求URL然后解析返回的JSON数据就可以了。
但是我们发现,URL里面from_station和to_station并不是汉字,而是一个代号,而用户输入的是汉字,我们要如何获取代号呢?我们打开网页源码看看有没有什么发现。
啊哈!果然,我们在网页里面找到了这个链接:
https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.8955
这里面貌似是包含了所有车站的中文名,拼音,简写和代号等信息, 我们在项目目录下将它保存为stations.html。但是这些信息挤在一起,而我们只想要中文名和大写字母的代号信息,怎么办呢? BINGO!正则表达式,我们写个小脚本来匹配提取出想要的信息吧, 在parse.py中: # coding: utf-8 import re from pprint import pprint ​ with open('stations.html', 'r') as f: text = f.read() stations = re.findall(u'([一-龥]+)\|([A-Z]+)', text) pprint(dict(stations), indent=4)
我们运行这个脚本,它将以字典的形式返回所有车站和它的大写字母代号, 我们将结果重定向到stations.py中, $ python3 parse.py > stations.py
我们为这个字典加名字,stations, 最终,stations.py文件是这样的: stations = { '一间堡': 'YJT', '一面坡': 'YPB', ... '龙镇': 'LZA', '龙骨甸': 'LGM' }
现在,用户输入车站的中文名,我们就可以直接从这个字典中获取它的字母代码了: ... from stations import stations ​ def cli(): arguments = docopt(__doc__) from_staion = stations.get(arguments['']) to_station = stations.get(arguments['']) date = arguments[''] # 构建URL url = 'https://kyfw.12306.cn/otn/lcxxcx/query?purpose_codes=ADULT&queryDate={}&from_station={}&to_station={}'.format( date, from_staion, to_station )
万事俱备,下面我们来请求这个URL获取数据吧!这里我们使用requests这个库, 先安装它: $ pip3 install requests
它提供了非常简单易用的接口, ... import requests ​ def cli(): ... # 添加verify=False参数, 不验证证书 r = requests.get(url, verify=False) print(r.json())
从结果中,我们可以观察到,与车票有关的信息需要进一步提取: def cli(): ... r = requsets.get(url); rows = r.json()['data']['datas']
显示结果
数据已经获取到了,剩下的就是提取我们要的信息并将它显示出来。prettytable这个库可以让我们它像MySQL数据库那样格式化显示数据。 $ pip3 install prettytable
这样使用它: ... from prettytable import PrettyTable ​ def cli(): ... headers = '车次 车站 时间 历时 商务 一等 二等 软卧 硬卧 软座 硬座 无座'.split() pt = PrettyTable() pt._set_field_names(headers) for row in rows: # 从row中根据headers过滤信息, 然后调用pt.add_row()添加到表中 ... print(pt)
源码:
https://github.com/protream/tickets
原文链接:
protream https://protream.com/2016/write-a-command-line-train-tickets-query-tool-with-python/ 文源网络,仅供学习之用,如有侵权请联系删除。
在学习Python的道路上肯定会遇见困难,别慌,我这里有一套学习资料,包含40+本电子书,800+个教学视频,涉及Python基础、爬虫、框架、数据分析、机器学习等,不怕你学不会! https://shimo.im/docs/JWCghr8prjCVCxxK/ 《Python学习资料》
关注公众号【Python圈子】,优质文章每日送达。
软件开发
2020-06-02 11:53:00
pyenv
安装:
安装到~/.pyenv目录下:

git clone https://github.com/yyuu/pyenv.git ~/.pyenv

添加:
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
到~/.bashrc末尾
注意:eval "$(pyenv virtualenv-init -)"是virtualenv的内容,暂时也先加进去,要是你不需要virtualenv,此行可以去掉

激活.bashrc
source ~/.bashrc
注意,此处不要写成 export PATH="~/.pyenv/bin:$PATH" 否则安装virtualenv的时候会有问题,找不到目录,很奇怪

查看:
//查看可安装的python版本
pyenv install --list
//安装python pyenv install
//例如:
pyenv install 3.6.8 注意: 解决国内下载非常缓慢的问题: 将下载好的相应的python安装包,例如Python-3.6.8.tgz 下载地址为: https://www.python.org/ftp/python/3.6.8/Python-3.6.8.tar.xz 放在 ~/.pyenv/cache下,并且重命名为Python-3.6.8.tar.gz,没有cache目录就mkdir一下 然后执行pyenv install 3.6.8
安装是一个非常缓慢的过程,程序会卡住不动,最好在后面加参数-v,可以看到安装每一步详情 pyenv install 3.6.8 -v

//安装完成以后,需要更新一下才能看到已安装的版本
pyenv rehash
//查看已经安装好的版本,带*号的为当前使用的版本 pyenv versions
//设置全局版本,即系统使用的将是此版本
pyenv global 3.6.8

//设置当前目录下的使用版本 pyenv local 3.6.8
会在当前目录下创建.python_version文件,文件内容为:3.6.8。pyenv根据此文件的内容,来切换版本。

查询当前使用的Python版本
pyenv version
注意:建议不要修改global全局版本,在linux系统里,部分工具依赖于默认的python.修改了以后可能会出问题。 假如修改了,再使用python global改回去就可以了。

pyenv virtualenv的安装

sudo git clone https://github.com/pyenv/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv
会自动把插件安装到pyenv的plugins目录下
$(pyenv root)就是刚刚写在.bashrc中 PYENV_ROOT环境变量

//从当前版本创建虚拟环境
pyenv virtualenv venv

//指定python版本创建虚拟环境
pyenv virtualenv 3.6.8 venv
虚拟环境将会创建在默认的.pyenv/versions/目录下

//列出现有virtualenvs
pyenv virtualenvs
每个virtualenv有两个条目,较短的一个只是一个符号链接。

//手动激活虚拟环境
pyenv activate venv
//手动离开 pyenv deactivate
//删除虚拟环境
pyenv uninstall venv

将虚拟环境和文件夹绑定
pyenv local venv
和指定python一样的

问题: 希望将虚拟环境创建到工程项目目录下而不是默认的.pyenv/versions/目录下 看到网上有个哥们是这样写的: pyenv shell 3.6.8 && pip install virtualenv && virtualenv venv && source venv/bin/activate
其实就是在抛弃了pyenv下的virtualenv插件,而是用pip在3.6.8下安装了一个virtualenv,然后就按正常操作virtualenv来使用虚拟环境了。
软件开发
2020-06-01 17:23:00
在 IntelliJ 中选择 File > Project Structure > Platform Settings > SDKs

在添加 SDK 中,你可以选择添加 Python SDK

如果你的系统中已经安装过了 Anaconda,那么 SDK 被 IntelliJ 自动识别。
如果你的系统中没有安装 Anaconda 中的话,你可以参考下面的链接提供的方法进行安装。
https://www.ossez.com/t/windows-10-anaconda-3/123
当 SDK 安装好了以后,你可以在 IntelliJ 设置成功后退出。
保存后,你会看到你的 IntelliJ 正在刷新库。

配置成功。

https://www.ossez.com/t/intellij-anaconda/124
软件开发
2020-06-01 08:51:00
缓存设计
1. 前言
在写代码的时候缓存可以说是用的比较多了,主要还是用来缓存已经查询出的一些数据方便下一次能够更加快速的获取对应的结果,比如同一条sql或者以及比较复杂的sql等等,其中比较出名的就是redis缓存了,因为基于内存操作且是分布式的缓存所以项目中用的比较多了,但是一般的框架也会实现本地缓存,在现在分布式项目下,本地缓存无法跨过服务器所以用处被降低,但是大多数项目还是会设置本地缓存,毕竟不可能让别人使用你的框架还得先开启一个redis才能使用,这样不合理也没必要,所以想根据市面上框架的缓存设计进行学习和总结以便加深自己对缓存的了解。
1.1 技术选型
首先要有点基本概念,就是怎么考虑一个缓存,第一个就是缓存的存储类,毕竟缓存,缓存总得有个类来存,第二个就是缓存的操作,可以往容器中添加,那第三个就是容器了,用来存储我们的缓存对象。不过其实用HashMap作为底层也ok,就平常我们学spring框架,BeanFactory是工厂的基础操作类,而BeanDefiniton才是真正的对象包装类,看上去的关系就如下。
目前所研究的框架主要有以下几个 google.guava /** * key-value的缓存形式,存储到缓存中直到被手动释放或者驱逐 * 该接口的实现是线程安全的并且可以由多个线程并发安全的访问 */ @Beta @GwtCompatible public interface Cache < K , V > { ​ /** * 返回与Key关联的值,如果没有缓存该key则返回null */ @Nullable V getIfPresent ( Object key ); ​ /** * 返回此缓存将指定键映射到的值,并在必要时从{@code valueLoader}获取该值。 此方法为常规的“如果已缓存,则返回;否则创建,缓存并返回”模式提供了简单的替代方法。 * 如果可能的话,实现应确保加载操作是同步的,以便在同时访问同一键的情况下,仅调用一次指定的{@code valueLoader}。 */ V get ( K key , Callable valueLoader ) throws ExecutionException ; ​ ImmutableMap < K , V > getAllPresent ( Iterable keys ); ​ void put ( K key , V value ); ​ void putAll ( Map m ); ​ /** * 清除该键的所有缓存之 */ void invalidate ( Object key ); ​ /** * 清除该键的所有缓存之 * * @since 11.0 */ void invalidateAll ( Iterable keys ); ​ /** * 清除所有键 */ void invalidateAll (); ​ /** * 返回该缓存的entries数量hk; */ long size (); ​ /** * 返回此缓存的累积统计信息的当前快照。 所有统计信息均初始化为零,并在缓存的整个生命周期内单调增加。 */ CacheStats stats (); ​ /** * 以线程安全映射的形式返回存储在此缓存中的entries的视图。 对map的修改将直接影响缓存。 */ ConcurrentMap < K , V > asMap (); ​ /** * 执行缓存所需的所有暂挂维护操作。 究竟要执行的活动(如果有)取决于实现。 */ void cleanUp (); } spring-context /** * 通用的接口操作工具类 * * Note: 推荐允许存储null值,比如方法返回null */ public interface Cache { ​ String getName (); ​ /** * 返回底层native缓存提供程序。 */ Object getNativeCache (); ​ @Nullable ValueWrapper get ( Object key ); ​ /** * 将返回值转换成指定的type类型 */ @Nullable < T > T get ( Object key , @Nullable Class < T > type ); ​ /** * 返回此缓存将指定键映射到的值,并在必要时从{@code valueLoader}获取该值。 此方法为常规的“如果已缓存,则返回;否则创建,缓存并返回”模式提供了简单的替代方法。 * 如果可能的话,实现应确保加载操作是同步的,以便在同时访问同一键的情况下,仅调用一次指定的{@code valueLoader}。 */ @Nullable < T > T get ( Object key , Callable < T > valueLoader ); ​ void put ( Object key , @Nullable Object value ); ​ /** * 先从缓存获取,如果不存在则进行插入 *
     * ValueWrapper existingValue = cache.get(key);     * if (existingValue == null) {     *     cache.put(key, value);     * }     */   default ValueWrapper putIfAbsent ( Object key , @Nullable Object value ) {       ValueWrapper existingValue = get ( key );       if ( existingValue == null ) {         put ( key , value );     }       return existingValue ;   } ​   /**     * 清除指定key     */   void evict ( Object key ); ​   /**     * 委托给evict执行,如果没有或者不确定之前存在该key,则返回false     */   default boolean evictIfPresent ( Object key ) {       evict ( key );       return false ;   } ​   /**     * 清除所有映射     */   void clear (); ​   /**     * 清除所有映射     * {@code true} 如果之前有缓存映射     * {@code false} 如果没有或者无法确定之前是否存在entries     */   default boolean invalidate () {       clear ();       return false ;   } ​ ​   /**     * 缓存包装对象     */   @FunctionalInterface   interface ValueWrapper { ​       /**       * 返回真实值       */       @Nullable       Object get ();   } ​ ​   /**     * 值加载程序回调失败而引起异常     *     */   class ValueRetrievalException extends RuntimeException { ​       @Nullable       private final Object key ; ​       public ValueRetrievalException ( @Nullable Object key , Callable  loader , Throwable ex ) {         super ( String . format ( "Value for key '%s' could not be loaded using '%s'" , key , loader ), ex );         this . key = key ;     } ​       @Nullable       public Object getKey () {         return this . key ;     }   } ​ } shiro-cache /** * 此接口在基础缓存框架的缓存实例(例如JCache,Ehcache,JCS,OSCache,JBossCache,TerraCotta,Coherence,GigaSpaces等)之上提供了一个抽象(包装器)API,从而允许Shiro用户配置任何 他们选择的缓存机制。 */ public interface Cache < K , V > { ​       public V get ( K key ) throws CacheException ; ​         public V put ( K key , V value ) throws CacheException ; ​         public V remove ( K key ) throws CacheException ; ​         public void clear () throws CacheException ; ​         public int size (); ​         public Set < K > keys (); ​       public Collection < V > values (); } ibatis /**   * SPI用于缓存提供程序。   * 将为每个名称空间创建一个缓存实例。   *缓存实现必须具有一个构造函数,该构造函数将缓存ID作为String参数接收。   * MyBatis将把名称空间作为ID传递给构造函数。   * public MyCache(final String id) {   * if (id == null) {   *   throw new IllegalArgumentException("Cache instances require an ID");   * }   * this.id = id;   * initialize();   * }   */ public interface Cache { ​   /**   * 获取缓存id   */   String getId (); ​     void putObject ( Object key , Object value ); ​     Object getObject ( Object key ); ​     Object removeObject ( Object key ); ​     void clear (); ​     int getSize (); ​     default ReadWriteLock getReadWriteLock () {     return null ; } 
2.实现
对目前框架中Cache接口的方法定义进行总结,基础的方法有get/put/size/evict/clear,有人可能会觉得有点少,不过没关系毕竟这是基础的底层操作接口,之后可以再让其他接口继承这个接口以实现更加多的扩展功能。那么接下来实现我们自己的Cache接口吧。 package com . zxw . cache ; ​ import java . util . concurrent . Callable ; ​ /** * @author zxw * Cache缓存基础操作类 * 提升程序性能 * @date 2020/5/30 18:47 */ public interface Cache < K , V > { /** * 获取对应的Value * * @param Key * @return */ V get ( K Key ); ​ /** * 将Value转成K的类型 * * @param key * @param type * @return */ V get ( K key , Class < K > type ); ​ /** * @param key * @param valueLoader * @return */ V get ( K key , Callable valueLoader ); ​ /** * 添加缓存 * * @param key * @param val */ void put ( K key , V val ); ​ /** * 清除K的值 * * @param key */ void evict ( Object key ); ​ /** * 清除所有 */ void clear (); ​ /** * 返回entries的数量 * @return */ long size (); ​ /** * 如果该值尚未有关联,则该值尚未设置 * @param key * @param val * @return */ default V putIfAbsent ( K key , V val ) { V v = get ( key ); if ( v == null ) { put ( key , val ); } return v ; } }
接下来我们可以提供一个抽象的实现类实现这些基础的操作能够让后面的继承类进行复用。 package com . zxw . cache ; ​ import java . util . IllegalFormatException ; import java . util . concurrent . Callable ; ​ /** * @author zxw * @date 2020/5/31 10:21 */ public abstract class AbstractCache < K , V > implements Cache < K , V > { ​ private final boolean allowNullValues ; ​ public AbstractCache ( boolean allowNullValues ) { this . allowNullValues = allowNullValues ; } ​ public final boolean isAllowNullValues () { return this . allowNullValues ; } ​ public AbstractCache () { } ​ @Override public V get ( K Key ) { V v = lookup ( Key ); return v ; } ​ @Override public V get ( K key , Class < K > type ) { V v = lookup ( key ); if ( v != null && type != null && ! type . isInstance ( v )) { throw new IllegalStateException ( "Cached value is not of required type [" + type . getName () + "]:" + v ); } return v ; } ​ /** * 子类实现获取key的逻辑 * * @param key * @return */ protected abstract V lookup ( K key ); } ​
这里我们使用Object作为底层数据存储对象,以ConcurrentHashMap作为对象存储容器,实现一个map的缓存操作类 package com . zxw . cache . concurrent ; ​ import com . zxw . cache . AbstractCache ; import com . zxw . util . Assert ; ​ import java . util . concurrent . Callable ; import java . util . concurrent . ConcurrentHashMap ; ​ /** * @author zxw * @date 2020/5/31 11:00 */ public class ConcurrentMapCache < K , V > extends AbstractCache < K , V > { private final String name ; private final ConcurrentHashMap < K , V > store ; private final String serialization ; ​ /** * 使用指定的名称创建一个ConcurrentMapCache * * @param name */ public ConcurrentMapCache ( String name ) { this ( name , new ConcurrentHashMap <> ( 256 ), true ); } ​ public ConcurrentMapCache ( boolean allowNullValues , String name ) { this ( name , new ConcurrentHashMap <> ( 256 ), allowNullValues ); } ​ public ConcurrentMapCache ( String name , ConcurrentHashMap < K , V > store , boolean allowNullValues ) { this ( name , store , allowNullValues , null ); } ​ public ConcurrentMapCache ( String name , ConcurrentHashMap < K , V > store , boolean allowNullValues , String serialization ) { Assert . isNull ( name , "Name must not be null" ); Assert . isNull ( store , "store must not be null" ); this . name = name ; this . store = store ; this . serialization = serialization ; } ​ @Override protected V lookup ( K key ) { return this . store . get ( key ); } ​ @Override public String getName () { return this . name ; } ​ @Override public V get ( K key , Callable valueLoader ) { return this . store . computeIfAbsent ( key , k -> { try { return valueLoader . call (); } catch ( Exception e ) { throw new IllegalArgumentException (); } }); } ​ @Override public void put ( K key , V val ) { this . store . put ( key , val ); } ​ @Override public void evict ( Object key ) { this . store . remove ( key ); } ​ @Override public void clear () { this . store . clear (); } ​ @Override public long size () { return 0 ; } } ​
目前使用的是Cache的实现类作为我们的缓存实体以及操作类,HashMap作为字段容器存储缓存实体。
操作操作类和缓存实体都有了,接下来就是我们的缓存容器了,首先定义一个CacheManager管理我们的所有缓存实体。 package com . zxw . cache ; ​ import java . util . Collection ; ​ /** * 提供并维护Cache实例的生命周期 * * @author zxw * @date 2020/5/31 13:28 */ public interface CacheManager { /** * 根据名称获取Cache * @param name * @param * @param * @return */ < K , V > Cache < K , V > getCache ( String name ); ​ /** * 获取所有的Cache名称 * @return */ Collection < String > getCacheNames (); } package com . zxw . cache . support ; ​ import com . zxw . cache . Cache ; import com . zxw . cache . CacheManager ; ​ import java . util . Collection ; import java . util . Collections ; import java . util . Set ; import java . util . concurrent . ConcurrentHashMap ; ​ /** * @author zxw * @date 2020/5/31 13:34 */ public abstract class AbstractCacheManager implements CacheManager { private final ConcurrentHashMap < String , Cache > cacheMap ; private volatile Set < String > cacheNames ; ​ public AbstractCacheManager () { this ( new ConcurrentHashMap <> ( 16 )); } ​ public AbstractCacheManager ( ConcurrentHashMap < String , Cache > cacheMap ) { this ( cacheMap , Collections . emptySet ()); } ​ public AbstractCacheManager ( ConcurrentHashMap < String , Cache > cacheMap , Set < String > cacheNames ) { this . cacheMap = cacheMap ; this . cacheNames = cacheNames ; } ​ @Override public < K , V > Cache < K , V > getCache ( String name ) { Cache cache = this . cacheMap . get ( name ); // 如果存在该cache,则直接返回 if ( cache != null ) { return cache ; } // 如果cache不存在,则创建一个cache Cache missingCache = getMissingCache ( name ); if ( missingCache == null ) { synchronized ( this . cacheMap ) { cache = this . cacheMap . get ( name ); if ( cache == null ) { cache = CreateCache ( name ); cacheMap . putIfAbsent ( name , cache ); } } } return cache ; } ​ public abstract Cache CreateCache ( String name ); ​ protected abstract Collection loadCaches (); ​ @Override public Collection < String > getCacheNames () { return cacheNames ; } ​ public Cache getMissingCache ( String name ) { return null ; } }
private final ConcurrentHashMap cacheMap;
这里用一个Map类型存储所有的cache实现类
总结:现在我们从头到尾在重新看一遍这个缓存。首先我们得有个管理器,对所有的缓存类型进行管理。
然后我们需要有个被管理的对象,即缓存
软件开发
2020-05-31 15:56:00
软件开发的是一项系统的工程,就像盖房子一样其分工也是很明确的,比如写前端的,后端编程的,写底层的。。。等等。
那么,这里就有一些同学可能会听说过的一些名词了。前台、后台、前端、后端,到底什么是前台,什么是前端,什么是后台什么是后端。今天就带着大家把这些概念科普一下,希望能够帮大家选择适合自己的方向来学习。
我们就先从前台开始说起吧。注意啊,这里的前台可不是酒店里面的前台,而是网络上的一个前台。具体是什么意思呢?当普通用户(也就是我们)访问一个网站的时候,我们看到的东西都属于前台的内容,也就是说,前台就是普通用户所看到的。比如,我们访问淘宝,看到琳琅满目的商品以及各种各种的广告,都是属于前台展示的区域。也就是前台页面.
那么后台又是什么呢?后台就是网站管理人员所看的,操作的。所有动态网站(即内容会不定期更新)的网站,都会配备一个后台。方便管理人员管理网站。
就像淘宝商家,添加商品,审核订单,等等。只能他们操作吧。所以说,后台就是管理人员所看到的平台,用来管理前台的呈现。对比现实生活中,后台就相当于咋们去逛商场(又是商场啊),然后会发现有些黑洞洞的地方,然后你还进不去。商场管理人员告诉你,那里是库房。对,就是库房,管理人员可以从库房中拿产品放到前厅售卖。
那么我们经常听到web前端呢?概念稍微显得有一些生涩。前端就是指网站上我们视线所及之处的内容。这里有人会问了,那不是和前台一样?其实区别还是挺大的,前端包含了整个前台后台所能看到的内容,通俗一点讲,前端就是所有能看到的页面的内容。这里再打个比方啊,咋们还是去逛个商场,这商城啥样?卖啥东西?库房啥样?电梯几部?等等,咋们能看到的都相当于是前端的东西。
聊完了前端我们再来看看后端吧,后端是什么呢?后端就是用户所看不到的地方的数据交互了。比如你淘宝买个商品,那商品的数据是由谁提供的呢?下单之后提交订单谁来提交的呢?等等,咋们看不见却可能能够感知的东西都是属于后端了。也是是说,后端是负责数据处理与更新,以及逻辑处理等等。也就是相当于商场核心的那些规章制度啊,渠道啊,等等咋们看不到的东西。
到这里可能就有人要说了,那后端好高大上啊,前端就不行了。其实并不然。
首先,前后端本质没有区别,都是逻辑代码。
其次,随着网络技术的进步,浏览器等的支持,越来越多的逻辑已经放在了前端去处理,而且,前端人还要兼顾着用户的个性化需求,用户体验。要绚!!!要好看!!!还要好用!!!后端只需处理数据。网站好不好大部分用户的评判标准还是前端做得好不好,听完了这个你应该知道前端有多么的重要了吧。
前端程序员 是大家都公认的高薪岗位,那他们的工资到底有没有传说中那么高呢?我们来看看职友集的数据。


数据来源:职友集
就北上广深杭的前端薪资来看,平均薪资均达到 一万以上 。
通常来讲:
工作1~2年后会成为前端高级软件工程师,年薪可以达到 15万以上。
工作3-5年后可以成为前端技术主管或者经理,年薪在 15-50万之间。
工作年限5年以上,会成为互联网公司的技术总监或产品经理,年薪将达到 50万-100万之间 。
前端程序员的薪资处在行业中的较高水平,那么相对的,他们需要 掌握这些重要技术。
企业对前端人才有什么要求?
根据对100家互联网名企对前端开发工程师的招聘要求分析如下:
核心要求包括:
**语言技术:**JS基础和核心、html5、CSS
**框架技术:**JQuery、AJAX、Bootstrap、AngularJS
**通讯协议技术:**HTTP协议、服务端开发技术
**交互技术:**UI交互设计
**客户端技术:**微信开发技术、html5、JS、Android、iOS、Web App开发技术
辅助要求:
学历及相关专业、文档规范写作能力、团队合作能力、责任心
现在常常网络上常说前端已经饱和,现在入行已经晚了,但事实真的如此吗?通过下面几则数据,我们看看 前端发展前景 如何。
前端发展前景怎么样?
数据来源:百度指数
通过百度搜索指数的趋势图我们可以看到,从2014年开始至今,前端一直处于 逐步上升、正常波动 的状态。2020年1月,受疫情影响有所降低,但总体来看,前端发展前景还是较为稳定的。
数据来源:百度百聘
从百度百聘数据来看,前端岗位的需求依然很大。
资料来源:拉钩网
从拉钩网的前端岗位的 发布频率 来看,岗位发布时间较为密集。因此公司对前端程序员的需求量还是比较大的。
综上所述的,前端开发的工作主要是开发用户操作界面,其中涉及的内容包括实现炫酷的页面交互、提供良好的用户体验、配合服务端工程师处理复杂的业务逻辑和实现 Web 的跨终端适配兼容等,这些都是一个合格前端要掌握的必备技能;
至此,我们已经大致了解了前端开发到底是做什么的。现在的问题是如何成为一名合格的前端开发工程师?怎样才能将前端 开发的各项工作做好?要成为专业的前端开发工程师、做好前端开发的各项工作,需要掌握哪些相关的技术呢?
专门建立的学习Q-q-u-n ⑦⑧④-⑦⑧③-零①② 分享学习方法和需要注意的小细节,互相交流学习,不停更新最新的教程和学习技巧(网页制作,网站开发,web开发,从0基础开始的的HTML+CSS+JavaScript。jQuery,Ajax,node,angular框架等到移动端HTML5的项目实战【视频+工具+系统路线图】全栈工程师学习路线以及规划都有整理,分享给小伙伴)点: 学习前端,我们是认真的
软件开发
2020-05-30 16:48:00
Python--从集合中随机取出一个元素
博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢!
说明
有时候有一个这样的需求,需要在一个数据表里面随机获取到一条数据,这就需要我们借助python的模块random了
思路
首先是使用flask的数据库模块SQLAlchemy查询数据库,然后取随机的数 from random import choice model_user_all = User.query.filter().all() model_user_one = choice(model_user_all)
如果是普通列表取随机元素的话 list = ['a', 'b', 'c', 'd', 'e'] from random import choice one = choice(list)
感谢 万能的网络
以及勤劳的自己
软件开发
2020-05-29 23:49:00
Python--报错RecursionError: maximum recursion depth exceeded in comparison
博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢!
说明
这个问题我是在页面跳转的时候遇到的,查了资料说大概就是递归出现的问题,python的递归的次数默认是1000次,如果超过了这个数就会报错
思路
可以修改这个递归的次数 import sys sys.setrecursionlimit(100000)
建议
一般不是在高深算法出现这个问题的话,我们基本是不会遇到这个问题的,我遇到这个问题是因为我代码的逻辑有问题,所以遇到这个的问题多看看自己的代码,多多优化就好了
感谢 万能的网络
以及勤劳的自己
软件开发
2020-05-29 23:11:00
关于Python的就业方向,如果你还在学习还没开始找工作,这篇文章绝对会对你有所帮助。
目前信息化产业发展势头很好,互联网就成为了很多普通人想要涉及的行业,因为相比于传统行业,互联网行业涨薪幅度大,机会也多,所以就会大批的人想要转行来学习Python开发。如果你想入行Python,最好还是有老师带领学习,少走弯路,快速找到工作!
◆◆Python就业方向◆◆
1.爬虫。
感觉python就是天然为爬虫而生,我第一个项目就是爬某东的图片,简单快速,非常有成就感。
爬虫掌握熟练的话,包括简单的mysql语句、html和css简单的知识以及最厉害的scrapy爬虫框架,基本上就可以去尝试海投一下爬虫岗位。
2.数据分析。
我的目标就是转行数据分析师,所以在这里多啰嗦几句,学会了爬虫,便有了数据来源,运用这些数据以及相应的爬虫库和excel表格,就可以进行简单的数据分析,这是相对爬虫更高级的岗位。
3.web后端。
推荐诸位转行可以走这个方向,一是工作岗位多,相对好找工作,二是学成周期短,只需要把python基础知识吃透,Django框架和flask框架吃透,再补充一些html知识,然后做一些项目便可以投简历了。
**4.机器学习。**这个方向就比较高端了,对于想转行的新手不建议触碰。
5、运维工程师
我们都知道,Python不仅能做人工智能、web开发,还在运维中有着举足轻重的作用。
很多公司在招聘中高级运维工程师的要求时,必须的具备的技术就是掌握Python语言。
因为Python是一门非常NB的编程语言,它能满足绝大部分自动化运维的需求,又能做后端 C/S 架构,又能用 WEB 框架快速开发出高大上的 WEB 界面,还能做机器学习。
当运维工程师需要自己独立开发一个完整的自动化系统时,Python就派上用场了,这个时候才是真正价值的体现,才能证明自身的能力。
运维工作者对Python的需求很大,学习Python自动化运维的起薪至少是12k-15k,这比普通运维工程师高出了至少百分之30。
6.游戏、图像、XML、机器人等,这个方向同样相对高端,不建议新手触碰。
零基础学习Python必须明确的几点:
1.明确你将来是做什么工作的,需要掌握哪些技能,很多人连这个就不知道就盲目的学,你首先清楚,现在公司需要什么人才,你应该奔着什么目标努力。
2.系统的学习规划,规划好你每天学习的新知识和每天做的作业和练习,很多人想自学Python,两个月就看完了,这样的能找到工作算是出奇了,现在学习Python想找到工作,没有4-6个月的根本不行,所以规划是一定要有的,我建议半年时间。
3.注意学习方法,很多人在学习Python的时候,开始学那一刻起就选择了错误的学习方法,所以最后注定会放弃,回到原来的岗位继续做着自己不喜欢的事情,学习Python需要一定的技巧,在开始学之前多跟别人问问,不要自己盲目的自学,浪费时间。
应届生是否培训:
富裕的,或者想少走弯路的,可以选择培训,但是培训不一定就能找到工作
有的培训机构说包就业,这些本身就是骗人的,不要信
但是我目前是不建议选择那种速成的培训,因为学习时间短,而且学费太高,成本太大,培训之后还不一定能找到工作,现在的选择我更加建议大家选择在线上进行培训,成本低,还方便
开始学注意几点:
1.代码规范,这本身就是一个非常好的习惯,如果开始不养好好的代码规划,以后会很痛苦
2.多动手,少看书,很多人学Python就一味的看书,这不是学数学物理,你看例题可能就会了,学习Python主要是学习编程思想。
3.勤练习,学完新的知识点,一定要记得如何去应用,不然学完就会忘,学我们这行主要都是实际操作。
4.学习要有效率,如果自己都觉得效率非常低,那就停不停,找一下原因,去问问过来人这是为什么
对于初学者想更轻松的学好Python开发,爬虫技术,Python数据分析,人工智能等技术,这里也给大家准备了一套系统教学资源, 加Python技术学习教程qq裙:855408893 ,免费领取。学习过程中有疑问,群里有专业的老司机免费答疑解惑!点击加入我们的 python学习圈
软件开发
2020-05-29 19:20:00
前几天小编分别利用 Python正则表达式 、 BeautifulSoup 、 Xpath 分别爬取了京东网商品信息,今天小编利用CSS选择器来为大家展示一下如何实现京东商品信息的精准匹配~~
CSS选择器
目前,除了官方文档之外,市面上及网络详细介绍BeautifulSoup使用的技术书籍和博客软文并不多,而在这仅有的资料中介绍CSS选择器的少之又少。在网络爬虫的页面解析中,CCS选择器实际上是一把效率甚高的利器。虽然资料不多,但官方文档却十分详细,然而美中不足的是需要一定的基础才能看懂,而且没有小而精的演示实例。
京东商品图
首先进入京东网,输入自己想要查询的商品,向服务器发送网页请求。在这里小编仍以关键词“狗粮”作为搜索对象,之后得到后面这一串网址: https://search.jd.com/Search?keyword=%E7%8B%97%E7%B2%AE&enc=utf-8 ,其中参数的意思就是我们输入的keyword,在本例中该参数代表“狗粮”,具体详情可以参考Python大神用正则表达式教你搞定京东商品信息。所以,只要输入keyword这个参数之后,将其进行编码,就可以获取到目标URL。之后请求网页,得到响应,尔后利用CSS选择器进行下一步的数据采集。
商品信息在京东官网上的部分网页源码如下图所示:
部分网页源码
仔细观察源码,可以发现我们所需的目标信息在红色框框的下面,那么接下来我们就要一层一层的去获取想要的信息。
在Python的urllib库中提供了quote方法,可以实现对URL的字符串进行编码,从而可以进入到对应的网页中去。
CSS选择器在线复制
很多小伙伴都觉得CSS表达式很难写,其实掌握了基本的用法也就不难了。在线复制CSS表达式如上图所示,可以很方便的复制CSS表达式。但是通过该方法得到的CSS表达式放在程序中一般不能用,而且长的没法看。所以CSS表达式一般还是要自己亲自上手。
直接上代码,利用CSS去提取目标信息,如商品的名字、链接、图片和价格,具体的代码如下图所示:
代码实现
如果你想快速的实现功能更强大的网络爬虫,那么BeautifulSoupCSS选择器将是你必备的利器之一。BeautifulSoup整合了CSS选择器的语法和自身方便使用API。在网络爬虫的开发过程中,对于熟悉CSS选择器语法的人,使用CSS选择器是个非常方便的方法。
最后得到的效果图如下所示:
最终效果图
新鲜的狗粮再一次出炉咯~~~
CSS选择器
关于CSS选择器的简单介绍:
BeautifulSoup支持大部分的CSS选择器。其语法为:向tag对象或BeautifulSoup对象的.select()方法中传入字符串参数,选择的结果以列表形式返回,即返回类型为list。
tag.select("string")
BeautifulSoup.select("string")
注意:在取得含有特定CSS属性的元素时,标签名不加任何修饰,如class类名前加点,id名前加 /#。 想学习更多Python网络爬虫与数据挖掘知识,可前往专业网站: http://pdcfighting.com/
软件开发
2020-05-28 08:12:00
>作者 谢恩铭,公众号「 程序员联盟 」。 转载请注明出处。 原文: https://www.jianshu.com/p/49fdba563d53
> 《C语言探索之旅》 全系列
内容简介 前言 if…else 条件表达式 多个条件 一些容易犯的错误 布尔值,条件语句的核心 switch 语句 三元表达式:精简的条件语句 总结 第一部分第九课预告
1. 前言
上一课是 C语言探索之旅 | 第一部分第七课:运算那点事 。
在之前的课程中,我们已经知道世界上有很多编程语言,有些相互之间很类似,其中不少都受了 C语言的启发。
事实上,C语言是很早之前被发明的(1972 年左右),所以它也成了很多近代编程语言的参考模板。
我们说编程语言相似,是说他们都汲取了它们的前辈的编程基础。
说到编程基础,那就有很多了。其中,我们已经学过的有:变量,运算。这一课我们来学习条件表达式。
2. if…else 条件表达式
简单来说,条件表达式使我们可以测试变量。
打个比方,我们可以这样说: “假如变量的值等于 7,就执行这样那样的操作。”
如果 C语言里面不能做“等于”的判断,那该多可惜啊。我们还要做“小于”,“大于”,“大于等于”,“小于等于”,“不等于”的判断。
不必担心,C语言都为你预备好了。
为了学习 if…else 语句,我们准备按照下面的顺序来进行: 预先需要了解的几个符号 if 语句 else 语句 else…if 语句 多个条件 一些需要避免的错误
在我们学习写 if…else 表达式前,须要先了解几个基本符号,这些符号对于实现条件判断是必不可少的。
几个需要了解的符号
下表列出了 C语言中需要用心记住的几个符号:
符号 含义 == 等于
> 大于
< 小于
>= 大于等于
<=
!=
小于等于
不等于
>判断“等于”需要两个等号( == )! 一个很常见的错误是只写一个等号,在 C语言中一个等号和两个等号具有不同的含义。一个等号表示赋值,两个等号才是判断是否相等。
单一的 if 语句
if 表示“如果,假如”。
逻辑是这样的:
> 如果 变量的值是这样 则 执行如下操作
所以单一的 if 语句是这样写的: 先写一个 if 。 接着写一个括号 (),在这个括号中写条件。 接着写一个大括号 {},在大括号中写在 () 中条件为真时所要执行的操作。
格式如下: if (/* 条件 */) { // 如果条件为真,所要执行的操作 }
我们把上面注释的地方换成实际的代码,写一个程序测试一下: #include int main(int argc, char *argv[]) { int age = 20; if (age >= 18) { printf("你成年了!n"); } return 0; }
解释 : int age = 20;
首先定义一个 int 型的变量,名字是 age(表示“年龄”),值为 20。
if 语句的括号 () 中的条件是 age >=18 ,意思是“假如 age 的值大于或等于 18”。
if 语句的大括号 {} 中的语句是 printf("你成年了!n");
就是说当 age 的值大于或等于 18 时,显示“你成年了!”这句话。
因为程序中变量 age 的值是 20, 大于 18,所以条件为真,所以程序运行起来后显示: 你成年了!
如果我们把变量 age 的值改为 12,再次运行程序,什么也没显示,因为 12 小于 18,所以 age >= 18 这个条件不为真,不执行大括号里的 printf 语句。
唠叨一下代码的格式
假如我们将上面的代码写成这样: if(age>=18){printf("你成年了!");}
程序也是可以正确运行的,但是 非常不推荐 这样的代码格式!
如果我们的程序没有空行,没有空格,不缩进,都写在一行里,那将会使代码非常难以阅读。
所以从一开始学习编程就请养成良好的编码习惯,不然以后写一些大型程序,别人根本不知道怎么阅读你的代码,你也会迷失在自己的代码里。
推荐看林锐老师的《高质量C++/C编程指南》一书,里面提到了编码规范。
当然,每个程序员的代码风格都不一样,但是我们推荐大家遵从本系列课程中的代码格式或者一些著名公司的代码格式(例如 Google,阿里,等等),因为是比较通用的编码格式。
else 语句
现在你知道怎么写单一的 if 语句了。那当条件为假时,我们想要电脑也执行对应的操作怎么办呢?对了,此时就轮到 else 关键字出场了。
else 表示“否则”。
但是 else 语句一定要跟 if 语句配合才能使用,独立的 else 语句是不可用的!
>至于关键字,是电脑语言里事先定义的,有特别意义的标识符,有时又叫保留字,是有特别意义的变量。 C语言有不少关键字,比如 int,float,char,double,if,else,等,暂时我们不多涉及,可以去了解一下。之前关于变量的那一课,提到变量的命名时,其实漏讲了一点: 变量的名字不要跟关键字重名 。
所以一个完整的 if...else 语句的格式是: if (/* 条件 */) { // 条件为真的时候,所要执行的操作 } else // 如果上面 () 里的条件为假 { // 条件为假的时候,所要执行的操作 }
再用之前测试年龄的例子,来写一下实际的程序 if (age >= 18) // 如果 age 变量的值大于或等于 18 { printf("你成年了!"); } else // 如果age变量的值小于 18 { printf("你还未成年,可以适当任性!"); }
如果 age 的值大于或等于 18,则打印“你成年了!”;如果 age 的值小于 18,则打印“你还未成年,可以任性!”。
else...if 语句
上面我们学习了如何用单一的 if 语句,以及 if...else 语句
其实除了“假如...”(if 语句)和“否则...”(else 语句),还有else...if(“又假如”)语句,用于在 if 语句的条件不为真时对其他的情况的判断,else…if 语句放在 if 语句和 else 语句之间。
逻辑是这样的:
> 如果 变量值为 A, 则 执行 if 对应操作; 如果 变量值不为 A,而为 B, 则 执行 else...if 对应操作; 如果 变量值不为 A 也不为 B, 则 执行 else 对应操作。
我们也用一个实例来看一下 if,else…if 和 else 合在一起用的情况: if (age >= 18) // 如果 age 变量的值大于或等于 18 { printf("你成年了 !"); } else if (age > 2) // 如果 age 小于 18 但是大于 2 { printf("还好你不是太年幼。"); } else // 如果 age 小于或等于 2 { printf("mama, guagua, blablabla..."); // 儿语,听不懂 }
流程是这样的: 首先判断第一个 if 语句中的括号里的表达式,如果为真,则执行第一个大括号里的语句。 如果 if 的条件不为真,则接着判断 else…if 语句的括号里的表达式,如果为真,则执行对应的大括号里面的语句。 如果 if 和 else…if 里的表达式都为假,则无需再判断,直接执行 else 语句的大括号里的命令。
在完整的条件语句块中,else…if 和 else 语句不是必须的,但是必须要有一个 if 语句。
我们可以写任意多的 else…if 语句,但是 if 语句和 else 语句则都是最多有一个。
else 后面是没有括号 () 的,但是 if 和 else…if 后面都有括号,也就是都包含判断条件,应该不难理解吧,很合乎逻辑。
3. 多个条件
我们也可以在条件语句的括号 () 中测试多个条件表达式。
例如,你想要测试这个人的年龄是不是介于 18 岁和 25 岁之间,就需要两个条件表达式来判断了。
为了达成我们的目的,我们需要用到新的符号:
符号 含义 && 逻辑与
II !
逻辑或 逻辑非
本来其实上表中的几个应该也叫做:与,或,非。但为什么叫“逻辑与”,“逻辑或”和“逻辑非”呢?
那是因为之后我们还会学到 & , | 等符号,称为“按位或”和“按位与”。暂时不用知道什么意思,之后会学到。
逻辑与
为了做逻辑与判断,我们则要用到两个 & 符号。这个符号在美式键盘上需要按下 Shift + 数字键 7。
如果我们要做上面提到过的年龄的判断,则需要用逻辑与: if (age > 18 && age < 25)
两个 & 号连在一起表示逻辑与,就是说当两边的表达式都为真时,括号中的整个表达式才为真,所以这里只有当 age 大于 18 并且小于 25 的情况下,括号里的表达式才为真。
逻辑或
为了做逻辑或判断,我们则要用到两个 | 符号。这个符号在美式键盘上需要按下 Shift + `````` 键。
逻辑或只要其两边的两个表达式有一个为真,整个表达式就为真。
假设我们现在要写一个程序,目的是判断一个人是不是够资格开设银行账户。
众所周知,要开一个银行账户,申请人不能太年幼(我们假定需要大于 20 岁)或者有很多钱(“有钱任性”嘛,即使是 10 岁小孩,也得让他开户。开个玩笑~)。
所以我们的程序如下: if (age > 20 || money > 150000) { printf("欢迎来到**银行 !"); } else { printf("我还不够资格,悲催啊 !"); }
所以这个测试只要申请人年龄大于 20 岁或者拥有超过 15 万现金时,就可以让其开户。
逻辑非
我们最后要看的符号是感叹号 ! ,表示“取反”,加在表达式之前。这个符号在美式键盘上需要按下 Shift + 数字键 1。
如果表达式为真,那么加上感叹号则为假;如果表达式为假,那么加上感叹号则为真。就是跟你“唱反调”。
例如: if (!(age < 18))
上面的表达式表示“假如已经成年”(不小于 18 岁)。
当然,逻辑与和逻辑或可以连用,甚至多个一起用,例如: if ((age > 18 && age < 25) || age < 4)
4. 一些容易犯的错误
== 号
不要忘了之前讲过的 == (两个等号)是用于判断是否相等。例如: if (age == 18) { printf("你刚成年 !"); }
上例中如果错把 == (两个等号)写成了 = (单个等号),那后果很严重,表达式就变成 age = 18 了。单个等号是赋值,所以 age 变为 18,整个表达式的值变为 18,就起不到判断的作用了。
一种避免这样错误的写法是 18 == age ,这样如果我们漏写了一个等号,变成 18 = age ,那编译器会报错,因为常量(例如 18)不能做左值。
关于左值和右值,可以去搜索网上的资料,简单来说,位于赋值运算符两侧的两个值,左边的就叫左值,右边的就叫右值。
多余的分号(;)
还有一个经常会犯的错误是:在 if 的括号或者 else...if 的括号后面多加了一个分号,如下: if (age == 18); // 注意这个分号,本来不应该出现的 { printf("你刚好成年"); }
上面的代码实际上相当于 if (age == 18) { ; } { printf ("你刚好成年"); }
看到没有,分号就相当于 if 语句的大括号里的执行语句,而 ; 是空语句,什么也不执行。
我们原先想让其在 age 等于 18 时执行的 printf 语句却成了一个必定会被执行的语句,不论 age 的值是不是等于 18(大括号是可以把多个语句集合起来的分隔区域,可以拿掉大括号再来理解)。
可能有点晕,请多看几遍代码。
5. 布尔值,条件语句的核心
我们现在更加深入地来看 if...else 条件语句。事实上,条件表达式(这里指括号里的表达式)在其内部是做了布尔值的判断。
布尔类型英语叫 boolean,是只能取“真”或者“假”的一种变量类型。
事实上,在其他的一些编程语言,如 Java,C++,C# 中,本身定义了 boolean 类型,只能有两个值,true(表示“真”)和 false(表示“假”)。
但是 C语言没有定义 boolean 类型,怎么办呢?
>不用担心,C语言把零作为假,非零的一切值都算为真。
我们可以做几个小测试来加深理解: if (1) { printf("真!"); } else { printf("假!"); }
以上代码执行结果是显示: 真!
我们可以把 1 换成非零的任意 int 型整数:5,9,13,-2,448,等等,结果都会显示: 真! 。 if (0) { printf("真!"); } else { printf("假!"); }
以上代码执行结果是显示: 假!
所以 C语言中只有 0 是假,其他非零的值都被看作真。这就是 C语言的“布尔类型”,因为 C语言没有定义真正的布尔值类型。
必要的一些解释
事实上,每当你在条件表达式里做判断时,如果表达式是真的,则整个表达式的值是 1;如果是假的,则整个表达式的值为 0。
例如: if (age >= 18)
假如 age 的值为 25,是大于等于 18 的,那么 age >= 18 这个表达式的值为真,电脑其实会把它替换为 1。
假如 age 的值为 15,是小于 18 的,那么 age >= 18 这个表达式的值为假,电脑其实会把它替换为 0。
用变量来测试一下: #include int main(int argc, char *argv[]) { int age = 25; int adult = 0; adult = age >= 18; printf("adult 的值是 : %dn", adult); return 0; }
运行,显示: adult 的值是 : 1
如果 age 为 15,则显示: adult 的值是 : 0
那句 adult = age >= 18; ,其实等价于 adult = (age >= 18); 。因为 >= 这个符号的优先级比 = 符号要高。运算符的优先级,不需要死记硬背,如果实在记不住,就加上括号 () 吧。
这里的 adult 其实已经是一个 C语言中的“布尔值”了。
6. switch 语句
我们刚学的 if...else 类型的条件语句是最常用的。
但是 C语言还给出了 if...else 语句的一个替代语句:switch 语句。
因为,有时候,当我们的条件判断很多时,就会感觉冗余。例如下面的代码: if (age == 2) { printf("宝宝,你好 !"); } else if (age == 6) { printf("小朋友,你好 !"); } else if (age == 12) { printf("少年,你好 !"); } else if (age == 16) { printf("青少年,你好 !"); } else if (age == 18) { printf("成年人,你好 !"); } else if (age == 67) { printf("爷爷,你好 !"); } else { printf("对你的年龄我还没有对应的问候方式。"); }
之前我们也说过,程序员(IT 工作者)是很会“偷懒”的,他们不喜欢做重复、单调的工作。
因此,为了避免重复写一些代码,他们发明了新的 switch 语句。switch 表示“转换,替换”。
以下展示了用 switch 语句改写的上面那个 if...else 语句的例子: switch (age) { case 2: printf("宝宝,你好 !"); break; case 6: printf("小朋友,你好 !"); break; case 12: printf("少年,你好 !"); break; case 16: printf("青少年,你好 !"); break; case 18: printf("成年人,你好 !"); break; case 67: printf("爷爷,你好 !"); break; default: printf("对你的年龄我还没有对应的问候方式。"); break; }
switch 语句虽然没有 if...else 语句那么常用,但是对于判断情况很多的条件语句,用 switch 可以少写不少代码呢,而且程序也一目了然,比较清晰。
switch 语句的格式 首先,写 switch 这个关键字,接着写一个括号,括号里面是要判断的变量。 case 加上变量可能的取值,再加一个冒号( : ),再加上对应取值时的操作,再加上一个 break; 。
>要注意:case 后面的值只能是整型或字符型的常量或常量表达式。 default 负责处理除了各个 case 以外的情况。 多个 case 就相当于 if...else 语句里的 if 和 else...if 。 default 相当于 if...else 语句里的 else。
想想看,switch 语句是不是很像我们去饭店用餐:
服务员拿了一个酒水单给你,上面有好多饮料,就像好多个 case 后面的取值,你点一种饮料,服务生就去给你拿对应的饮料,这个操作就像 case 的冒号后面的语句。
假如你什么都不要,说:还是给我来杯水吧,那服务生就只能给你拿一杯水了,就相当于 default。
每个 case 语句的结尾绝对不要忘了加 break,否则将导致多个分支重叠(除非有意使多个分支重叠)。
假如上例中我们少写了一个 break; ,看看会变成怎样: int age = 2; switch (age) { case 2: printf("宝宝,你好 !n"); // 这里我们没有写 break; case 6: printf("小朋友,你好 !n"); break; case 12: printf("少年,你好 !"); break; case 16: printf("青少年,你好 !"); break; case 18: printf("成年人,你好 !"); break; case 67: printf("爷爷,你好 !"); break; default: printf("对你的年龄我还没有对应的问候方式。"); break; }
上面的代码,当 age 的值为 2 的时候,显示的内容是: 宝宝,你好 ! 小朋友,你好 !
很奇怪吧,这是因为没有 break,程序就不跳出 switch 的大括号,而继续执行,“穿透”了 case 6 。虽然 age 的值是 2,不等于 6,但是也执行了 case 6 对应的语句 printf("小朋友,你好 !n");
因为 case 6 的执行语句后面加了 break,所以程序执行完 printf("小朋友,你好 !n");
就跳出了 switch 语句。当然有时候也有故意不加 break,使得多个情况做同一个操作的,例如: switch (age) { case 2: case 6: case 12: case 16: printf("未成年人,你好 !"); break; case 18: printf("成年人,你好 !"); break; case 67: printf("爷爷,你好 !"); break; default: printf("对你的年龄我还没有对应的问候方式。"); break; }
上面的代码,当 age 的值为 2 或 6 或 12 或 16 时,都会执行 printf("未成年人,你好 !");
是不是也很妙呢?
break 关键字的作用不仅于此(用于跳出 switch 语句),在下一课 循环语句 中我们会更深入学习 break 的作用。
>还有要注意的是:最后必需使用 default 分支。 虽然 default 不加其实也不会报错,但即使程序真的不需要 default 处理,也应该保留语句,这样做并非画蛇添足,可以避免让人误以为你忘了 default 处理。 要把 default 子句用于检查真正的默认情况。
稍微有点晕?没关系,让我们用一个更完整的例子来加深对 switch 语句的理解:
假设你到了一个饭店,服务员给你看当日菜单:
>=== 菜单 === >1. 北京烤鸭 >2. 锅包肉 >3. 地三鲜 >4. 梅菜扣肉 > >您的选择是?
然后根据你的选择做出不同反应。应该怎么写呢?希望不要看答案,先自己写代码试试。
下面给出完整的示例程序: #include int main(int argc, char *argv[]) { int choiceMenu; printf("=== 菜单 ===nn"); printf("1. 北京烤鸭n"); printf("2. 锅包肉n"); printf("3. 地三鲜n"); printf("4. 梅菜扣肉n"); printf("n您的选择是 ? "); scanf("%d", &choiceMenu); printf("n"); switch (choiceMenu) { case 1: printf("您选了北京烤鸭。这是本店的招牌菜!"); break; case 2: printf("您选了锅包肉。您是东北人吧?"); break; case 3: printf("您选了地三鲜。实惠好吃,就是油有点多."); break; case 4: printf("您选了梅菜扣肉。嗯,那个味道很棒!"); break; default: printf("您没有选择餐单上的数字,难道您要饿肚子吗 ?"); break; } printf("n"); return 0; }
以上代码可以根据用户输入的不同数字做出相应的回答。
7. 三元表达式:精简的条件语句
除了 if...else 语句和 switch 语句,还有第三种条件语句,比 switch 更少用。
我们称其为 三元表达式 。
更确切地说,其实它就是一个 if...else 的变体,只不过我们把它写在一行里了。
因为实例总比长篇的解释来得更清晰易懂,所以我们用两个例子来说明。
这两个例子的功能相同,只不过第一个使用 if...else 语句,第二个使用三元表达式。
假设我们有一个布尔类型的变量 adult,当“成年”时它的值为真(1),当“未成年”时它的值为假(0)。
我们还有一个 int 型变量 age,要根据 adult 的值来改变 age 变量的值,下面先给出 if...else 的实现: if (adult) age = 18; else age = 17;
>注意:上例中我把 if 和 else 对应的大括号给去掉了。在只有一句执行语句的时候,去掉大括号是可以的,两句或以上就须要加上大括号了。 不过其实按照良好的代码习惯,应该无论如何都加上大括号。
上例用三元表达式实现则是这样: age = (adult) ? 18 : 17;
三元表达式使我们可以只用一行代码来根据条件改变变量的值。
问号表示首先判断 adult 是不是真值,如果是真,则取问号后面的 18,将 18 赋给 age;如果为假,取冒号后面的 17,将 17 赋给 age。
这里的问号就有点像 if 的条件判断,冒号就像 else。
事实上,三元表达式并不是那么常用,因为它会使代码变得难读,特别是当判断条件多且复杂的时候。
8. 总结 条件表达式是所有编程语言的基础内容,使电脑能够根据变量的值做出相应决定。 关键字 if,else...if,else 意味着“假如”,“又假如”,“否则”,我们可以写多个 else...if 。 布尔变量是一种特殊的变量,它只有两种状态:真(1)和假(0)(实际上,所有非零的整数值都被看作真),我们用整数类型 int 来储存布尔变量的值,因为 C语言其实没有定义布尔变量这种类型,而是用整数值来代表。Java、C++、C# 等语言就定义了布尔类型。 switch 语句是 if...else 语句的一个可用替换。在有多个 else...if 的情况下,可以使用 switch 代替 if...else 语句,能使代码更易阅读。 三元表达式(xx ? xx : xx)是精简的条件语句,但是我们要谨慎使用,因为它会让代码不那么易读。
9. 第一部分第九课预告
今天的课就到这里,一起加油吧!
下一课: C语言探索之旅 | 第一部分第九课:循环语句
>我是 谢恩铭 ,公众号「 程序员联盟 」运营者,慕课网精英讲师 Oscar 老师 ,终生学习者。 热爱生活,喜欢游泳,略懂烹饪。 人生格言:「向着标杆直跑」
软件开发
2020-05-27 11:48:00
题目
给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
例如: 给定二叉树 [3,9,20,null,null,15,7]
示例: 给定的有序链表: [-10, -3, 0, 5, 9], 一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树: 0 / -3 9 / / -10 5
返回其自底向上的层次遍历为: [ [15,7], [9,20], [3] ]
思路
本题和 102 题基本一致,区别就是每层的结点在二维数组中存放的先后顺序不同
前面所有代码都和 102 题一样,只需将最后存放位置每次都从 0 开始放就可以了
具体区别步骤写在代码注释处。
代码 /** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ class Solution { public List> levelOrderBottom(TreeNode root) { List> ans = new ArrayList<>(); if (root == null) { return ans; } Queue q = new LinkedList<>(); q.add(root); while (!q.isEmpty()) { List tmp = new ArrayList<>(); int len = q.size(); for (int i = 0; i < len; i++) { TreeNode node = q.poll(); tmp.add(node.val); if (node.left != null) { q.add(node.left); } if (node.right != null) { q.add(node.right); } } // 在索引 0 的位置加入一维数组 tmp // 每次新的数组进来都会被放在开始的位置 ans.add(0, tmp); } return ans; } }
软件开发
2020-05-27 10:45:00
一、观察者模式的概念 观察者模式(又被称为发布-订阅(Publish/Subscribe)模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
二、什么时候使用观察者模式
当一个对象被修改,或者发生某些变化时,需要自动通知依赖它的一些对象,则可以使用观察者模式。
例如:当汽车熄火时,需要更新汽车的定位、发送熄火的通知、生成本次的行程,就可以使用观察者模式,这个例子中发生变化的位置是熄火,那么发生熄火需要通知依赖它的一些对象,就是定位、通知和行程。
下面来看看,如何用Java语言实现观察者模式。
三、怎么使用观察者模式
3.1 不适用观察者模式的弊端
用上面说的业务场景为例,如果不使用观察者模式来实现,可以看下面的伪代码: public class EngineOffEvent() { public void engineOff() { Location localtion = new Localtion(); Notification notification = new Notification(); Trip trip = new Trip(); localtion.update(); notification.notify(); trip.createTrip(); } }
这种做法有如下两个主要弊端: 耦合度高:可以看到EngineOffEvent类耦合了所有其他的业务类,造成维护困难; 扩展性差:当需要在熄火时做新业务时,需要修改engineOff方法内的代码,违反了开闭原则; 灵活度低:不能控制通知的对象个数;
3.2 如何用观察者模式解决问题
根据上面的需求,用观察者模式进行设计,下面看看类图和代码:
代码如下: /** * 观察者接口。 * */ public interface Observer { /** * 当气象监测数据改变时,主题会把这些状态值当作方法的参数,传送给观察者。 * */ void update(String data); } public class LocaltionObserver implements Observer{ public void update(String data) { System.out.println(data + ":更新位置。"); } } public class NotificationObserver implements Observer{ public void update(String data) { System.out.println(data + ":发送通知。"); } } public class TripObserver implements Observer { public void update(String data) { System.out.println(data + ":生成行程。"); } } /** * 主题接口。 * */ public interface Subject { /** * registerObserver和removeObserver方法都需要一个观察者作为变量,该观察者是用来注册或被删除的。 * */ void registerObserver(Observer ob); void removeObserver(Observer ob); /** * 当主题的状态改变时,这个方法会被调用,已通知所有的观察者。 * */ void notifyObservers(String data); } // 熄火事件 public class EngineOffEvent implements Subject { // 观察者对象集合 private List observers; public EngineOffEvent() { observers = new ArrayList(); } // 注册观察者 public void registerObserver(Observer ob) { observers.add(ob); } // 移除观察者 public void removeObserver(Observer ob) { int index = observers.indexOf(ob); if (index >= 0) observers.remove(index); } // 发生变化进行通知 public void notifyObservers(String data) { // 通知全部观察者 for (Observer ob : observers) { ob.update(data); } } } // 使用 public class Client { public static void main(String[] args) { Subject subject = new EngineOffEvent(); subject.registerObserver(new LocaltionObserver()); subject.registerObserver(new NotificationObserver()); subject.registerObserver(new TripObserver()); subject.notifyObservers("京A88888熄火"); } } //京A88888熄火:更新位置。 //京A88888熄火:发送通知。 //京A88888熄火:生成行程。
观察者模式主要分为两部分: Subject,主题,是发生变化的那个对象,例子中是熄火事件; Observer,观察者,是发生变化后需要通知的多个对象,例子中是通知、位置和行程;
在使用时,可以动态添加或者删除观察者,灵活配置。
3.3 观察者模式的好处 耦合度低:解除了3.1小节中,EngineOffEvent类与其他的业务类的耦合; 扩展性高:当需要在熄火时做新业务时,创建一个新对象实现Observer,然后注册到Subject即可; 灵活度高:可以随意添加或移除Observer;
3.4 其他可以注意的地方
Java内置了观察者模式: java.util.Observable(类) java.util.Observer(接口)
可以使用内置的观察者,但是其中Observable对应的是上面例子中的Subject,他是一个Class而不是Interface,如果使用内置的接口,则需要把宝贵继承机会用掉,若你的类已经使用了继承关系,则无法使用。
四、总结
上面的观察者模式,从代码上来说,就是面相接口编程,在Subject中有一个List,在发生变化后,循环调用List中的对象的方法,确保都通知到,并且可以对List进行添加和删除。
从业务上来说,当发生某个事件后,需要批量通知许多个对象。则可以使用这个模式。
以上就是我对观察者模式的一些理解,有不足之处请大家指出,谢谢。
软件开发
2020-05-26 23:46:00
求:
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:
输入: [1,3,4,2,2]
输出: 2
示例 2:
输入: [3,1,3,4,2]
输出: 3
说明:
不能更改原数组(假设数组是只读的)。
只能使用额外的 O(1) 的空间。
时间复杂度小于 O(n2) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。

解:
思路:使用常规的数组遍历方法很容易获得答案,但是时间复杂度是O(N^2),而题目要求时间复杂度小于O(N^2)。我们也容易想到使用哈希表存储数字出现的次数,但这违反了只能使用O(1)的额外空间的约束。因此本题得到的算法,我们可以认为并不要求时间复杂度最优,而是使用部分时间换取空间,在时空效率上达到一个相对的平衡。(实际生产中时间换空间是少见的,本题具有特殊性)
常规方法如下:
int findDuplicate( int * nums, int numsSize){ int i,j; for (i= 0 ;i 因此我们进一步进行优化,我们先假设没有重复数,设cnt[i]为小于等于i的个数,那么对区间[1,N]上的每一个数,均出现一次,均满足:cnt[i]=i。现在我们假设在区间[1,N]中的某个位置(设为target)加入一个重复的数。加入重复的数后,对任意i=target,cnt[i]=i+1>i。以上假设target对应的数仅重复了一次,如果我们让这个数重复多次,那么可以认为我们使用target位置上的数去对其他位置的数进行了若干次替换。如果替换的数小于target,那么对ii。 因此我们可以得到,如果target是我们的目标结果,则对itarget,总是有cnt[i]>i。因此我们使用二分查找的方法,令low=0,high=numsSize-1,每次取区间中值mid,遍历原数组后如果cnt[mid]<=mid,说明结果在[mid+1,high]的分支中,更新low为mid+1,继续查找。如果cnt[mid]>mid,说明结果在[low,mid-1]的分支中,更新high为mid-1,记录当前的i,继续查找直到low==high。返回此时的i,即为所求的target。 int findDuplicate( int * nums, int numsSize){ int i,mid,ret; int low = 1 ; int high = numsSize- 1 ; while (low<=high){ int count = 0 ; mid = (low+high)>> 1 ; for (i= 0 ;i
软件开发
2020-05-26 19:26:00
状态模型
上图描述了 git 对象的在不同的生命周期中不同的存储位置,通过不同的 git 命令改变 git 对象的存储生命周期。
工作区 (workspace)
就是我们当前工作空间,也就是我们当前能在本地文件夹下面看到的文件结构。初始化工作空间或者工作空间 clean 的时候,文件内容和 index 暂存区是一致的,随着修改,工作区文件在没有 add 到暂存区时候,工作区将和暂存区是不一致的。
暂存区 (index)
老版本概念也叫 Cache 区,就是文件暂时存放的地方,所有暂时存放在暂存区中的文件将随着一个 commit 一起提交到 local repository 此时 local repository 里面文件将完全被暂存区所取代。暂存区是 git 架构设计中非常重要和难理解的一部分。
本地仓库 (local repository)
git 是分布式版本控制系统,和其他版本控制系统不同的是他可以完全去中心化工作,你可以不用和中央服务器 (remote server) 进行通信,在本地即可进行全部离线操作,包括 log,history,commit,diff 等等。完成离线操作最核心是因为 git 有一个几乎和远程一样的本地仓库,所有本地离线操作都可以在本地完成,等需要的时候再和远程服务进行交互。
远程仓库 (remote repository)
中心化仓库,所有人共享,本地仓库会需要和远程仓库进行交互,也就能将其他所有人内容更新到本地仓库把自己内容上传分享给其他人。结构大体和本地仓库一样。
文件在不同的操作下可能处于不同的 git 生命周期,下面看看一个文件变化的例子。
文件变化
对象模型 仓库结构
git 分布式的一个重要体现是 git 在本地是有一个完整的 git 仓库也就是 .git 文件目录,通过这个仓库,git 就可以完全离线化操作。在这个本地化的仓库中存储了 git 所有的模型对象。下面是 git 仓库的 tree 和相关说明:
git 主要有四个对象,分别是 Blob,Tree, Commit, Tag 他们都用 SHA-1 进行命名。
你可以用 git cat-file -t 查看每个 SHA-1 的类型,用 git cat-file -p 查看每个对象的内容和简单的数据结构。git cat-file 是 git 的瑞士军刀,是底层核心命令。
Blob 对象
只用于存储单个文件内容,一般都是二进制的数据文件,不包含任何其他文件信息,比如不包含文件名和其他元数据。
Tree 对象
对应文件系统的目录结构,里面主要有:子目录 (tree),文件列表 (blob),文件类型以及一些数据文件权限模型等。
详细解释如下:
Commit 对象
是一次修改的集合,当前所有修改的文件的一个集合,可以类比一批操作的“事务”。是修改过的文件集的一个快照,随着一次 commit 操作,修改过的文件将会被提交到 local repository 中。通过 commit 对象,在版本化中可以检索出每次修改内容,是版本化的基石。
修复增量bug 详细解释如下:
Tag 对象
tag 是一个"固化的分支",一旦打上 tag 之后,这个 tag 代表的内容将永远不可变,因为 tag 只会关联当时版本库中最后一个 commit 对象。
分支的话,随着不断的提交,内容会不断的改变,因为分支指向的最后一个 commit 不断改变。所以一般应用或者软件版本的发布一般用 tag。
git 的 Tag 类型有两种: 1 lightweight (轻量级)
创建方式:
这种方式创建的 Tag,git 底层不会创建一个真正意义上的 tag 对象,而是直接指向一个 commit 对象,此时如果使用 git cat-file -t tagName 会返回一个 commit。
2 annotated (含附注)
创建方式:
这种方式创建的标签,git 底层会创建一个 tag 对象,tag 对象会包含相关的 commit 信息和 tagger 等额外信息,此时如果使用 git cat-file -t tagname 会返回一个 tag。

总结:所有对象模型之间的关系大致如下:
存储模型
概念
git 区别与其他 vcs 系统的一个最主要原因之一是:git 对文件版本管理和其他 vcs 系统对文件版本的实现理念完成不一样。这也就是 git 版本管理为什么如此强大的最核心的地方。
Svn 等其他的 VCS 对文件版本的理念是以文件为水平维度,记录每个文件在每个版本下的 delta 改变。
Git 对文件版本的管理理念却是以每次提交为一次快照,提交时对所有文件做一次全量快照,然后存储快照引用。
Git 在存储层,如果文件数据没有改变的文件,Git 只是存储指向源文件的一个引用,并不会直接多次存储文件,这一点可以在 pack 文件中看见。
如下图所示:
存储
随着需求和功能的不断复杂,git 版本的不断更新,但是主要的存储模型还是大致不变。如下图所示:
检索模型
git 的对象有两种:
一种是松散对象,就是在如上 .git/objects 的文件夹 03 28 7f ce d0 d5 e6 f9 等,这些文件夹只有 2 个字符开头,其实就是每个文件 SHA-1 值的前 2 个字母,最多有 #OXFF 256 个文件夹。
一种是打包压缩对象,打包压缩之后的对象主要存在的是 pack 文件中,主要用于文件在网络上传输,减少网络消耗。
为了节省存储空间,可以手动触发打包压缩操作 (git gc),将松散对象打包成 pack 文件对象。也可以将 pack 文件解压缩成松散对象 (git unpack-objects)
为了加快 pack 文件的检索效率,git 基于 pack 文件会生成相应的索引 idx 文件。
pack 文件
pack 文件设计非常精密和巧妙,本着降低文件大小,减少文件传输,降低网络开销和安全传输的原则设计的。
pack 文件设计的概图如下:
pack 文件主要有三部分组成,Header, Body, Trailer
Header 部分主要 4-byte "PACK", 4-byte "版本号", 4-byte "Object 条目数"。 Body 部分主要是一个个 Git 对象依次存储,存储位置在 idx 索引文件中记录改对象在 pack 文件中的偏移量 offset。 Trailer 部分主要是所有 Objects 的名 (SHA-1)的校验和,为了安全可靠的文件传输。 下面我们看具体的 pack 文件:
从上图可知:通过 idx 索引文件在 pack 文件中定位到对象之后,对象的结构主要 Header 和 Data 两部分。
1 Header 部分
Header 中首 8-bits:1-bit 是 MSB,接着的 3-bits 表示的是当前对象类型,主要有 6种存储类型,接着的 4-bits 是用于表示该 Object 展开的 (length) 大小的一部分,只是一部分,完整的大小取决于MSB和接下来的多个 bits,完整算法如下:
如果 8-bits 中第一位是 1,表示下一个字节还是 header 的一部分,用于表示该对象展开的大小。 如果 8-bits 中第一位是 0,表示从下一个字节开始,将是数据 Data 文件。 如果对象类型是 OBJ_OFS_DELTA 类型, 表示的是 Delta 存储,当前 git 对象只是存储了增量部分,对于基本的部分将由接下来的可变长度的字节数用于表示 base object 的距离当前对象的偏移量,接下来的可变字节也是用 1-bit MSB 表示下一个字节是否是可变长度的组成部分。对偏移量取负数,就可知 base 对象在当前对象的前面多少字节。 如果对象类型是 OBJ_REF_DELTA 类型,表示的是 Delta 存储,当前 git 对象只是存储了增量部分,对于基本的部分,用 20-bytes 存储 Base Object 的 SHA-1 。
2 Data 部分
是经过 Zlib 压缩过的数据。可能是全部数据,也有可能是 Delta 数据,具体看 Header 部分的存储类型,如果是 OBJ_OFS_DELTA 或者 OBJ_REF_DELTA 此处存储的就是增量 (Delta) 数据,此时如果要取得全量数据的话,需要递归的找到最 Base Object,然后 apply delta 数据,在 base object 基础上进行 apply delta 数据也是非常精妙的,此文暂不做介绍。
从上面可以很清晰知道 pack 文件格式,我们再从本地仓库中一探究竟:
不是增量 delta 格式:
增量 delta 格式:
如 399334856af4ca4b49c0008a25b6a9f524e40350(SHA-1) 表示对象的 base object SHA-1 是 cb5a93c4cf9c0ee5b7153a3a35a4fac7a7584804,base 对象最大深度 (depth) 为 1,如果 cb5a93c4cf9c0ee5b7153a3a35a4fac7a7584804 还有引用对象,则改变 depth 为 2。
pack Header 中最后 4-bytes 用于表示的 pack 文件中 objects 的数量,最多 2 的 32 次方个对象,所以一些大的工程中有多个 pack 文件和多个 idx 文件。
文件的 size (文件解压缩后大小) 有什么用呢,这个是为了方便我们进行解压的时候,设置流的大小,也就是方便知道流有多大。这里 size 不是说明下一个文件的偏移量,偏移量都是来自索引文件,见下面 idx:
index 文件
由于 version1 比较简单,下面用 version2 为例子:
分层模式:Header,Fanout,SHA,CRC,Offset,Big File Offset,Trailer。
Header 层
version2 的 Header 部分总共有 8-bytes,version 1 的 header 部分是没有的,前 4-bytes 总是 255, 116, 79, 99 因为这个也是版本 1 的开头四个字节,后面 4-bytes 用于表示的是版本号,在当前就是 version 2。
Fanout 层
fanout 层是 git 的亮点设计,也叫 Fanout Table(扇表)。fanout 数组中存储的是相关对象的数目,数组下标是对应 16 进制数。fanout 最后一个存储的是整个 pack 文件中所有对象的总数量。Fanout Table 是整个 git 检索的核心,通过它我们可以快速进行查询,用于定位 SHA 层的数组起始 - 终止下标,定位好 SHA 层范围之后,就可以对 SHA 层进行二分查找了,而不用对所有对象进行二分查找。
fanout 总共 256 个,刚好是十六进制的 #0xFF。fanout 数组用 SHA 的前面 2 个字符作为下标(对应 .git/objects 中的松散文件目录名,将 16 进制的目录名转换 10 进制数字),里面值就是用这两个字符开头的文件数量,而且是逐层累加的,后面的数组数量是包含前面数组的数据的个数的一个累加。
举例如下:
1)如果数组下标为 0,且 Fanout[0] = 10 代表着 #0x00 开头的 SHA-1 值的总数为 10 个。
2) 如果数组下标为 1,且 Fanout[1] = 15 代表着小于 #0x01 开头的 SHA-1 值的总数为 15 个,从 Fanout[0] = 10 知 Fanout[1] = (15-10)
为什么 git 设计上 Fanout[n] 会累加 Fanout[n-1] 的数量?这个主要是为了快速确定 SHA 层检索的初始位置,而不用每次去把前面所有 fanout[..n-1] 数量进行累加。
SHA 层
是所有对象的 SHA-1 的排序,按照名称排序,按照名称进行排序是为了用二分搜索进行查找。每个 SHA-1 值占 20-bytes。
CRC 层
由于文件打包主要解决网络传输问题,网络传输的时候必须通过 crc 进行校验,避免传输过程中的文件损坏。CRC 数组对应的是每个对象的 CRC 校验和。
Offset 层
是由 4 byte 字节所组成,表示的是每个 SHA-1 文件的偏移量,但是如果文件大于 2G 之后,4 byte 字节将无法表示,此时将:
4 byte 中的第一 bit 就是 MSB,如果是 1 表示的是文件的偏移量是放在第 6 层去存储,此时剩下的 31-bits 将表示文件在 Big File Offset 中的偏移量,也就是图中的,通过 Big File Offset 层 就可以知道对象在 pack 中的 offset。
4 byte 中的第一 bit 就是 MSB,如果是 0 31-bits 表示的存储对象在 packfile 中的文件偏移量,此时不涉及 Big File Offset 层
Big File Offset 层
用于存储大于 2G 的文件的偏移量。如果文件大于 2G,可以通过 offset 层最后 31 bits 决定在 big file offset 中的位置,big file offset 通过 8 bytes 来表示对象在 pack 文件中的位置,理论上可以表示 2 的 64 次方文件大小。
Trailer 层
包含的是 packfile checksum 和关联的 idx 的 checksum。
索引流程
从上面的分层知道 git 设计的巧妙。git 索引文件偏移量的查询流程如下:
查询算法
通过 idx 文件查询 SHA-1 对应的偏移量:
在 pack 文件中通过偏移量找到对象:
如果是普通的存储类型。定位到的对象就是用 Zlib 压缩之后的对象,直接解压缩即可。
如果是 Delta 类型需要 递归查出 Delta 的 Base 对象,然后再把 delta data 应用到 base object 上(可参考 git-apply-delta)。
参考资料
git 大多资料主要介绍是 git 使用,很少系统去讲解底层数据结构和原理。本文通过多个开源代码入手,结合 git 文档,参考相关 git 开发者或相关研究文章,git 邮件列表等。下面是我探究觉得比较可靠的资料文档集。
参考文档
https://stackoverflow.com/questions/8198105/how-does-git-store-files https://www.npmjs.com/package/git-apply-delta https://git-scm.com/book/en/v2/Git-Internals-Packfiles https://codewords.recurse.com/issues/three/unpacking-git-packfiles http://shafiulazam.com/gitbook/7_the_packfile.html http://wiki.jikexueyuan.com/project/git-community-book/packfile.html http://documentup.com/skwp/git-workflows-book http://www.runoob.com/git/git-workspace-index-repo.html http://shafiulazam.com/gitbook/1_the_git_object_model.html http://eagain.net/articles/git-for-computer-scientists/ https://www.kernel.org/pub/software/scm/git/docs/user-manual.html#object-details https://stackoverflow.com/documentation/git/topics https://stackoverflow.com/search?page=2&tab=Votes&q=user%3a1256452%20%5bgit%5d http://git.oschina.net/progit/9-Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86.html#9.5-The-Refspec https://codewords.recurse.com/issues/three/unpacking-git-packfiles http://shafiulazam.com/gitbook/7_the_packfile.html https://w.org/pub/software/scm/git/docs/user-manual.html#object-details
git 源码
sha1_file.c sha1_object_info_extended 读取对象 sha1_file.c find_pack_entry_one 从索引中寻找
其他 git 源码
go-git https://github.com/src-d/go-git
gitgo https://github.com/ChimeraCoder/gitgo
编程之外 程序员需要哪些软技能? 程序员想要不断自我提升,除了持续精进技术之外,还需要哪些必备的软技能?在职业、学习上有没有可行的建议?程序员也需要自我营销吗?识别下方二维码,了解程序员编程之外的升值之道:
软件开发
2020-05-26 17:35:00
一.相关介绍
Dockerfile:关于Dockerfile的使用说明,我在文章《 让.NetCore程序跑在任何有docker的地方 》中有说到,这里不在赘述,需要的可以先看下,本文主要介绍Jenkinsfile结合dockerfile配合使用,自动构建.NetCore应用程序。
Jenkinsfile :Jenkinsfile 是 Jenkins 2.x 或更高版本核心特性 Pipeline(流水线) 的脚本,或者说对于Jenkins 流水线的定义被写在一个叫Jenkinsfile的文本文件中,该文件可以被提交到项目的源代码的控制仓库。这是"流水线即代码"的基础; 将CD 流水线作为应用程序的一部分,像其他代码一样进行版本化和审查。 创建 `Jenkinsfile`并提交它到源代码控制中提供了以下几个好处: 自动地为所有分支创建流水线构建过程并拉取请求。 在流水线上代码复查/迭代 (以及剩余的源代码)。 对流水线进行审计跟踪。 该流水线的真正的源代码 , 可以被项目的多个成员查看和编辑。
Jenkinsfile 能使用两种语法进行编写,分别是“声明式”和“脚本化”,二者语法都是 DSL(Domain-specific language) 语言,二者语法差不多,下面我们具体看下


二 .Jenkins和GitLab的安装
工欲善其事,必先利其器
Jenkins的安装可以看我之前的一篇文章: https://my.oschina.net/leonadmin/blog/4284501
Gitlab的安装推荐看下这篇文章: https://segmentfault.com/a/1190000021593151

三.打通GitLab Webhooks与Jenkins流程
1.这里先新建一个.net core应用程序,除了新增加了一个Jenkinsfile文件以外,其他没有任何代码的修改
注意,Jenkinsfile的J需要大写,我测试了,小写会提示找不到文件.
Jenkinsfile中添加了几行测试代码:
pipeline{ agent any stages { stage( ' Build ' ) { steps{ echo ' This is a build step ' } } stage( ' Test ' ) { steps{ echo ' This is a test step ' } } stage( ' Deploy ' ) { steps{ echo ' This is a deploy step ' } } } } 然后将程序文件push到Gitlab上

2.在Jenkins系统管理,系统配置中,在Gitlab处添加相关信息


下面点击添加凭据,类型选择GitLab Api Token,由于这个Token需要由GitLab来提供,所以先到GitLab生成一个Api Token

3.生成Api Token


点击创建之后,Token就生效了

将Api Token复制到Jenkins中,点击添加
添加完成后,就可以在 Credentials中选择刚刚添加好的凭据
点击保存,这样系统配置中的Gitlab的全局配置就生效了

4.新建流水线任务测试
下面打开Jenkins,新建一个流水线发布任务,取名test,然后点击确定.
勾选构建触发器
由于我们需要达到的目的是在GitLab提交代码,Jenkins可以自动构建相关任务,所以我们还需要配置一个触发器。如图,点击下面的"高级"按钮
点击“ Generate ”,生成一个 SecretToken
然后再流水线Tab定义处,下拉选择“ Pipeline script from SCM ”, SCM选择Git,然后填写代码库的地址和访问用户密码, 下面脚本路径有个Jenkinsfile,意思就是从我们提交到源代码管理根目录下面的Jenkinsfile中读取相关定义好的的流水线任务流程
点击保存

在Gitlab如下图地方进入,来配置web钩子

将在Jenkin中截图的一个url和生成的token分别填写在下方,在触发器的地方勾选上 Push events,然后点击下方的“ Add webhook ” 按钮。如果你还有其他事件发生时想触发Jenkins的任务,也可以自行勾选上
添加完成后点击测试
这个时候可以看到上方出现了成功的提示
而另一边Jenkins中,正在执行任务,并且任务的执行是 GitLab有推送任务
完成之后,可以看到流水线任务流程图
下面通过代码推送来测一下,我在跟目录新建一个空的 README.md文件,然后推送,推送完成后可以看到Jenkins开始执行第二次构建任务
至此,从GitLab提交代码到Jenkins自动构建的整个流程已经完成了,细心的你会发现,只是流程跑通了,这个Jenkinsfile没有实质性的内容,下面进入Jenkinsfile

四.接入Jenkinsfile,Dockerfile实现自动发布
编写Dockerfile如下:
FROM mcr.microsoft.com/dotnet/core/sdk: 3.1 - buster AS build COPY *.csproj ./app/ WORKDIR / app RUN dotnet restore COPY . . / RUN dotnet publish -o out /p:PublishWithAspNetCoreTargetManifest= " false " FROM mcr.microsoft.com /dotnet/core/aspnet: 3.1 -buster- slim AS runtime ENV ASPNETCORE_URLS http: // +:80 WORKDIR / app COPY -- from =build /app/ out ./ ENTRYPOINT [ " dotnet " , " WebApplication_Jenkinsfile.dll " ]


编写Jenkinsfile如下:
pipeline{ agent any stages { stage( ' Checkout ' ) { steps{ git credentialsId: ' 85ca7e47-532e-4901-9828-50a8da071d16 ' , url: ' http://xxx.gitlab.com/webapplication_jenkinsfile.git ' , branch: ' master ' echo ' ---This is a Checkout step--- ' } } stage( ' Build ' ) { steps{ sh ''' cd WebApplication_Jenkinsfile docker rmi -f docker_webapplication_test: 1.0 docker build -t docker_webapplication_test: 1.0 . ''' echo ' ---This is a Build step--- ' } } stage( ' Run ' ) { steps{ sh ''' docker rm -f docker_webapplication_test docker run --name docker_webapplication_test -d -p 7489 : 80 docker_webapplication_test: 1.0 ''' echo ' ---This is a run step--- ' } } } }

说明:
stages 必须,包括顺序执行的一个或多个stage命令,在pipeline内仅能使用一次,通常位于agent/options后面
steps 必须,steps位于stage指令块内部,包括一个或多个step。仅有一个step的情况下可以忽略关键字step及其{}
这里为了演示,只添加了几个核心的步骤,可以根据需要自行添加自动测试,邮件提醒等额外步骤

流水线语法,可以参照下图中的示例步骤点击后,可以生成脚本信息,上面Jenkinsfile中的git credentialsId信息就是在这里生成的

完成之后,将Jenkinsfile和Dockerfile推送到Gitlab,推送成功可以看到如下图的Jenkins自动构建过程,并且遵照定义规范发布了应用


如果需要查看日志,或者在发布失败后查看失败原因,可以通过下面的 Console Output来查看
Console Output中的详细日志信息如下图,可以看到构建过程是遵循在Dockerfile中的定义
docker 容器运行状态检查
自动部署效果截图

五.结束总结
在定义web钩子的时候,我勾选的是push events,所以每次有提交代码到master,都会自动去走Jenkinsfile中的流水线任务,在实际生产中,如果不想这样操作,可以将持续集成流水线与持续发布流水线独立开来
其实只需要前期一次将Jenkins和Gitlab触发器打通之后,后面只需要在源代码中维护Dockerfile和Jenkinsfile即可,这是比较优雅的一种CI、CD方式,动手试试吧!
软件开发
2020-05-26 17:34:00
C++ 九阴真经之单例模式
单例类在日常开发中是非常常见的,用于管理一些配置数据及资源,提供全局访问。
通过单例模式, 可以做到: 确保一个类只有一个实例被建立 提供了一个对对象的全局访问指针 在不影响单例类的客户端的情况下允许将来有多个实例
代码实现: //单例 template class Singleton : public noncopyable { public: static T & get_instance() { static T ins; do_nothing(instance); return ins; } private: static T & instance; static void do_nothing(T const &) {} }; template T & Singleton< T >::instance = Singleton< T >::get_instance();
优点: 编译期实例化,避免在多线程环境下产生多个实例。 与业务类分离,用户根据需要可自行定义多份单例,比较灵活。
测试代码: class Test { public: Test() { std::cout << "创建对象" << std::endl; a = 100; m_val = {{"1234", "234"}}; } int a; std::map m_val; }; using TestAgent = Singleton; int main() { std::cout << "开始" << std::endl; TestAgent::get_instance().a = 10; std::cout << "value:" << TestAgent::get_instance().a << std::endl; return 0; }
运行结果: 创建对象 开始 value:10
软件开发
2020-05-26 17:29:00
题目
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
例如,给定三角形: [ [2], [3,4], [6,5,7], [4,1,8,3] ]
自顶向下的最小路径和为 11 (即, 2 + 3 + 5 + 1 = 11)。
说明:
如果你可以只使用 O ( n ) 的额外空间( n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。
思路
https://leetcode-cn.com/problems/triangle/solution/javadong-tai-gui-hua-si-lu-yi-ji-dai-ma-shi-xian-b/
代码 public int minimumTotal(List> triangle) { // 特判 if (triangle == null || triangle.size() == 0) { return 0; } // dp中记录了求第i行时,第i+1的最小路径和 int[] dp = new int[triangle.size() + 1]; for (int i = triangle.size() - 1; i >= 0; i--) { List rows = triangle.get(i); for (int j = 0; j < rows.size(); j++) { dp[j] = Math.min(dp[j], dp[j + 1]) + rows.get(j); } } return dp[0]; }
软件开发
2020-05-26 16:16:00
支付对账,即检查第三方支付与数据库中账单是否一一对应,涉及到微信对账单的处理,成功时,微信账单接口返回数据以文本表格的方式返回,第一行为表头,后面各行为对应的字段内容,字段内容跟查询订单或退款结果一致,具体字段说明可查阅相应接口。
Ps:至于如何调用微信接口下载对账单,在微信的官方SDK包中就有现成的方法,直接调用即可。注意对账单接口一次只能查询一天的数据。
代码大致如下:
// 引入微信sdk文件 require_once APP_DIR . '/ome/lib/wxpay/lib/WxPay.Api.php'; require_once APP_DIR . '/ome/lib/wxpay/log.php'; // 实列化下载对账单对象 $input = new WxPayDownloadBill(); // 对账单日期 $input->SetBill_date(date("Ymd", strtotime("-1 day"))); // 对账单类型 $input->SetBill_type('ALL'); // 获取账单信息 $downloadBillResult = WxPayApi::downloadBill($input);
打印$downloadBillResult就会发现他是一个数据文本(string),本文程序需要实现的功能就是从这个字符串从提取每一笔订单中的有效信息,参考代码如下:
/** * 微信对账单数据处理 * @param $response 对账单数据 * @return array 返回结果 */ public function deal_WeChat_response($response){ $result = array(); $response = str_replace(","," ",$response); $response = explode(PHP_EOL, $response); foreach ($response as $key=>$val){ if(strpos($val, '`') !== false){ $data = explode('`', $val); array_shift($data); // 删除第一个元素并下标从0开始 if(count($data) == 24){ // 处理账单数据 $result['bill'][] = array( 'pay_time' => $data[0], // 支付时间 'APP_ID' => $data[1], // app_id 'MCH_ID' => $data[2], // 商户id 'IMEI' => $data[4], // 设备号 'order_sn_wx' => $data[5], // 微信订单号 'order_sn_sh' => $data[6], // 商户订单号 'user_tag' => $data[7], // 用户标识 'pay_type' => $data[8], // 交易类型 'pay_status' => $data[9], // 交易状态 'bank' => $data[10], // 付款银行 'money_type' => $data[11], // 货币种类 'total_amount' => $data[12], // 总金额 'coupon_amount' => $data[13], // 代金券或立减优惠金额 'refund_number_wx' => $data[14], // 微信退款单号 'refund_number_sh' => $data[15], // 商户退款单号 'refund_amount' => $data[16], // 退款金额 'coupon_refund_amount' => $data[17], // 代金券或立减优惠退款金额 'refund_type' => $data[18], // 退款类型 'refund_status' => $data[19], // 退款状态 'goods_name' => $data[20], // 商品名称 'service_charge' => $data[22], // 手续费 'rate' => $data[23], // 费率 ); } if(count($data) == 5){ // 统计数据 $result['summary'] = array( 'order_num' => $data[0], // 总交易单数 'turnover' => $data[1], // 总交易额 'refund_turnover' => $data[2], // 总退款金额 'coupon_turnover' => $data[3], // 总代金券或立减优惠退款金额 'rate_turnover' => $data[4], // 手续费总金额 ); } } } return $result; }
返回的数据分为2部分,bill节点下存放的是具体流水数据,summary节点下存放的是统计数据
主要的思路是微信账单返回的结果格式是固定的,可以用 '`',换行符(PHP_EOL)实现字符串的分割,然后每 24 个 字段为一个订单的描述信息,最后 6 个字段为账单的汇总信息。因此通过循环就可以遍历整个账单。

软件开发
2020-06-18 13:44:00
一、概述
分布式文件系统是分布式领域的一个基础应用,其中最著名的毫无疑问是 HDFS/GFS 。如今该领域已经趋向于成熟,但了解它的设计要点和思想,对我们将来面临类似场景/问题时,具有借鉴意义。
并且,分布式文件系统并非只有 HDFS/GFS 这一种形态,在它之外,还有其他形态各异、各有千秋的产品形态,对它们的了解,也对扩展我们的视野有所俾益。
本文试图分析和思考,在分布式文件系统领域,我们要解决哪些问题、有些什么样的方案、以及各自的选择依据。
二、过去的样子
在几十年以前,分布式文件系统就已经出现了,以 Sun 在 1984 年开发的“Network File System (NFS)”为代表,那时候解决的主要问题,是网络形态的磁盘,把磁盘从主机中独立出来。
这样不仅可以获得更大的容量,而且还可以随时切换主机,还可以实现数据共享、备份、容灾等,因为数据是电脑中最重要的资产。NFS 的数据通信图如下:
部署在主机上的客户端,通过 TCP/IP 协议把文件命令转发到远程文件 Server 上执行,整个过程对主机用户透明。
到了互联网时代,流量和数据快速增长,分布式文件系统所要解决的主要场景变了,开始需要非常大的磁盘空间,这在磁盘体系上垂直扩容是无法达到的,必须要分布式,同时分布式架构下,主机都是可靠性不是非常好的普通服务器,因此容错、高可用、持久化、伸缩性等指标,就成为必须要考量的特性。
三、对分布式文件系统的要求
对一个分布式文件系统而言,有一些特性是必须要满足的,否则就无法有竞争力。主要如下: 应该符合 POSIX 的文件接口标准,使该系统易于使用,同时对于用户的遗留系统也无需改造; 对用户透明,能够像使用本地文件系统那样直接使用; 持久化,保证数据不会丢失; 具有伸缩性,当数据压力逐渐增长时能顺利扩容; 具有可靠的安全机制,保证数据安全; 数据一致性,只要文件内容不发生变化,什么时候去读,得到的内容应该都是一样的。
除此之外,还有些特性是分布式加分项,具体如下: 支持的空间越大越好; 支持的并发访问请求越多越好; 性能越快越好; 硬件资源的利用率越高越合理,就越好。
四、架构模型
从业务模型和逻辑架构上,分布式文件系统需要这几类组件: 存储组件:负责存储文件数据,它要保证文件的持久化、副本间数据一致、数据块的分配 / 合并等等; 管理组件:负责 meta 信息,即文件数据的元信息,包括文件存放在哪台服务器上、文件大小、权限等,除此之外,还要负责对存储组件的管理,包括存储组件所在的服务器是否正常存活、是否需要数据迁移等; 接口组件:提供接口服务给应用使用,形态包括 SDK(Java/C/C++ 等)、CLI 命令行终端、以及支持 FUSE 挂载机制。
而在部署架构上,有着“中心化”和“无中心化”两种路线分歧,即是否把“管理组件”作为分布式文件系统的中心管理节点。两种路线都有很优秀的产品,下面分别介绍它们的区别。
1、有中心节点
以 GFS 为代表,中心节点负责文件定位、维护文件 meta 信息、故障检测、数据迁移等管理控制的职能,下图是 GFS 的架构图:
该图中 GFS master 即为 GFS 的中心节点,GF chunkserver 为 GFS 的存储节点。其操作路径如下: Client 向中心节点请求“查询某个文件的某部分数据”; 中心节点返回文件所在的位置 (哪台 chunkserver 上的哪个文件) 以及字节区间信息; Client 根据中心节点返回的信息,向对应的 chunk server 直接发送数据读取的请求; chunk server 返回数据。
在这种方案里,一般中心节点并不参与真正的数据读写,而是将文件 meta 信息返回给 Client 之后,即由 Client 与数据节点直接通信。其主要目的是降低中心节点的负载,防止其成为瓶颈。这种有中心节点的方案,在各种存储类系统中得到了广泛应用,因为中心节点易控制、功能强大。
2、无中心节点
以 ceph 为代表,每个节点都是自治的、自管理的,整个 ceph 集群只包含一类节点,如下图(最下层红色的 RADOS 就是 ceph 定义的“同时包含 meta 数据和文件数据”的节点)。
无中心化的最大优点是解决了中心节点自身的瓶颈,这也就是 ceph 号称可以无限向上扩容的原因。但由 Client 直接和 Server 通信,那么 Client 必须要知道,当对某个文件进行操作时,它该访问集群中的哪个节点。ceph 提供了一个很强大的原创算法来解决这个问题——CRUSH 算法。
CRUSH: https://ceph.com/wp-content/uploads/2016/08/weil-crush-sc06.pdf
五、持久化
对于文件系统来说,持久化是根本,只要 Client 收到了 Server 保存成功的回应之后,数据就不应该丢失。这主要是通过多副本的方式来解决,但在分布式环境下,多副本有这几个问题要面对。 如何保证每个副本的数据是一致的? 如何分散副本,以使灾难发生时,不至于所有副本都被损坏? 怎么检测被损坏或数据过期的副本,以及如何处理? 该返回哪个副本给 Client?
1、如何保证每个副本的数据是一致的?
同步写入是保证副本数据一致的最直接的办法。当 Client 写入一个文件的时候,Server 会等待所有副本都被成功写入,再返回给 Client。
这种方式简单、有保障,唯一的缺陷就是性能会受到影响。假设有 3 个副本,如果每个副本需要 N 秒,则可能会阻塞 Client 3N 秒的时间,有几种方式,可以对其进行优化: 并行写:由一个副本作为主副本,并行发送数据给其他副本; 链式写:几个副本组成一个链 (chain),并不是等内容都接受到了再往后传播,而是像流一样,边接收上游传递过来的数据,一边传递给下游。
还有一种方式是采用 CAP 中所说的 W+R>N 的方式,比如 3 副本 (N=3) 的情况,W=2,R=2,即成功写入 2 个就认为成功,读的时候也要从 2 个副本中读。这种方式通过牺牲一定的读成本,来降低写成本,同时增加写入的可用性。这种方式在分布式文件系统中用地比较少。
2、如何分散副本,以使灾难发生时,不至于所有副本都被损坏?
这主要避免的是某机房或某城市发生自然环境故障的情况,所以有一个副本应该分配地比较远。它的副作用是会带来这个副本的写入性能可能会有一定的下降,因为它离 Client 最远。所以如果在物理条件上无法保证够用的网络带宽的话,则读写副本的策略上需要做一定考虑。
可以参考同步写入只写 2 副本、较远副本异步写入的方式,同时为了保证一致性,读取的时候又要注意一些,避免读取到异步写入副本的过时数据。
3、怎么检测被损坏或数据过期的副本,以及如何处理?
如果有中心节点,则数据节点定期和中心节点进行通信,汇报自己的数据块的相关信息,中心节点将其与自己维护的信息进行对比。如果某个数据块的 checksum 不对,则表明该数据块被损坏了;如果某个数据块的 version 不对,则表明该数据块过期了。
如果没有中心节点,以 ceph 为例,它在自己的节点集群中维护了一个比较小的 monitor 集群,数据节点向这个 monitor 集群汇报自己的情况,由其来判定是否被损坏或过期。
当发现被损坏或过期副本,将它从 meta 信息中移除,再重新创建一份新的副本就好了,移除的副本在随后的回收机制中会被收回。
4、该返回哪个副本给 Client?
这里的策略就比较多了,比如 round-robin、速度最快的节点、成功率最高的节点、CPU 资源最空闲的节点、甚至就固定选第一个作为主节点,也可以选择离自己最近的一个,这样对整体的操作完成时间会有一定节约。
六、伸缩性
1、存储节点的伸缩
当在集群中加入一台新的存储节点,则它主动向中心节点注册,提供自己的信息,当后续有创建文件或者给已有文件增加数据块的时候,中心节点就可以分配到这台新节点了,比较简单。但有一些问题需要考虑。 如何尽量使各存储节点的负载相对均衡? 怎样保证新加入的节点,不会因短期负载压力过大而崩塌? 如果需要数据迁移,那如何使其对业务层透明?
1)如何尽量使各存储节点的负载相对均衡?
首先要有评价存储节点负载的指标。有多种方式,可以从磁盘空间使用率考虑,也可以从磁盘使用率 +CPU 使用情况 + 网络流量情况等做综合判断。一般来说,磁盘使用率是核心指标。
其次在分配新空间的时候,优先选择资源使用率小的存储节点;而对已存在的存储节点,如果负载已经过载、或者资源使用情况不均衡,则需要做数据迁移。
2)怎样保证新加入的节点,不会因短期负载压力过大而崩塌?
当系统发现当前新加入了一台存储节点,显然它的资源使用率是最低的,那么所有的写流量都路由到这台存储节点来,那就可能造成这台新节点短期负载过大。因此,在资源分配的时候,需要有预热时间,在一个时间段内,缓慢地将写压力路由过来,直到达成新的均衡。
3)如果需要数据迁移,那如何使其对业务层透明?
在有中心节点的情况下,这个工作比较好做,中心节点就包办了——判断哪台存储节点压力较大,判断把哪些文件迁移到何处,更新自己的 meta 信息,迁移过程中的写入怎么办,发生重命名怎么办。无需上层应用来处理。
如果没有中心节点,那代价比较大,在系统的整体设计上,也是要考虑到这种情况,比如 ceph,它要采取逻辑位置和物理位置两层结构,对 Client 暴露的是逻辑层 (pool 和 place group),这个在迁移过程中是不变的,而下层物理层数据块的移动,只是逻辑层所引用的物理块的地址发生了变化,在 Client 看来,逻辑块的位置并不会发生改变。
2、中心节点的伸缩
如果有中心节点,还要考虑它的伸缩性。由于中心节点作为控制中心,是主从模式,那么在伸缩性上就受到比较大的限制,是有上限的,不能超过单台物理机的规模。我们可以考虑各种手段,尽量地抬高这个上限。有几种方式可以考虑:
以大数据块的形式来存储文件——比如 HDFS 的数据块的大小是 64M,ceph 的的数据块的大小是 4M,都远远超过单机文件系统的 4k。它的意义在于大幅减少 meta data 的数量,使中心节点的单机内存就能够支持足够多的磁盘空间 meta 信息。
中心节点采取多级的方式——顶级中心节点只存储目录的 meta data,其指定某目录的文件去哪台次级总控节点去找,然后再通过该次级总控节点找到文件真正的存储节点;
中心节点共享存储设备——部署多台中心节点,但它们共享同一个存储外设 / 数据库,meta 信息都放在这里,中心节点自身是无状态的。这种模式下,中心节点的请求处理能力大为增强,但性能会受一定影响。iRODS 就是采用这种方式。
七、高可用性
1、中心节点的高可用
中心节点的高可用,不仅要保证自身应用的高可用,还得保证 meta data 的数据高可用。
meta data 的高可用主要是数据持久化,并且需要备份机制保证不丢。一般方法是增加一个从节点,主节点的数据实时同步到从节点上。也有采用共享磁盘,通过 raid1 的硬件资源来保障高可用。显然增加从节点的主备方式更易于部署。
meta data 的数据持久化策略有以下几种方式: 直接保存到存储引擎上,一般是数据库。直接以文件形式保存到磁盘上,也不是不可以,但因为 meta 信息是结构化数据,这样相当于自己研发出一套小型数据库来,复杂化了。 保存日志数据到磁盘文件 (类似 MySQL 的 binlog 或 Redis 的 aof),系统启动时在内存中重建成结果数据,提供服务。修改时先修改磁盘日志文件,然后更新内存数据。这种方式简单易用。
当前内存服务 + 日志文件持久化是主流方式。一是纯内存操作,效率很高,日志文件的写也是顺序写;二是不依赖外部组件,独立部署。
为了解决日志文件会随着时间增长越来越大的问题,以让系统能以尽快启动和恢复,需要辅助以内存快照的方式——定期将内存 dump 保存,只保留在 dump 时刻之后的日志文件。这样当恢复时,从最新一次的内存 dump 文件开始,找其对应的 checkpoint 之后的日志文件开始重播。
2、存储节点的高可用
在前面“持久化”章节,在保证数据副本不丢失的情况下,也就保证了其的高可用性。
八、性能优化和缓存一致性
这些年随着基础设施的发展,局域网内千兆甚至万兆的带宽已经比较普遍,以万兆计算,每秒传输大约 1250M 字节的数据,而 SATA 磁盘的读写速度这些年基本达到瓶颈,在 300-500M/s 附近,也就是纯读写的话,网络已经超过了磁盘的能力,不再是瓶颈了,像 NAS 网络磁盘这些年也开始普及起来。
但这并不代表,没有必要对读写进行优化,毕竟网络读写的速度还是远慢于内存的读写。常见的优化方法主要有: 内存中缓存文件内容; 预加载数据块,以避免客户端等待; 合并读写请求,也就是将单次请求做些积累,以批量方式发送给 Server 端。
缓存的使用在提高读写性能的同时,也会带来数据不一致的问题: 会出现更新丢失的现象。当多个 Client 在一个时间段内,先后写入同一个文件时,先写入的 Client 可能会丢失其写入内容,因为可能会被后写入的 Client 的内容覆盖掉; 数据可见性问题。Client 读取的是自己的缓存,在其过期之前,如果别的 Client 更新了文件内容,它是看不到的;也就是说,在同一时间,不同 Client 读取同一个文件,内容可能不一致。
这类问题有几种方法: 文件只读不改:一旦文件被 create 了,就只能读不能修改。这样 Client 端的缓存,就不存在不一致的问题; 通过锁:用锁的话还要考虑不同的粒度。写的时候是否允许其他 Client 读? 读的时候是否允许其他 Client 写? 这是在性能和一致性之间的权衡,作为文件系统来说,由于对业务并没有约束性,所以要做出合理的权衡,比较困难,因此最好是提供不同粒度的锁,由业务端来选择。但这样的副作用是,业务端的使用成本抬高了。
九、安全性
由于分布式文件存储系统,肯定是一个多客户端使用、多租户的一个产品,而它又存储了可能是很重要的信息,所以安全性是它的重要部分。
主流文件系统的权限模型有以下这么几种:
DAC:全称是 Discretionary Access Control,就是我们熟悉的 Unix 类权限框架,以 user-group-privilege 为三级体系,其中 user 就是 owner,group 包括 owner 所在 group 和非 owner 所在的 group、privilege 有 read、write 和 execute。这套体系主要是以 owner 为出发点,owner 允许谁对哪些文件具有什么样的权限。
MAC:全称是 Mandatory Access Control,它是从资源的机密程度来划分。比如分为“普通”、“机密”、“绝密”这三层,每个用户可能对应不同的机密阅读权限。这种权限体系起源于安全机构或军队的系统中,会比较常见。它的权限是由管理员来控制和设定的。Linux 中的 SELinux 就是 MAC 的一种实现,为了弥补 DAC 的缺陷和安全风险而提供出来。关于 SELinux 所解决的问题可以参考 What is SELinux?
RBAC:全称是 Role Based Access Control,是基于角色 (role) 建立的权限体系。角色拥有什么样的资源权限,用户归到哪个角色,这对应企业 / 公司的组织机构非常合适。RBAC 也可以具体化,就演变成 DAC 或 MAC 的权限模型。
What is SELinux: https://www.cyberciti.biz/faq/what-is-selinux/
市面上的分布式文件系统有不同的选择,像 ceph 就提供了类似 DAC 但又略有区别的权限体系,Hadoop 自身就是依赖于操作系统的权限框架,同时其生态圈内有 Apache Sentry 提供了基于 RBAC 的权限体系来做补充。
十、其他
1、空间分配
有连续空间和链表空间两种。连续空间的优势是读写快,按顺序即可,劣势是造成磁盘碎片,更麻烦的是,随着连续的大块磁盘空间被分配满而必须寻找空洞时,连续分配需要提前知道待写入文件的大小,以便找到合适大小的空间,而待写入文件的大小,往往又是无法提前知道的(比如可编辑的 word 文档,它的内容可以随时增大);
而链表空间的优势是磁盘碎片很少,劣势是读写很慢,尤其是随机读,要从链表首个文件块一个一个地往下找。
为了解决这个问题,出现了索引表——把文件和数据块的对应关系也保存一份,存在索引节点中(一般称为 i 节点),操作系统会将 i 节点加载到内存,从而程序随机寻找数据块时,在内存中就可以完成了。通过这种方式来解决磁盘链表的劣势,如果索引节点的内容太大,导致内存无法加载,还有可能形成多级索引结构。
2、文件删除
实时删除还是延时删除?
实时删除的优势是可以快速释放磁盘空间;延时删除只是在删除动作执行的时候,置个标识位,后续在某个时间点再来批量删除,它的优势是文件仍然可以阶段性地保留,最大程度地避免了误删除,缺点是磁盘空间仍然被占着。
在分布式文件系统中,磁盘空间都是比较充裕的资源,因此几乎都采用逻辑删除,以对数据可以进行恢复,同时在一段时间之后 (可能是 2 天或 3 天,这参数一般都可配置),再对被删除的资源进行回收。
怎么回收被删除或无用的数据? 可以从文件的 meta 信息出发——如果 meta 信息的“文件 - 数据块”映射表中包含了某个数据块,则它就是有用的;如果不包含,则表明该数据块已经是无效的了。所以,删除文件,其实是删除 meta 中的“文件 - 数据块”映射信息 (如果要保留一段时间,则是把这映射信息移到另外一个地方去)。
3、面向小文件的分布式文件系统
有很多这样的场景,比如电商——那么多的商品图片、个人头像,比如社交网站——那么多的照片,它们具有的特性,可以简单归纳下: 每个文件都不大; 数量特别巨大; 读多写少; 不会修改。
针对这种业务场景,主流的实现方式是仍然是以大数据块的形式存储,小文件以逻辑存储的方式存在,即文件 meta 信息记录其是在哪个大数据块上,以及在该数据块上的 offset 和 length 是多少,形成一个逻辑上的独立文件。这样既复用了大数据块系统的优势和技术积累,又减少了 meta 信息。
4、文件指纹和去重
文件指纹就是根据文件内容,经过算法,计算出文件的唯一标识。如果两个文件的指纹相同,则文件内容相同。在使用网络云盘的时候,发现有时候上传文件非常地快,就是文件指纹发挥作用。云盘服务商通过判断该文件的指纹,发现之前已经有人上传过了,则不需要真的上传该文件,只要增加一个引用即可。在文件系统中,通过文件指纹可以用来去重、也可以用来判断文件内容是否损坏、或者对比文件副本内容是否一致,是一个基础组件。
文件指纹的算法也比较多,有熟悉的 md5、sha256、也有 google 专门针对文本领域的 simhash 和 minhash 等。
十一、总结
分布式文件系统内容庞杂,要考虑的问题远不止上面所说的这些,其具体实现也更为复杂。本文只是尽量从分布式文件系统所要考虑的问题出发,给予一个简要的分析和设计,如果将来遇到类似的场景需要解决,可以想到“有这种解决方案”,然后再来深入研究。
同时,市面上也是存在多种分布式文件系统的形态,下面就是有研究小组曾经对常见的几种分布式文件系统的设计比较。
从这里也可以看到,选择其实很多,并不是 GFS 论文中的方式就是最好的。在不同的业务场景中,也可以有更多的选择策略。
作者 | 张轲
来源丨 https://www.jianshu.com/p/fc0aa34606ce 文源网络,仅供学习之用,如有侵权请联系删除。
我将面试题和答案都整理成了PDF文档,还有一套学习资料,涵盖Java虚拟机、spring框架、Java线程、数据结构、设计模式等等,但不仅限于此。
关注公众号【java圈子】获取资料,还有优质文章每日送达。
软件开发
2020-06-17 16:24:00