Airthink


  • 首页

  • 分类

  • 归档

  • 标签

  • 关于

  • 搜索

阿里云web应用部署

发表于 2019-02-24 | 分类于 java

阿里云服务器

  1. 阿里云服务器的注册及申请
  2. 添加安全组规则。安全组在云端提供类似虚拟防火墙功能,用于设置单个或多个 ECS 实例的网络访问控制,它是重要的安全隔离手段。在创建 ECS 实例时,必须选择一个安全组。您还可以添加安全组规则,对该安全组下的所有 ECS 实例的出方向和入方向进行网络控制。 若没有配置安全组规则,直接在本地ping服务器,结果ping不通,ssh也连不上。

默认安全组中的默认规则仅设置针对ICMP协议、SSH 22端口、RDP 3389端口、HTTP 80端口和HTTPS 443端口的入方向规则。网络类型不同,安全且规则不同。

安全组常用配置 例如:redis端口6379/mongo数据库端口27017/mysql端口3306/

服务器环境部署

  1. 使用shell工具上传 JDK;安装svn,安装maven;新建项目文件夹上传tomcat、mongo、redis安装包。
  2. 配置环境变量

    java环境变量

    JAVA_HOME=/usr/local/jdk1.8.0_162
    export JRE_HOME=/usr/local/jdk1.8.0_162/jre
    export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
    export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$JAVA_HOME:$PATH:$MAVEN_HOME/bin
    export MAVEN_HOME=/usr/local/maven/apache-maven-3.5.4
    

    redis配置

    找到redis.conf文件将bind 127.0.0.1注释,改为bind 0.0.0.0这样就可以支持远程连接
    找到requirepass  设置redis登录密码
    

    mongo配置

    找到mongo.conf配置auth先设置false
    port=27017
    logpath=/airthink/mongodb-linux-x86_64-3.6.3/mongod.log
    pidfilepath=/airthink/mongodb-linux-x86_64-3.6.3/mongod.pid
    logappend=true
    fork=true
    maxConns=3000
    dbpath=/airthink/mongodb-linux-x86_64-3.6.3/data
    auth=false
    
    ./mongodb-linux-x86_64-3.6.3/bin/mongo   进入mongo shell  创建超级管理员admin账户
    use admin
    db.createUser(
      {
        user: "admin",
        pwd: "123456",
        roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
      } )
    
    现在启用auth 将配置文件中auth=false 改为 auth=true
    
    use admin
    db.auth('admin','123456');授权
    
    创建数据库
    db.createUser({user:"用户名",pwd:"密码",roles:[{role:"readWrite",db:"项目名"}]})
    

部署web项目

在项目文件夹下使用svn命令
svn checkout svn://路径(目录或文件的全路径)

首次checkout出项目后,运行自动发布脚本,删除开发配置文件,替换生产配置文件,使用mvn打包,将war包复制到tomcat的webapp下,重启tomcat。

问题

Linux执行.sh文件,提示No such file or directory的问题的解决方法:

原因:在windows中写好shell脚本测试正常,但是上传到 Linux 上以脚本方式运行命令时提示No such file or directory错误,那么一般是文件格式是dos格式的缘故,改成unix 格式即可。一般有如下几种修改办法。

用vim打开该sh文件,输入:
:set ff
回车,显示fileformat=dos,重新设置下文件格式:
:set ff=unix
保存退出:
:wq
再执行

免费ssh证书申请及配置

参考https://help.aliyun.com/knowledge_detail/95496.html?spm=a2c4g.11186623.2.11.53674c07M5nwXN

Java异常及使用

发表于 2019-02-02 | 分类于 java

Java异常

Java异常类层次结构图

java 异常是程序运行过程中出现的错误。Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。在Java API中定义了许多异常类,分为两大类,错误Error和异常Exception。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常(非runtimeException),也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

Error与Exception

Error是程序无法处理的错误,比如OutOfMemoryError、ThreadDeath等。
这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。

运行时异常和非运行时异常

1.运行时异常: 都是RuntimeException类及其子类异常:

IndexOutOfBoundsException:索引越界异常

ArithmeticException:数学计算异常

NullPointerException:空指针异常

ArrayOutOfBoundsException:数组索引越界异常

ClassNotFoundException:类文件未找到异常

ClassCastException:造型异常(类型转换异常)

这些异常是不检查异常(Unchecked Exception),程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的。

2.非运行时异常:是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如:

IOException:文件读写异常

FileNotFoundException:文件未找到异常

EOFException:读写文件尾异常

MalformedURLException:URL格式错误异常

SocketException:Socket异常

SQLException:SQL数据库异常

异常的捕获和处理

Java异常的捕获和处理是一个不容易把握的事情,如果处理不当,不但会让程序代码的可读性大大降低, 而且导致系统性能低下,甚至引发一些难以发现的错误。

1.try、catch方式:

try{   
//(尝试运行的)程序代码   
}catch(异常类型 异常的变量名){   
//异常处理代码   
}finally{   
 //异常发生,方法返回之前,总是要执行的代码   
}  

try、catch、finally三个语句块应注意的问题
第一、try、catch、finally三个语句块均不能单独使用,三者可以组成 try…catch…finally、try…catch、 try…finally三种结构,catch语句可以有一个或多个,finally语句最多一个。
第二、try、catch、finally三个代码块中变量的作用域为代码块内部,分别独立而不能相互访问。如果要在三个块中都可以访问,则需要将变量定义到这些块的外面。
第三、多个catch块时候,只会匹配其中一个异常类并执行catch块代码,而不会再执行别的catch块,并且匹配catch语句的顺序是由上到下。

2.抛异常给上一级方式:

public static void demo() throws Exception{   
  //抛出一个检查异常   
  throw new Exception("方法demo中的Exception");   
}

上面的代码可以看到两个关键字,throw和throws关键字
throw:是用于方法体内部,用来抛出一个Throwable类型的异常。
throws:是用于方法体外部的方法声明部分,用来声明方法可能会抛出某些异常。仅当抛出了检查异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出。
异常处理是为了程序的健壮性。异常能处理就处理,不能处理就抛出,最终没有处理的异常JVM会进行处理。对于一个应用系统来说,应该有自己的一套异常处理框架,这样当异常发生时,也能得到统一的处理风格,将优雅的异常信息反馈给用户。

Java异常使用

在开发业务系统中,目前绝大多数采用MVC模式。
考虑如下场景:系统提供一个API,用于修改用户信息,服务器端采用json数据交互.首先我们定义BaseException,用来表示业务逻辑受理失败,它仅表示我们处理业务的时候发现无法继续执行下去。

/**
 * 业务受理失败异常
 */
public class BaseException extends RuntimeException{
    public ServiceException() {
        super();
    }

    public BaseException(String arg0, Throwable arg1) {
        super(arg0, arg1);
    }

    public BaseException(String arg0) {
        super(arg0);
    }

    public BaseException(Throwable arg0) {
        super(arg0);
    }
}

接下来Controller层

@RequestMapping
pulic ReturnStatus updateUser(User user){
    userService.updateUser(user);
    ReturnStatus status = new ReturnStatus(ture,"更新成功");
    status.setEntity(user)
    return status;
}

关于上述Controller写法乍一看会有一些冗余,如果无法理解,请仔细研读MVC设计模式. 先不管service,我们来考虑下。 一个业务系统不可能不对用户提交的数据进行验证,验证包括两方面:有效性和合法性

  • 有效性: 比如用户所在岗位,是否属于数据库有记录的岗位ID,如果不存在,无效.
  • 合法性: 比如用户名只允许输入最多12个字符,用户提交了20个字符,不合法.

有效性检查,可以交给java的校验框架执行,比如JSR303。假设用户提交的数据经过验证都合法,还是有一些情况是不能调用修改逻辑的。

  1. 要修改的用户ID不存在.
  2. 用户被锁定,不允许修改.
  3. 乐观锁机制发现用户已经被被人修改过.
  4. 由于某种原因,我们的程序无法保存到数据库.
  5. 一些程序员错误的开发了代码,导致保存过程中出现异常,比如NPE.

对于前3种,我们认为是有效性检查失败,第4种属与我们无法处理的异常,第5种就是程序员bug。

现在的问题是,前三种情况我们如何通知用户呢?

1.在ccontroller 调用userService的checkUserExist()方法.
2.在controller直接书写业务逻辑.
3.在service响应一个状态码机制,比如1 2 3表示错误信息,0 表示没有任何错误.

显然前2种方法都不可取,因为MVC设计模式告诉我们,controller是用来接收页面参数,并且调用逻辑处理,最后组织页面响应的地方。我们不可以在controller进行逻辑处理,controller只应该负责用户API入口和响应的处理(如若不然,思考一下如果有一天service的代码打包成jar放到另一个平台,没有controller了,该怎么办?)

状态码机制是个不错的选择,可是如此一来,用户保存逻辑变了,比如增加一个情况,不允许修改已经离职的用户,那么我们还需要修改controller的代码,代码量增加,维护成本增高,并且还耦合了service,不符合MVC设计模式。

那么怎么办呢?现在我们来看下service代码如何编写

public void updateUser(User user){
    User userOrig = userDao.getUserById(user.getUserById);
    if(null == userOrig){
        throw new BaseException("用户不存在");
    }
    if(userOrig.isLocked()){
        throw new BaseException("用户被锁定,不允许修改");
    }
    if(!user.getVersion.equals(userOrig.getVersion)){
        throw new BaseException("用户已经被别人修改,请刷新");
    }
    //TODO保存用户数据...
}

这样一来只要我们检查到不允许保存的项目,我们就可以直接throw 一个新的异常,异常机制会帮助我们中断代码执行。

接下来有2种选择:

  1. 在controller 使用try-catch进行处理.
  2. 直接把异常抛给上层框架统一处

第1种方式是不可取的,注意我们抛出的BaseException,它仅仅逻辑处理异常,并且我们的方法前面没有声明throws BaseException,这表示他是一个非受查异常.controller也没有关心会发生什么异常。

为什么不定义成受查异常呢?如果是一个受查异常,那么意味着controller必须要处理你的异常。并且如果有一天你的业务逻辑变了,可能多一种检查项,就需要增加一个异常,反之需要删除一个异常,那么你的方法签名也需要改变,controller也随之要改变,这又变成了紧耦合,这和用状态码123表示处理结果没有什么不同。

我们可以为每一种检查项定义一个异常吗?可以,但是那样显得太多余了。因为业务逻辑处理失败的时候,根据我们需求,我们只需要通知用户失败的原因(通常应该是一段字符串),以及服务器受理失败的一个状态码(有时可能不需要状态码,这要看你的设计了),这样这需要一个包含原因属性的异常即可满足我们需求。

最后我们决定这个异常继承自RuntimeException。并且包含一个接受一个错误原因的构造器,这样controller层也不需要知道异常,只要全局捕获到BaseException做统一的处理即可,这无论是在struct1,2时代,还是springMVC中,甚至servlet年代,都是极为容易的!

异常不提供无参构造器,因为绝对不允许你抛出一个逻辑处理异常,但是不指明原因,想想看,你是必须要告诉用户为什么受理失败的!

如此一来,我们只需要全局统一处理下 BaseException 就可以了,很好,spring为我们提供了ControllerAdvice机制,有关ControllerAdvice,可以查阅springMVC使用文档,下面是一个简单的示例:

@ControllerAdvice(basePackages = {"com.xxx.xxx.bussiness.xxx"})
public class ModuleControllerAdvice{
    public static final Logger LOGGER = LoggerFactory.getLogger(ModuleControllerAdvice.class);
    public static final Logger BASE_LOGGER = LoggerFactory.getLogger(BaseException.class);
    /**
     * 业务受理失败
     */
     @ResponseBody
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     @ExceptionHandler(BaseException.class)
     private ReturnStatus handleServiceException(BaseException exception){
        String message = "业务受理失败,原因:"+exception.getLocalizedMessage();
        SERVICE_LOGGER.info(message);
        ReturnStatus status = new ReturnStatus(MErrorCode.e2001.code(), MErrorCode.e2001.message());//自定义枚举异常类
        return status;
     }
}

在这个时候,我们就可以很轻松的处理各种情况了.

注意一点,在这个类中,我们定义了2个log对象,分别指向 BaseException.class 和 ModuleControllerAdvice.class。并且处理 BaseException的时候使用了info级别的日志输出,这是很有用的。

首先,BaseException一定要和其他的代码错误分离,不应该混为一谈.
其次,BaseException并不一定要记录日志,我们应该提供独立的log对象,方便开关.
接下来你可以在修改用户的时候想客户端响应这样的JSON

{
    code : e2001,
    message : "用户不存在"
}

如此一来没有任何地方需要关心异常,或者业务逻辑校验失败的情况。用户也可以得到很友好的错误提示。
如果你只需要一句概括,那么直接定义一个简单的异常,用于中断处理,并且与用户保持友好交互即可。
如果不可能一句话描述清楚,并且包含附加信息,比如需要在日志或者数据库记录消息ID,此时可能专门针对这种重要/复杂业务创建独立异常。
上述两种情况因为web系统,是用户发起请求之后需要等待程序给予响应结果的。

如果是后台作业,或者复杂业务需要追溯性。这种通常用流程判断语句控制,要用异常处理。我们认为这些流程判断一定在一个原子性处理中。并且检查到(不是遇到)的问题(不是异常)需要记录到用户可友好查看的日志。这种情况属于处理反馈,并不叫异常。

综上,笔者通常分为如下几类:
逻辑异常,这类异常用于描述业务无法按照预期的情况处理下去,属于用户制造的意外。
代码错误,这类异常用于描述开发的代码错误,例如NPE,ILLARG,都属于程序员制造的BUG。
专有异常,多用于特定业务场景,用于描述指定作业出现意外情况无法预先处理。
各类异常必须要有单独的日志记录,或者分级,分类可管理。有的时候仅仅想给三方运维看到逻辑异常。

注意

异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
上面这句话出自<java编程思想>,但是我们思考如下几点:
业务逻辑检查,也是意外情况
UnknownHostException,表示找不到这样的主机,这个异常和NoUserException有什么区别么?换言之,没有这样的主机是异常,没有这样的用户不是异常了么?所以一定要弄明白什么是用异常来控制逻辑,什么是定义程序异常。

异常处理效率很低

书中所示的例子,是在循环中大量使用try-catch进行检查,但是业务系统,用户发起请求的次数与该场景天壤地别。淘宝的11`11是个很好的反例。但是请你的系统上到这个级别再考虑这种问题。
系统有千万并发,不可能还去考虑这些中规中矩的按部就班的方式,别忘了MVC本来就浪费很多资源,代码量增加很多。
业务系统也存在很多巨量任务处理的情况。但是那些任务都是原子性的,现在MVC中的controller和service可不是原子性的,不然为什么要区分这么多层呢。
如果那么在乎效率,考虑下重写Throwable的fillStackTrace方法。你要知道异常的开销大到底大在什么地方,fillStackTrace是一个native方法,会填充异常类内部的运行轨迹。

不要用异常进行业务逻辑处理

我们先来看一个例子:

public void processMessage(Message<String> message){
    try{
        //处理消息验证
        //处理消息解析
        //处理消息入库
    }catch(ValidateException e){
        //验证失败
    }catch(ParseException e){
        //解析失败
    }catch(PersistException e){
        //入库失败
    }

}

上述代码就是典型的使用异常来处理业务逻辑。这种方式需要严重的禁止!上述代码最大的问题在于,我们如何利用异常来自动处理事务呢?
然而这和我们的异常中断service没有什么冲突.也并不是一回事.
我们提倡在 业务处理 的时候,如果发现无法处理直接抛出异常即可。
而并不是在 逻辑处理 的时候,用异常来判断逻辑进行的状况。
改正后的逻辑

public void processMessage(Message<String> message){

    if(!message.isValud){
        MessageLogService.log("消息验证失败"+message.errors());
        return;
    }
    ...
    //TODO ...
}

docker系列

发表于 2019-01-16 | 分类于 docker

CentOS Docker 安装

Docker支持以下的CentOS版本:

  • CentOS 7 (64-bit)
  • CentOS 6.5 (64-bit) 或更高的版本

使用 yum 安装(CentOS 7下)

通过 uname -r 命令查看你当前的内核版本

[root@airthink ~]# uname -r 3.10.0-327.el7.x86_64

只需通过以下命令即可安装 Docker 软件:

yum -y install docker-io

可使用以下命令,查看 Docker 是否安装成功

docker version

Client:
Version:           18.09.1
API version:       1.39
Go version:        go1.10.6
Git commit:        4c52b90
Built:             Wed Jan  9 19:35:01 2019
OS/Arch:           linux/amd64
Experimental:      false

Server: Docker Engine - Community
Engine:
Version:          18.09.1
 API version:      1.39 (minimum version 1.12)
 Go version:       go1.10.6
 Git commit:       4c52b90
Built:            Wed Jan  9 19:06:30 2019
OS/Arch:          linux/amd64
Experimental:     false

若输出了 Docker 的版本号,则说明安装成功了,可通过以下命令启动 Docker 服务:

service docker start

下载镜像

docker search "镜像名",搜索镜像,第一个字段为imagename(镜像名字),第四个字段为official,是否为官方镜像,个人也可以把自己制作的镜像放到docker hub上,找到自己想要下载的镜像名字就可以拉取了
docker pull "镜像名",下载镜像
docker rmi "镜像名",删除镜像
运行一个新的container
docker run [-i -t -d -p -P -c] [--name]:在容器内运行一个应用程序
 -t :在新容器内指定一个伪终端或终端
 -i:允许你对容器内的标准输入进行交互
 -d:以进程方式运行容器,让容器在后台运行
 -p:设置端口
 -P:将容器内部使用的网络端口映射到我们使用的主机,就是让我们访问我们使用的主机就等同于访问到容器内部
 -c:command,后面接命令
 --name container name:指定容器名字

docker ps -a  列出创建的所有容器
docker ps -s  列出正在运行的容器
docker start [container ID]
docker exec -it [container ID] /bin/bash 即可进入一个已经在运行的容器
exit 可退出容器
docker stop [container ID] 停用容器
Docker 1.13版本以后,可以使用 docker containers prune 命令,删除孤立的容器。
docker container prune

docker rmi -f image_ID//删除镜像

Dockerfile部分命令

首先,每个指令的前缀必须大写
指令:FROM
功能描述:设置基础镜像
语法:FROM < image>[:< tag> | @< digest>]
提示:镜像都是从一个基础镜像(操作系统或其他镜像)生成,可以在一个Dockerfile中添加多条FROM指令,一次生成多个镜像
注意:如果忽略tag选项,会使用latest镜像

指令:MAINTAINER
功能描述:设置镜像作者
语法:MAINTAINER < name>

指令:RUN
功能描述:
语法:RUN < command>
RUN [“executable”,”param1”,”param2”]
提示:RUN指令会生成容器,在容器中执行脚本,容器使用当前镜像,脚本指令完成后,Docker Daemon会将该容器提交为一个中间镜像,供后面的指令使用
补充:RUN指令第一种方式为shell方式,使用/bin/sh -c < command>运行脚本,可以在其中使用\将脚本分为多行
RUN指令第二种方式为exec方式,镜像中没有/bin/sh或者要使用其他shell时使用该方式,其不会调用shell命令
例子:RUN source $HOME/.bashrc;\
echo $HOME

RUN [“/bin/bash”,”-c”,”echo hello”]

RUN [“sh”,”-c”,”echo”,”$HOME”] 使用第二种方式调用shell读取环境变量

指令:CMD
功能描述:设置容器的启动命令
语法:CMD [“executable”,”param1”,”param2”]
CMD [“param1”,”param2”]
CMD < command>
提示:CMD第一种、第三种方式和RUN类似,第二种方式为ENTRYPOINT参数方式,为entrypoint提供参数列表
注意:Dockerfile中只能有一条CMD命令,如果写了多条则最后一条生效

指令:LABEL
功能描述:设置镜像的标签
延伸:镜像标签可以通过docker inspect查看
格式:LABEL < key>=< value> < key>=< value> …
提示:不同标签之间通过空格隔开
注意:每条指令都会生成一个镜像层,Docker中镜像最多只能有127层,如果超出Docker Daemon就会报错,如LABEL ..=.. <假装这里有个换行> LABEL ..=..合在一起用空格分隔就可以减少镜像层数量,同样,可以使用连接符\将脚本分为多行
镜像会继承基础镜像中的标签,如果存在同名标签则会覆盖

指令:EXPOSE
功能描述:设置镜像暴露端口,记录容器启动时监听哪些端口
语法:EXPOSE < port> < port> …
延伸:镜像暴露端口可以通过docker inspect查看
提示:容器启动时,Docker Daemon会扫描镜像中暴露的端口,如果加入-P参数,Docker Daemon会把镜像中所有暴露端口导出,并为每个暴露端口分配一个随机的主机端口(暴露端口是容器监听端口,主机端口为外部访问容器的端口)
注意:EXPOSE只设置暴露端口并不导出端口,只有启动容器时使用-P/-p才导出端口,这个时候才能通过外部访问容器提供的服务

指令:ENV
功能描述:设置镜像中的环境变量
语法:ENV < key>=< value>…|< key> < value>
注意:环境变量在整个编译周期都有效,第一种方式可设置多个环境变量,第二种方式只设置一个环境变量
提示:通过${变量名}或者 $变量名使用变量,使用方式${变量名}时可以用${变量名:-default} ${变量名:+cover}设定默认值或者覆盖值
ENV设置的变量值在整个编译过程中总是保持不变的

指令:ADD
功能描述:复制文件到镜像中
语法:ADD < src>… < dest>|[“< src>”,… “< dest>”]
注意:当路径中有空格时,需要使用第二种方式
当src为文件或目录时,Docker Daemon会从编译目录寻找这些文件或目录,而dest为镜像中的绝对路径或者相对于WORKDIR的路径
提示:src为目录时,复制目录中所有内容,包括文件系统的元数据,但不包括目录本身
src为压缩文件,并且压缩方式为gzip,bzip2或xz时,指令会将其解压为目录
如果src为文件,则复制文件和元数据
如果dest不存在,指令会自动创建dest和缺失的上级目录

指令:COPY
功能描述:复制文件到镜像中
语法:COPY < src>… < dest>|[“< src>”,… “< dest>”]
提示:指令逻辑和ADD十分相似,同样Docker Daemon会从编译目录寻找文件或目录,dest为镜像中的绝对路径或者相对于WORKDIR的路径

指令:ENTRYPOINT
功能描述:设置容器的入口程序
语法:ENTRYPOINT [“executable”,”param1”,”param2”]
ENTRYPOINT command param1 param2(shell方式)
提示:入口程序是容器启动时执行的程序,docker run中最后的命令将作为参数传递给入口程序
入口程序有两种格式:exec、shell,其中shell使用/bin/sh -c运行入口程序,此时入口程序不能接收信号量
当Dockerfile有多条ENTRYPOINT时只有最后的ENTRYPOINT指令生效
如果使用脚本作为入口程序,需要保证脚本的最后一个程序能够接收信号量,可以在脚本最后使用exec或gosu启动传入脚本的命令
注意:通过shell方式启动入口程序时,会忽略CMD指令和docker run中的参数
为了保证容器能够接受docker stop发送的信号量,需要通过exec启动程序;如果没有加入exec命令,则在启动容器时容器会出现两个进程,并且使用docker stop命令容器无法正常退出(无法接受SIGTERM信号),超时后docker stop发送SIGKILL,强制停止容器

指令:VOLUME
功能描述:设置容器的挂载点
语法:VOLUME [“/data”]
VOLUME /data1 /data2
提示:启动容器时,Docker Daemon会新建挂载点,并用镜像中的数据初始化挂载点,可以将主机目录或数据卷容器挂载到这些挂载点

指令:USER
功能描述:设置RUN CMD ENTRYPOINT的用户名或UID
语法:USER < name>

指令:WORKDIR
功能描述:设置RUN CMD ENTRYPOINT ADD COPY指令的工作目录
语法:WORKDIR < Path>
提示:如果工作目录不存在,则Docker Daemon会自动创建
Dockerfile中多个地方都可以调用WORKDIR,如果后面跟的是相对位置,则会跟在上条WORKDIR指定路径后(如WORKDIR /A   WORKDIR B   WORKDIR C,最终路径为/A/B/C)

指令:ARG
功能描述:设置编译变量
语法:ARG < name>[=< defaultValue>]
注意:ARG从定义它的地方开始生效而不是调用的地方,在ARG之前调用编译变量总为空,在编译镜像时,可以通过docker build –build-arg < var>=< value>设置变量,如果var没有通过ARG定义则Daemon会报错
可以使用ENV或ARG设置RUN使用的变量,如果同名则ENV定义的值会覆盖ARG定义的值,与ENV不同,ARG的变量值在编译过程中是可变的,会对比使用编译缓存造成影响(ARG值不同则编译过程也不同)
例子:ARG CONT_IMAG_VER <换行> RUN echo $CONT_IMG_VER
ARG CONT_IMAG_VER <换行> RUN echo hello
当编译时给ARG变量赋值hello,则两个Dockerfile可以使用相同的中间镜像,如果不为hello,则不能使用同一个中间镜

示例:Dockerfile以阿里中间件大赛给的debian-jdk8镜像为例,Dockerfile文件如下:

FROM debian:stretch
ARG DEBIAN_FRONTEND=noninteractive
ARG JAVA_VERSION=8
ARG JAVA_UPDATE=172
ARG JAVA_BUILD=11
ARG JAVA_PACKAGE=jdk
ARG JAVA_HASH=a58eab1ec242421181065cdc37240b08

ENV LANG C.UTF-8
ENV JAVA_HOME=/opt/jdk
ENV PATH=${PATH}:${JAVA_HOME}/bin

RUN set -ex \
 && apt-get update \
 && apt-get -y install ca-certificates wget unzip \
 && wget -q --header "Cookie: oraclelicense=accept-securebackup-cookie" \
     -O /tmp/java.tar.gz \
     http://download.oracle.com/otn-pub/java/jdk/${JAVA_VERSION}u${JAVA_UPDATE}-b${JAVA_BUILD}/${JAVA_HASH}/${JAVA_PACKAGE}-${JAVA_VERSION}u${JAVA_UPDATE}-linux-x64.tar.gz \
 && CHECKSUM=$(wget -q -O - https://www.oracle.com/webfolder/s/digest/${JAVA_VERSION}u${JAVA_UPDATE}checksum.html | grep -E "${JAVA_PACKAGE}-${JAVA_VERSION}u${JAVA_UPDATE}-linux-x64\.tar\.gz" | grep -Eo '(sha256: )[^<]+' | cut -d: -f2 | xargs) \
 && echo "${CHECKSUM}  /tmp/java.tar.gz" > /tmp/java.tar.gz.sha256 \
 && sha256sum -c /tmp/java.tar.gz.sha256 \
 && mkdir ${JAVA_HOME} \
 && tar -xzf /tmp/java.tar.gz -C ${JAVA_HOME} --strip-components=1 \
 && wget -q --header "Cookie: oraclelicense=accept-securebackup-cookie;" \
     -O /tmp/jce_policy.zip \
     http://download.oracle.com/otn-pub/java/jce/${JAVA_VERSION}/jce_policy-${JAVA_VERSION}.zip \
 && unzip -jo -d ${JAVA_HOME}/jre/lib/security /tmp/jce_policy.zip \
 && rm -rf ${JAVA_HOME}/jar/lib/security/README.txt \
   /var/lib/apt/lists/* \
   /tmp/* \
   /root/.wget-hsts

体验一下如何用Dockerfile打包一个镜像,新建一个空目录,假设就是~/debian-jdk8吧,cd进这个目录,新建一个Dockerfile,然后把上面的内容copy进去,然后执行下面的命令:
docker build -t debian-jdk8:v1.0 .
其中-t debian-jdk8:v1.0表示打包的镜像名为debian-jdk,tag为v1.0(前面说过,tag是可以任意命名的,不一定要是这种格式),注意命令的最后有一个.,这个表示打包的上下文(其实就是Dockerfile所在目录)是在当前目录,然后目录下的Dockerfile就会被编译执行。
执行完毕后运行docker images就会发现多了一个debian-jdk8镜像。
下面来解释一下Dockerfile的结构,那些字母全部大写的每行第一个单词都是Dockerfile的指令,可以看出这个Dockefile中包括的指令有FROM、ARG、ENV、RUN
FROM:FROM debian:stretch表示以debian:stretch作为基础镜像进行构建
RUN 可以看出RUN后面跟的其实就是一些shell命令,通过&&将这些脚本连接在了一行执行,这么做的原因是为了减少镜像的层数,每多一行RUN都会给镜像增加一层,所以这里选择将所有命令联结在一起执行以减少层数
ARG 特地将这个指令放在RUN之后讲解,这个指令可以进行一些宏定义,比如我定义ENV JAVAHOME=/opt/jdk,之后RUN后面的shell命令中的${JAVAHOME}都会被/opt/jdk代替
ENV 可以看出这个指令的作用是在shell中设置一些环境变量(其实就是export)

Docker部署web程序

为了搭建 Java Web 运行环境,我们需要安装 JDK 、数据库、redis与 Tomcat,首先要再宿主机上传JDK 、数据库、redis与 Tomcat压缩包,以/mnt/software/为例。下面的过程均在容器内部进行。我们不妨选择/opt/目录作为安装目录,首先需要通过cd /opt/命令进入该目录。

首先,解压程序包

tar -zxf /mnt/software/jdk-7u67-linux-x64.tar.gz
tar -zxf /mnt/software/apache-tomcat-7.0.55.tar.gz
tar -zxf /mnt/software/redis
tar -zxf /mnt/software/mongo

设置环境变量

首先,编辑.bashrc文件

vi ~/.bashrc

然后,在该文件末尾添加如下配置:

export JAVA_HOME=/opt/jdk
export PATH=$PATH:$JAVA_HOME

最后,需要使用source命令,让环境变量生效:

source ~/.bashrc

编写运行脚本

vi /root/run.sh

然后,编辑脚本内容如下:

source ~/.bashrc
sh /opt/tomcat/bin/catalina.sh run

注意:这里必须先加载环境变量,然后使用 Tomcat 的运行脚本来启动 Tomcat 服务。

最后,为运行脚本添加执行权限:

chmod u+x /root/run.sh

退出容器

当以上步骤全部完成后,可使用exit命令,退出容器。

随后,可使用如下命令查看正在运行的容器:

docker ps//未输出
docker ps -a//输出

输出如下内容:

CONTAINER ID        IMAGE                             COMMAND             CREATED             STATUS                      PORTS               NAMES
57c312bbaad1        docker.cn/docker/centos:centos6   "/bin/bash"         27 minutes ago      Exited (0) 19 seconds ago                       naughty_goldstine

记住以上CONTAINER ID(容器 ID),随后我们将通过该容器,创建一个可运行 Java Web 的镜像。

创建 Java Web 镜像

使用以下命令,根据某个“容器 ID”来创建一个新的“镜像”:

docker commit 57c312bbaad1 ikangbow/javaweb:0.1

该容器的 ID 是“57c312bbaad1”,所创建的镜像名是“ikangbow/javaweb:0.1”,随后可使用镜像来启动 Java Web 容器。

启动 Java Web 容器

首先使用docker images命令,查看当前所有的镜像:

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ikangbow/javaweb    0.1                 95b5f6b09ca6        6 hours ago         1.51GB
centos              latest              1e1148e4cc2c        5 weeks ago         202MB

可见,此时已经看到了最新创建的镜像“ikangbow/javaweb:0.1”,其镜像 ID 是“95b5f6b09ca6”。正如上面所描述的那样,我们可以通过“镜像名”或“镜像 ID”来启动容器,与上次启动容器不同的是,我们现在不再进入容器的命令行,而是直接启动容器内部的 Tomcat 服务。此时,需要使用以下命令:

docker run -d -p 58080:8080 --name javaweb ikangbow/javaweb:0.1 /root/run.sh

稍作解释:

  • -d:表示以“守护模式”执行/root/run.sh脚本,此时 Tomcat 控制台不会出现在输出终端上。
  • -p:表示宿主机与容器的端口映射,此时将容器内部的 8080 端口映射为宿主机的 58080 端口,这样就向外界暴露了 58080 端口,可通过 Docker 网桥来访问容器内部的 8080 端口了。
  • –name:表示容器名称,用一个有意义的名称命名即可。

关于 Docker 网桥的内容,需要补充说明一下。实际上 Docker 在宿主机与容器之间,搭建了一座网络通信的桥梁,我们可通过宿主机 IP 地址与端口号来映射容器内部的 IP 地址与端口号,

在一系列参数后面的是“镜像名”或“镜像 ID”,怎么方便就怎么来。最后是“初始命令”,它是上面编写的运行脚本,里面封装了加载环境变量并启动 Tomcat 服务的命令。

当运行以上命令后,会立即输出一长串“容器 ID”,我们可通过docker ps命令来查看当前正在运行的容器。

CONTAINER ID        IMAGE                   COMMAND             CREATED             STATUS              PORTS                     NAMES
82f47923f926        ikangbow/javaweb:0.1   "/root/run.sh"      4 seconds ago       Up 3 seconds        0.0.0.0:58080->8080/tcp   javaweb

此处实测有问题,需要继续研究。

附常用命令:

  1. 删除容器实例

寻找已经停止运行的container

docker ps -a  
docker rm 容器id 删除实例
docker ps -a 查看实例已经删除
docker rm $(docker ps -a -q)  删除所有container

2.删除镜像

停止所有的container,这样才能够删除其中的images:

docker stop $(docker ps -a -q)
docker rmi <image id>
docker images 查看所有镜像
docker rmi 镜像id 删除镜像
docker images 查看镜像 发现已经删除 

实际应用

docker stop application//停止应用
docker rm application//删除镜像
docker run -i -t -d -p 88:8080 -v /badou/badou/badou1.war:/usr/local/tomcat/webapps/badou.war -v /badou/conf/apiclient_cert.p12:/usr/local/tomcat/apiclient_cert.p12 -v /badou/badou/index.html:/usr/local/tomcat/webapps/ROOT/index.html --name=application andylaun/tomcat:v5

-v  挂载文件目录

docker运行nginx

which nginx

查看nginx安装在哪个目录

docker网络

docker具有隔离性,网络也是个隔离性的一部分,linux使用了命名空间来进行资源的隔离,比如pid namespace就是用来隔离进程的,mount namespace是用来隔离文件系统的,network namespace 是用来隔离网络的.每一个network namespace都提供了一个独立的网络环境,包括网卡路由iptable规则等等,都是与以其它的network space隔离的

docker容器在默认情况下,一般会分配一个独立的network-namespace,也就是网络类型中的Bridge模式,在使用Bridge时就涉及到了一个问题,既然它有独立的namesapce,这就需要一种技术使容器内的端口可以在主机上访问到,这种技术就是端口映射,docker可以指定你想把容器内的某一个端口可以在容器所在主机上的某一个端口它俩之间做一个映射,当你在访问主机上的端口时,其实就是访问容器里面的端口。

docker部署web

  1. 在宿主机系统下载所需要的的jdk版本gz文件

    docker cp gz文件 容器ID:/root

  2. 复制gz文件到容器中的root的用户目录下

    docker cp jdk-8u121-linux-x64.tar.gz ea49f55dde3d :/root

  3. 进入运行中的容器

    docker exec -it ea49f55dde3d /bin/bash

  4. 在容器中解压gz文件

    tar -zxvf jdk-8u121-linux-x64.tar.gz

  5. 建立容器系统的JAVA_HOME目录

    mkdir /usr/lib/jvm

  6. 移动jdk目录到JAVA_HOME目录

    mv ~/jdk1.8.0_121 /usr/lib/jvm

  7. 安装vim(可选)

    apt-get update
    apt-get install vim

  8. 设置环境变量

    vim ~/.bashrc
    export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_121
    export JRE_HOME=${JAVA_HOME}/jre
    export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
    export PATH=${JAVA_HOME}/bin:$PATH

  9. 使得环境变量马上生效

    source ~/.bashrc

  10. 验证JDK是否安装成功

    java -version

安装tomcat

  1. 查找tomcat信息

    docker search tomcat

  2. 下载官方的镜像Starts最高的那个

    docker pull docker.io/tomcat

  3. 查看docker镜像

    docker images

  4. 后台运行tomcat镜像

    docker run -d –name tomcat -p 8081:8080 hub.c.163.com/library/tomcat

  5. 若端口被占用,可以指定容器和主机的映射端口,前者是外围访问端口,后者是容器内部端口。
    -d参数:容器会在后台运行并不会把输出的结果打印到宿主机上面。使用 -d 参数启动后会返回一个唯一的 id。

部署web项目

  1. 把宿主机的war包丢到docker容器tomcat/webapps下:

    docker cp lsz.war ab6bce2c5826:/usr/local/tomcat/webapps

  2. 进入docker容器中

    docker exec -it ab6bce2c5826 /bin/bash

  3. 查看webapps中的web项目

  4. 退出docker容器Ctrl+p+q
  5. 重新运行tomcat容器docker restart ab6bce2c5826
  6. 停止tomcat容器docker stop ab6bce2c5826

http基础

发表于 2019-01-07 | 分类于 https

HTTP 简介

HTTP协议(HyperText Transfer Protocol,超文本传输协议)是因特网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。

HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。

工作原理

HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

Web服务器有:Apache服务器,IIS服务器(Internet Information Services)等。

Web服务器根据接收到的请求后,向客户端发送响应信息。

HTTP默认端口号为80,但是你也可以改为8080或者其他端口。

HTTP三点注意事项:

1.HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

2.HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。

3.HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

HTTP 消息结构

HTTP是基于客户端/服务端(C/S)的架构模型,通过一个可靠的链接来交换信息,是一个无状态的请求/响应协议。

一个HTTP”客户端”是一个应用程序(Web浏览器或其他任何客户端),通过连接到服务器达到向服务器发送一个或多个HTTP的请求的目的。

一个HTTP”服务器”同样也是一个应用程序(通常是一个Web服务,如Apache Web服务器或IIS服务器等),通过接收客户端的请求并向客户端发送HTTP响应数据。

HTTP使用统一资源标识符(Uniform Resource Identifiers, URI)来传输数据和建立连接。

一旦建立连接后,数据消息就通过类似Internet邮件所使用的格式[RFC5322]和多用途Internet邮件扩展(MIME)[RFC2045]来传送。

实例

下面实例是一点典型的使用GET来传递数据的实例:

客户端请求:

GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

服务端响应:

HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: “34aa387-d-1568eb00”
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

HTTP请求方法

根据HTTP标准,HTTP请求可以使用多种请求方法。

HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。

HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

  1. GET: 请求指定的页面信息,并返回实体主体。
  2. HEAD: 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
  3. POST: 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
  4. PUT: 从客户端向服务器传送的数据取代指定的文档的内容。
  5. DELETE: 请求服务器删除指定的页面。
  6. CONNECT: HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
  7. OPTIONS: 允许客户端查看服务器的性能。
  8. TRACE: 回显服务器收到的请求,主要用于测试或诊断。

HTTP 响应头信息

  1. Allow: 服务器支持哪些请求方法(如GET、POST等)。
  2. Content-Encoding: 文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压缩文档能够显著地减少HTML文档 的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩,但只有Unix上的Netscape和Windows上的IE 4、IE 5才支持它。因此,Servlet应该通过查看Accept-Encoding头(即request.getHeader(“Accept- Encoding”))检查浏览器是否支持gzip,为支持gzip的浏览器返回经gzip压缩的HTML页面,为其他浏览器返回普通页面。
  3. Content-Length: 表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。如果你想要利用持久连接的优势,可以把输出文档写入 ByteArrayOutputStream,完成后查看其大小,然后把该值放入Content-Length头,最后通过 byteArrayStream.writeTo(response.getOutputStream()发送内容。
  4. Content-Type: 表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置 Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。
  5. Date: 当前的GMT时间。你可以用setDateHeader来设置这个头以避免转换时间格式的麻烦。
  6. Expires: 应该在什么时候认为文档已经过期,从而不再缓存它?
  7. Last-Modified: 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件GET,只有改动时间迟于指定时间的文 档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。
  8. Location: 表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。
  9. Refresh: 表示浏览器应该在多少时间之后刷新文档,以秒计。除了刷新当前文档之外,你还可以通过setHeader(“Refresh”, “5; URL=http://host/path")让浏览器读取指定的页面。 注意这种功能通常是通过设置HTML页面HEAD区的<METAHTTP-EQUIV=”Refresh” CONTENT=”5;URL=http://host/path">实现,这是因为,自动刷新或重定向对于那些不能使用CGI或Servlet的HTML编写者十分重要。但是,对于Servlet来说,直接设置Refresh头更加方便。注意Refresh的意义是"N秒之后刷新本页面或访问指定页面",而不是"每隔N秒刷新本页面或访问指定页面"。因此,连续刷新要求每次都发送一 个Refresh头,而发送204状态代码则可以阻止浏览器继续刷新,不管是使用Refresh头还是<META HTTP-EQUIV=”Refresh” …>。
    Refresh头不属于HTTP 1.1正式规范的一部分,而是一个扩展,但Netscape和IE都支持它
  10. Server: 服务器名字。Servlet一般不设置这个值,而是由Web服务器自己设置。
  11. Set-Cookie: 设置和页面关联的Cookie。Servlet不应使用response.setHeader(“Set-Cookie”, …),而是应使用HttpServletResponse提供的专用方法addCookie。参见下文有关Cookie设置的讨论。
  12. WWW-Authenticate: 客户应该在Authorization头中提供什么类型的授权信息?在包含401(Unauthorized)状态行的应答中这个头是必需的。例 如,response.setHeader(“WWW-Authenticate”, “BASIC realm=\”executives\””)。 注意Servlet一般不进行这方面的处理,而是让Web服务器的专门机制来控制受密码保护页面的访问(例如.htaccess)。

HTTP状态码

当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含HTTP状态码的信息头(server header)用以响应浏览器的请求。

HTTP状态码的英文为HTTP Status Code。

HTTP状态码分类

  1. 200 OK 请求成功。一般用于GET与POST请求
  2. 401 Unauthorized 请求要求用户的身份认证
  3. 500 Internal Server Error 服务器内部错误,无法完成请求
  4. 404 Not Found 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置”您所请求的资源无法找到”的个性页面
  5. 502 Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应

HTTP content-type

Content-Type,内容类型,一般是指网页中存在的Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件

final和static的作用

发表于 2019-01-06 | 分类于 java

final关键字的作用

  1. 被final修饰的类不可以被继承
  2. 被final修饰的方法不可以被重写
  3. 被final修饰的变量不可被改变

重点

  1. 被fianl修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的。
  2. 被fina修饰的常量,在编译阶段会存入调用类的常量池中。

static关键字的作用

  1. 被static修饰的变量属于类变量,可以通过 类名.变量名 直接引用,而不需要new出一个类。
  2. 被static修饰的方法属于类方法,可以通过 类名.变量名 直接引用,而不需要new出一个类。

被static修饰的变量和方法属于类的静态资源,是类实例之间共享的。JDK把不同的静态资源放在了不同的类中而不把所有的静态资源放在一个类里面是因为:

  1. 不同的类有自己的静态资源,这可以实现静态资源分类。比如和数学相关的静态资源放在java.lang.Math中,和日历相关的静态资源放在java.util.Calendar中,这样比较清晰
  2. 避免重名。不同的类之间有重名的静态变量名,静态方法名也是很正常。
  3. 避免静态资源类无限膨胀。

问题
静态方法能不能引用非静态资源?静态方法里面能不能引用静态资源?非静态方法里面能不能引用静态资源?比如就以这段代码为例,是否有错?

public class A{
     private int i = 1;
    public static void main(String[] args){
    i = 1;//错误
    }
}

静态资源属于类,但是是独立于类存在的。从JVM类的加载机制的角度讲,静态资源是类初始化的时候加载的,而非静态资源是类new的时候加载的。类的初始化早于类的new,比如Class.forName(“xxx”),就是初始化了一个类,但是并没有new它,只是加载这个类的静态资源。所以对于静态资源来说,它是不可能知道一个类中有哪些非静态资源的;但是对于非静态资源来说就不一样了,它是new出来之后产生的,因此属于类的它都认识。所以结论是:

  1. 静态方法不能引用非静态资源。非静态资源在new对象的时候才会产生,晚于一初始化就存在的静态资源。
  2. 静态方法里面可以引用静态资源。都是类初始化的时候加载的。
  3. 非静态方法可以引用静态资源。非静态方法是new之后产生的,静态资源是类一初始化就存在的。

静态块

静态块是static的重要应用之一。主要用于初始化一个类的时候做操作用的,和静态变量,静态方法一样,静态块里面的代码只执行一次,且只在类初始化的时候执行。

public class A{
    private static int a = B();
    static{
        System.out.println("Enter A.static block");
    }
    public static void main(String[] args){
        new A();
    }

    public static int B(){
        System.out.println("Enter A.B()");
        return 1;
    }
}

打印结果:

Enter A.B()
Enter A.static block

结论:

静态资源的加载顺序是严格安装静态资源的定义顺序加载的。

public class A{
    static
    {
        c = 3;
        System.out.println(c);//Cannot reference a field before it is defined
    }

     private static int c;
}

结论:

静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问。

public class A{
    static
    {
        System.out.println("A.static block");
    }

    public A()
    {
        System.out.println("A.constructor()");
    }
}

public class B extends A{
    static 
    {
        System.out.println("B.static block");
    }

    public B()
    {
        System.out.println("B.constructor()");
    }

    public static void main(String[] args)
    {
        new B();
        new B();
    }
}

打印结果:

A.static block
B.static block
A.constructor()
B.constructor()
A.constructor()
B.constructor()

结论:

静态代码块是严格按照父类静态代码块》子类静态代码块的顺序加载的,且只加载一次。

项目中用到的

项目中的角色类型通常用static final修饰

public static final String TYPE_A = "a";
public static final String TYPE_B = "b";

public:使接口的实现类可以使用这个常量

static:static修饰的表示是属于类的,随着类的加载而存在。如果是非static的,就表示属于对象的,只有建立对象时才有它,而接口是不能建立对象的,所以接口的常量必须定义为satic

final:fina修饰保证接口定义的常量不能被实现类去修改,如果没有final的话,任由子类随意去修改的话,接口建立这个常量就没有意义了。

优雅的写代码

发表于 2018-12-19 | 分类于 code

代码优化的目标

1.减小代码的体积。

2.提高代码运行效率

代码规范

  1. 所有文件的开头都要有Java文档的注释(/* /)
  2. 常量应该全部大写,单词之间由下划线分隔(例如,MAX_WORK_HOURS)
  3. 数组标识:要用”int[] packets”,而不是”int packets[]”,后一种永远也不要用
  4. 不要在循环中构造和释放对象
  5. 不要在循环中频繁查询数据库
  6. 方法要通盘考虑,尽量做到复用。(多想)

代码优化细节

尽量重用对象

特别是String对象的使用,出现字符串连接时应该使用StringBuilder/StringBuffer代替。由于Java虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响。

string类是final类,不能被继承,并且它的成员方法都默认为final方法。在java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。在早期的JVM实现版本中,被final修饰的方法会被转为内嵌调用以提升执行效率。从Java SE5/6开始,就渐渐摒弃这种方式了。因此在现在版本中并不需要考虑用final去提升方法调用效率。只有在确定不想让该方法被覆盖时,才将方法设置为final.

String类其实是通过char数组来保存字符串的。所以无论是substring、concat、replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。所以结论是:对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象。

通过new关键字来生成对象是在堆区进行的,所以通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。

在class文件中有一部分来存储编译期间生成的字面常量以及符合的引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。下面代码 str1和str3都存储在常量池中,只存了一份。

public class Main {
    public static void main(String[] args) {
        String str1 = "hello world";
        String str2 = new String("hello world");
        String str3 = "hello world";
        String str4 = new String("hello world");

        System.out.println(str1==str2);
        System.out.println(str1==str3);
        System.out.println(str2==str4);
    }
}

输出结果为false true false

既然已经有了String类,那为什么还需要StringBuilder、StringBuffer类呢

public class Main {
    public static void main(String[] args) {
        String string = "";
        for(int i=0;i<10000;i++){
            string += "hello";
        }
    }
}

这句 string += “hello”;的过程相当于将原有的string变量指向的对象内容取出与”hello”作字符串相加操作再存进另一个新的String对象当中,再让string变量指向新生成的对象。结论:使用String进行字符串拼接会造成内存资源浪费。

public class Main {
    public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder();
        for(int i=0;i<10000;i++){
            stringBuilder.append("hello");
        }
    }
}

使用StringBuilder new操作只进行了一次,也就是说只生成了一个对象,append操作是在原有对象的基础上进行的。因此在循环了10000次之后,这段代码所占的资源要比上面小得多。

事实上,StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer类的成员方法前面多了一个关键字:synchronized,不用多说,这个关键字是在多线程访问时起到安全保护作用的,也就是说StringBuffer是线程安全的。

StringBuilder的insert方法:

public StringBuilder insert(int index, char str[], int offset, int len){
  super.insert(index, str, offset, len);
  return this;
}

StringBuffer的insert方法:

public synchronized StringBuffer insert(int index, char str[], int offset, int len) {

    super.insert(index, str, offset, len);
    return this;
}

结论:

1.对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如”I”+”love”+”java”; 的字符串相加,在编译期间便被优化成了”Ilovejava”。这个可以用javap -c命令反编译生成的class文件进行验证。

对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。

2.String、StringBuilder、StringBuffer三者的执行效率:

StringBuilder > StringBuffer > String

当然这个是相对的,不一定在所有情况下都是这样。

比如String str = “hello”+ “world”的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高。

因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:

当字符串相加操作或者改动较少的情况下,建议使用 String str=”hello”这种形式;

当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。

尽可能使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。

及时关闭流

Java编程过程中,进行数据库连接、I/O流操作时务必小心,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。尽量避免在for循环中进行数据库的操作。

尽量减少对变量的重复计算

明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:

for (int i = 0; i < list.size(); i++)
{...}

替换为

for (int i = 0, length = list.size(); i < length; i++)
{...}

这样,在list.size()很大的时候,就减少了很多的消耗

尽量采用懒加载的策略,即在需要的时候才创建

例如:

String str = "aaa";
if (i == 1) {
   list.add(str);
}

建议替换为:

if (i == 1) {
 String str = "aaa";
 list.add(str);
}

慎用异常

异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。

如果能估计到待添加的内容长度,为底层以数组方式实现的集合、工具类指定初始长度

比如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等,以StringBuilder为例:

StringBuilder()     // 默认分配16个字符的空间

StringBuilder(int size)  // 默认分配size个字符的空间

StringBuilder(String str) // 默认分配16个字符+str.length()个字符空间

可以通过类(这里指的不仅仅是上面的StringBuilder)的构造函数来设定它的初始化容量,这样可以明显地提升性能。比如StringBuilder吧,length表示当前的StringBuilder能保持的字符数量。因为当StringBuilder达到最大容量的时候,它会将自身容量增加到当前的2倍再加2,无论何时只要StringBuilder达到它的最大容量,它就不得不创建一个新的字符数组然后将旧的字符数组内容拷贝到新字符数组中—-这是十分耗费性能的一个操作。试想,如果能预估到字符数组中大概要存放5000个字符而不指定长度,最接近5000的2次幂是4096,每次扩容加的2不管,那么:

在4096 的基础上,再申请8194个大小的字符数组,加起来相当于一次申请了12290个大小的字符数组,如果一开始能指定5000个大小的字符数组,就节省了一倍以上的空间

把原来的4096个字符拷贝到新的的字符数组中去。

这样,既浪费内存空间又降低代码运行效率。所以,给底层以数组实现的集合、工具类设置一个合理的初始化容量是错不了的,这会带来立竿见影的效果。但是,注意,像HashMap这种是以数组+链表实现的集合,别把初始大小和你估计的大小设置得一样,因为一个table上只连接一个对象的可能性几乎为0。初始大小建议设置为2的N次幂,如果能估计到有2000个元素,设置成new HashMap(128)、new HashMap(256)都可以。

循环内不要不断创建对象引用

例如:

for (int i = 1; i <= count; i++){
    Object obj = new Object();    
}

这种做法会导致内存中有count份Object对象引用存在,count很大的话,就耗费内存了,建议为改为:

Object obj = null;
for (int i = 0; i <= count; i++) {
    obj = new Object();
}

基于效率和类型检查的考虑,应该尽可能使用array,无法确定数组大小时才使用ArrayList,尽量使用HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销。

实现RandomAccess接口的集合比如ArrayList,应当使用最普通的for循环而不是foreach循环来遍历

if (list instanceof RandomAccess){
    for (int i = 0; i < list.size(); i++){
    ...
    }
} else {
    Iterator<?> iterator = list.iterable();
        while (iterator.hasNext()){
        iterator.next()
    }
}

这是JDK推荐给用户的。JDK API对于RandomAccess接口的解释是:实现RandomAccess接口用来表明其支持快速随机访问,此接口的主要目的是允许一般的算法更改其行为,从而将其应用到随机或连续访问列表时能提供良好的性能。实际经验表明,实现RandomAccess接口的类实例,假如是随机访问的,使用普通for循环效率将高于使用foreach循环;反过来,如果是顺序访问的,则使用Iterator会效率更高。

使用带缓冲的输入输出流进行IO操作

带缓冲的输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,这可以极大地提升IO效率

字符串变量和字符串常量equals的时候将字符串常量写在前面

把一个基本数据类型转为字符串,基本数据类型.toString()是最快的方式、String.valueOf(数据)次之、数据+””最慢

public static void main(String[] args){
    int loopTime = 50000;
    Integer i = 0;
    long startTime = System.currentTimeMillis();
    for (int j = 0; j < loopTime; j++){
        String str = String.valueOf(i);
    }    
    System.out.println("String.valueOf():" + (System.currentTimeMillis() - startTime) + "ms");
    startTime = System.currentTimeMillis();
    for (int j = 0; j < loopTime; j++){
        String str = i.toString();
    }    
    System.out.println("Integer.toString():" + (System.currentTimeMillis() - startTime) + "ms");
    startTime = System.currentTimeMillis();
    for (int j = 0; j < loopTime; j++){
        String str = i + "";
    }    
        System.out.println("i + \"\":" + (System.currentTimeMillis() - startTime) + "ms");
}

运行结果为:

String.valueOf():11ms
Integer.toString():5ms
i + "":25ms

三者对比下来,明显是2最快、1次之、3最慢。结论:

以后遇到把一个基本数据类型转为String的时候,优先考虑使用toString()方法。至于为什么,很简单:

String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断

Integer.toString()方法就不说了,直接调用了

i + “”底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串

使用最有效率的方式去遍历Map

public static void main(String[] args){

    HashMap<String, String> hm = new HashMap<String, String>();
    hm.put("111", "222");

    Set<Map.Entry<String, String>> entrySet = hm.entrySet();
    Iterator<Map.Entry<String, String>> iter = entrySet.iterator();
    while (iter.hasNext())
    {
        Map.Entry<String, String> entry = iter.next();
        System.out.println(entry.getKey() + "\t" + entry.getValue());
    }
}

如果你只是想遍历一下这个Map的key值,那用”Set keySet = hm.keySet();”会比较合适一些

Redis相关及实践

发表于 2018-12-15 | 分类于 redis

定义

redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis 是一个高性能的key-value数据库。 redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,使用很方便。
Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。
redis的官网地址,非常好记,是redis.io。(特意查了一下,域名后缀io属于国家域名,是british Indian Ocean territory,即英属印度洋领地)
目前,Vmware在资助着redis项目的开发和维护。详见百度百科


RedisTemplate常用集合-opsForValue

1.set(K key, V value)

新增一个字符串类型的值,key是键,value是值。

redisTemplate.opsForValue().set("stringValue","bbb"); 

2.get(Object key)

获取key键对应的值

String stringValue = redisTemplate.opsForValue().get("stringValue")+"";  
System.out.println("通过get(Object key)方法获取set(K key, V value)方法新增的字符串值:" + stringValue); 

3.append(K key, String value)

在原有的值基础上新增字符串到末尾。

redisTemplate.opsForValue().append("stringValue","aaa"); 
String stringValueAppend = redisTemplate.opsForValue().get("stringValue")+""; `
System.out.println("通过append(K key, String value)方法修改后的字符串:"+stringValueAppend);  

4.get(K key, long start, long end)

截取key键对应值得字符串,从开始下标位置开始到结束下标的位置(包含结束下标)的字符串。

String cutString = redisTemplate.opsForValue().get("stringValue",0,3);
System.out.println("通过get(K key, long start, long end)方法获取截取的字符串:"+cutString);

5.getAndSet(K key, V value)

获取原来key键对应的值并重新赋新值。

String oldAndNewStringValue = redisTemplate.opsForValue().getAndSet("stringValue","ccc")+"";
System.out.print("通过getAndSet(K key, V value)方法获取原来的" + oldAndNewStringValue + ",");  
String newStringValue = redisTemplate.opsForValue().get("stringValue")+"";
System.out.println("修改过后的值:"+newStringValue); 

6.setBit(K key, long offset, boolean value)

key键对应的值value对应的ascii码,在offset的位置(从左向右数)变为value。

redisTemplate.opsForValue().setBit("stringValue",1,false);
newStringValue = redisTemplate.opsForValue().get("stringValue")+"";
System.out.println("通过setBit(K key,long offset,boolean value)方法修改过后的值:"+newStringValue); 

7.getBit(K key, long offset)

判断指定的位置ASCII码的bit位是否为1。

 boolean bitBoolean = redisTemplate.opsForValue().getBit("stringValue",1);
 boolean bitBoolean = redisTemplate.opsForValue().getBit("stringValue",1);

8.size(K key)

获取指定字符串的长度。

Long stringValueLength = redisTemplate.opsForValue().size("stringValue");
Long stringValueLength = redisTemplate.opsForValue().size("stringValue");

9.increment(K key, double delta)

 以增量的方式将double值存储在变量中。

double stringValueDouble = redisTemplate.opsForValue().increment("doubleValue",5);
System.out.println("通过increment(K key, double delta)方法以增量方式存储double值:" + stringValueDouble);

10.increment(K key, long delta)

以增量的方式将long值存储在变量中。

double stringValueLong = redisTemplate.opsForValue().increment("longValue",6); System.out.println("通过increment(K key, long delta)方法以增量方式存储long值:" + stringValueLong);

11.setIfAbsent(K key, V value)

如果键不存在则新增,存在则不改变已经有的值。

boolean absentBoolean = redisTemplate.opsForValue().setIfAbsent("absentValue","fff");  
System.out.println("通过setIfAbsent(K key, V value)方法判断变量值absentValue是否存在:" + absentBoolean);  
if(absentBoolean){  
String absentValue = redisTemplate.opsForValue().get("absentValue")+"";  
System.out.print(",不存在,则新增后的值是:"+absentValue);  
boolean existBoolean = redisTemplate.opsForValue().setIfAbsent("absentValue","eee");  
System.out.print(",再次调用setIfAbsent(K key, V value)判断absentValue是否存在并重新赋值:" + existBoolean);  
    if(!existBoolean){  
        absentValue = redisTemplate.opsForValue().get("absentValue")+"";  
        System.out.print("如果存在,则重新赋值后的absentValue变量的值是:" + absentValue);  
    }  
}

12.set(K key, V value, long timeout, TimeUnit unit)

设置变量值的过期时间。

redisTemplate.opsForValue().set("timeOutValue","timeOut",5,TimeUnit.SECONDS);  
String timeOutValue = redisTemplate.opsForValue().get("timeOutValue")+"";  
System.out.println("通过set(K key, V value, long timeout, TimeUnit unit)方法设置过期时间,过期之前获取的数据:"+timeOutValue);  
Thread.sleep(5*1000);  
timeOutValue = redisTemplate.opsForValue().get("timeOutValue")+"";  
System.out.print(",等待10s过后,获取的值:"+timeOutValue);  

13.set(K key, V value, long offset)

 覆盖从指定位置开始的值。

redisTemplate.opsForValue().set("absentValue","dd",1);  
String overrideString = redisTemplate.opsForValue().get("absentValue")+"";  
System.out.println("通过set(K key, V value, long offset)方法覆盖部分的值:"+overrideString);

14.multiSet(Map<? extends K,? extends V> map)

设置map集合到redis。

Map valueMap = new HashMap();  
valueMap.put("valueMap1","map1");  
valueMap.put("valueMap2","map2");  
valueMap.put("valueMap3","map3");  
redisTemplate.opsForValue().multiSet(valueMap);  

15.multiGet(Collection keys)

 根据集合取出对应的value值。

//根据List集合取出对应的value值  
List paraList = new ArrayList();  
paraList.add("valueMap1");  
paraList.add("valueMap2");  
paraList.add("valueMap3");  
List<String> valueList = redisTemplate.opsForValue().multiGet(paraList);  
for (String value : valueList){  
    System.out.println("通过multiGet(Collection<K> keys)方法获取map值:" + value);  
} 

16.multiSetIfAbsent(Map<? extends K,? extends V> map)

如果对应的map集合名称不存在,则添加,如果存在则不做修改。

Map valueMap = new HashMap();  
valueMap.put("valueMap1","map1");  
valueMap.put("valueMap2","map2");  
valueMap.put("valueMap3","map3");  
redisTemplate.opsForValue().multiSetIfAbsent(valueMap);  

RedisTemplate常用集合-opsForList

1.leftPush(K key, V value)

在变量左边添加元素值。

redisTemplate.opsForList().leftPush("list","a");
redisTemplate.opsForList().leftPush("list","b");
redisTemplate.opsForList().leftPush("list","c");

2.index(K key, long index)

获取集合指定位置的值。

String listValue = redisTemplate.opsForList().index("list",1) + "";
System.out.println("通过index(K key, long index)方法获取指定位置的值:" + listValue);

3.range(K key, long start, long end)

获取指定区间的值。

List<Object> list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println("通过range(K key, long start, long end)方法获取指定范围的集合值:"+list);

4.leftPush(K key, V pivot, V value)

把最后一个参数值放到指定集合的第一个出现中间参数的前面,如果中间参数值存在的话。

redisTemplate.opsForList().leftPush("list","a","n");
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println("通过leftPush(K key, V pivot, V value)方法把值放到指定参数值前面:" + list);

5.leftPushAll(K key, V… values)

向左边批量添加参数元素。

redisTemplate.opsForList().leftPushAll("list","w","x","y");
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println("通过leftPushAll(K key, V... values)方法批量添加元素:" + list);

6.leftPushAll(K key, Collection values)

以集合的方式向左边批量添加元素。

List newList = new ArrayList();
newList.add("o");
newList.add("p");
newList.add("q");
redisTemplate.opsForList().leftPushAll("list",newList);
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println("通过leftPushAll(K key, Collection<V> values)方法以集合的方式批量添加元素:" + list);

7.leftPushIfPresent(K key, V value)

 如果存在集合则添加元素。

redisTemplate.opsForList().leftPushIfPresent("presentList","o");
list =  redisTemplate.opsForList().range("presentList",0,-1);
System.out.println("通过leftPushIfPresent(K key, V value)方法向已存在的集合添加元素:" + list);

8.rightPush(K key, V value)

向集合最右边添加元素。

redisTemplate.opsForList().rightPush("list","w");
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println("通过rightPush(K key, V value)方法向最右边添加元素:" + list);

9.rightPush(K key, V pivot, V value)

向集合中第一次出现第二个参数变量元素的右边添加第三个参数变量的元素值。

redisTemplate.opsForList().rightPush("list","w","r");
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println("通过rightPush(K key, V pivot, V value)方法向最右边添加元素:" + list);

10.rightPushAll(K key, V… values)

向右边批量添加元素。

redisTemplate.opsForList().rightPushAll("list","j","k");
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println("通过rightPushAll(K key, V... values)方法向最右边批量添加元素:" + list);

11.rightPushAll(K key, Collection values)

以集合方式向右边添加元素。

newList.clear();
newList.add("g");
newList.add("h");
redisTemplate.opsForList().rightPushAll("list",newList);
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println("通过rightPushAll(K key, Collection<V> values)方法向最右边以集合方式批量添加元素:" + list);

12.rightPushIfPresent(K key, V value)

向已存在的集合中添加元素。

redisTemplate.opsForList().rightPushIfPresent("presentList","d");
list =  redisTemplate.opsForList().range("presentList",0,-1);
System.out.println("通过rightPushIfPresent(K key, V value)方法已存在的集合向最右边添加元素:" + list);

13.size(K key)

获取集合长度。

long listLength = redisTemplate.opsForList().size("list");
System.out.println("通过size(K key)方法获取集合list的长度为:" + listLength);

14.leftPop(K key)

移除集合中的左边第一个元素。

Object popValue = redisTemplate.opsForList().leftPop("list");
System.out.print("通过leftPop(K key)方法移除的元素是:" + popValue);
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println(",剩余的元素是:" + list);

15.leftPop(K key, long timeout, TimeUnit unit)

移除集合中左边的元素在等待的时间里,如果超过等待的时间仍没有元素则退出。

popValue = redisTemplate.opsForList().leftPop("presentList",1, TimeUnit.SECONDS);
System.out.print("通过leftPop(K key, long timeout, TimeUnit unit)方法移除的元素是:" + popValue);
list =  redisTemplate.opsForList().range("presentList",0,-1);
System.out.println(",剩余的元素是:" + list);

16.rightPop(K key)

移除集合中右边的元素。
popValue = redisTemplate.opsForList().rightPop("list");
System.out.print("通过rightPop(K key)方法移除的元素是:" + popValue);
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println(",剩余的元素是:" + list);

17.rightPop(K key, long timeout, TimeUnit unit)

移除集合中右边的元素在等待的时间里,如果超过等待的时间仍没有元素则退出。

popValue = redisTemplate.opsForList().rightPop("presentList",1, TimeUnit.SECONDS);
System.out.print("通过rightPop(K key, long timeout, TimeUnit unit)方法移除的元素是:" + popValue);
list =  redisTemplate.opsForList().range("presentList",0,-1);
System.out.println(",剩余的元素是:" + list);

18.rightPopAndLeftPush(K sourceKey, K destinationKey)

移除集合中右边的元素,同时在左边加入一个元素。

popValue = redisTemplate.opsForList().rightPopAndLeftPush("list","12");
System.out.print("通过rightPopAndLeftPush(K sourceKey, K destinationKey)方法移除的元素是:" + popValue);
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println(",剩余的元素是:" + list);

19.rightPopAndLeftPush(K sourceKey, K destinationKey, long timeout, TimeUnit unit)

 移除集合中右边的元素在等待的时间里,同时在左边添加元素,如果超过等待的时间仍没有元素则退出。

popValue = redisTemplate.opsForList().rightPopAndLeftPush("presentList","13",1,TimeUnit.SECONDS);
System.out.println("通过rightPopAndLeftPush(K sourceKey, K destinationKey, long timeout, TimeUnit unit)方法移除的元素是:" + popValue);
list =  redisTemplate.opsForList().range("presentList",0,-1);
System.out.print(",剩余的元素是:" + list);

20.set(K key, long index, V value)

在集合的指定位置插入元素,如果指定位置已有元素,则覆盖,没有则新增,超过集合下标+n则会报错。

redisTemplate.opsForList().set("presentList",3,"15");
list =  redisTemplate.opsForList().range("presentList",0,-1);
System.out.print("通过set(K key, long index, V value)方法在指定位置添加元素后:" + list);

21.remove(K key, long count, Object value)

从存储在键中的列表中删除等于值的元素的第一个计数事件。count> 0:删除等于从左到右移动的值的第一个元素;count< 0:删除等于从右到左移动的值的第一个元素;count = 0:删除等于value的所有元素。

long removeCount = redisTemplate.opsForList().remove("list",0,"w");
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println("通过remove(K key, long count, Object value)方法移除元素数量:" + removeCount);
System.out.println(",剩余的元素:" + list);

22.trim(K key, long start, long end)

截取集合元素长度,保留长度内的数据。

redisTemplate.opsForList().trim("list",0,5);
list =  redisTemplate.opsForList().range("list",0,-1);
System.out.println("通过trim(K key, long start, long end)方法截取后剩余元素:" + list);

RedisTemplate常用集合-opsForHash

1.put(H key, HK hashKey, HV value)

新增hashMap值。

redisTemplate.opsForHash().put("hashValue","map1","map1-1");
redisTemplate.opsForHash().put("hashValue","map2","map2-2");

2.values(H key)

获取指定变量中的hashMap值。

List<Object> hashList = redisTemplate.opsForHash().values("hashValue");
System.out.println("通过values(H key)方法获取变量中的hashMap值:" + hashList);

3.entries(H key)

获取变量中的键值对。

Map<Object,Object> map = redisTemplate.opsForHash().entries("hashValue");
System.out.println("通过entries(H key)方法获取变量中的键值对:" + map);

4.get(H key, Object hashKey)

获取变量中的指定map键是否有值,如果存在该map键则获取值,没有则返回null。

Object mapValue = redisTemplate.opsForHash().get("hashValue","map1");
System.out.println("通过get(H key, Object hashKey)方法获取map键的值:" + mapValue);

5.hasKey(H key, Object hashKey)

 判断变量中是否有指定的map键。

boolean hashKeyBoolean = redisTemplate.opsForHash().hasKey("hashValue","map3");
System.out.println("通过hasKey(H key, Object hashKey)方法判断变量中是否存在map键:" + hashKeyBoolean);

6.keys(H key)

获取变量中的键。

Set<Object> keySet = redisTemplate.opsForHash().keys("hashValue");
System.out.println("通过keys(H key)方法获取变量中的键:" + keySet);

7.size(H key)

获取变量的长度。

long hashLength = redisTemplate.opsForHash().size("hashValue");
System.out.println("通过size(H key)方法获取变量的长度:" + hashLength);

8.increment(H key, HK hashKey, double delta)

使变量中的键以double值的大小进行自增长。

double hashIncDouble = redisTemplate.opsForHash().increment("hashInc","map1",3);
System.out.println("通过increment(H key, HK hashKey, double delta)方法使变量中的键以值的大小进行自增长:" + hashIncDouble);

9.increment(H key, HK hashKey, long delta)

使变量中的键以long值的大小进行自增长。

long hashIncLong = redisTemplate.opsForHash().increment("hashInc","map2",6);
System.out.println("通过increment(H key, HK hashKey, long delta)方法使变量中的键以值的大小进行自增长:" + hashIncLong);

10.multiGet(H key, Collection hashKeys)

以集合的方式获取变量中的值。

List<Object> list = new ArrayList<Object>();
list.add("map1");
list.add("map2");
List mapValueList = redisTemplate.opsForHash().multiGet("hashValue",list);
System.out.println("通过multiGet(H key, Collection<HK> hashKeys)方法以集合的方式获取变量中的值:"+mapValueList);

11.putAll(H key, Map<? extends HK,? extends HV> m)

以map集合的形式添加键值对。

Map newMap = new HashMap();
newMap.put("map3","map3-3");
newMap.put("map5","map5-5");
redisTemplate.opsForHash().putAll("hashValue",newMap);
map = redisTemplate.opsForHash().entries("hashValue");
System.out.println("通过putAll(H key, Map<? extends HK,? extends HV> m)方法以map集合的形式添加键值对:" + map);

12.putIfAbsent(H key, HK hashKey, HV value)

如果变量值存在,在变量中可以添加不存在的的键值对,如果变量不存在,则新增一个变量,同时将键值对添加到该变量。

redisTemplate.opsForHash().putIfAbsent("hashValue","map6","map6-6");
map = redisTemplate.opsForHash().entries("hashValue");
System.out.println("通过putIfAbsent(H key, HK hashKey, HV value)方法添加不存在于变量中的键值对:" + map);

13.scan(H key, ScanOptions options)

匹配获取键值对,ScanOptions.NONE为获取全部键对,ScanOptions.scanOptions().match("map1").build()     匹配获取键位map1的键值对,不能模糊匹配。

Cursor<Map.Entry<Object,Object>> cursor = redisTemplate.opsForHash().scan("hashValue",ScanOptions.scanOptions().match("map1").build());
//Cursor<Map.Entry<Object,Object>> cursor = redisTemplate.opsForHash().scan("hashValue",ScanOptions.NONE);
while (cursor.hasNext()){
    Map.Entry<Object,Object> entry = cursor.next();
    System.out.println("通过scan(H key, ScanOptions options)方法获取匹配键值对:" + entry.getKey() + "---->" + entry.getValue());
}

14.delete(H key, Object… hashKeys)

删除变量中的键值对,可以传入多个参数,删除多个键值对。

redisTemplate.opsForHash().delete("hashValue","map1","map2");
map = redisTemplate.opsForHash().entries("hashValue");
System.out.println("通过delete(H key, Object... hashKeys)方法删除变量中的键值对后剩余的:" + map);

RedisTemplate常用集合-opsForSet

1.add(K key, V… values)

向变量中批量添加值。

redisTemplate.opsForSet().add("setValue","A","B","C","B","D","E","F");

2.members(K key)

 获取变量中的值。

Set set = redisTemplate.opsForSet().members("setValue");
System.out.println("通过members(K key)方法获取变量中的元素值:" + set);

3.size(K key)

获取变量中值的长度。

long setLength = redisTemplate.opsForSet().size("setValue");
System.out.println("通过size(K key)方法获取变量中元素值的长度:" + setLength);

4.randomMember(K key)

随机获取变量中的元素。

Object randomMember = redisTemplate.opsForSet().randomMember("setValue");
System.out.println("通过randomMember(K key)方法随机获取变量中的元素:" + randomMember);

5.randomMembers(K key, long count)

随机获取变量中指定个数的元素。

List randomMembers = redisTemplate.opsForSet().randomMembers("setValue",2);
System.out.println("通过randomMembers(K key, long count)方法随机获取变量中指定个数的元素:" + randomMembers);

6.isMember(K key, Object o)

检查给定的元素是否在变量中。

boolean isMember = redisTemplate.opsForSet().isMember("setValue","A");
System.out.println("通过isMember(K key, Object o)方法检查给定的元素是否在变量中:" + isMember);

7.move(K key, V value, K destKey)

转移变量的元素值到目的变量。

boolean isMove = redisTemplate.opsForSet().move("setValue","A","destSetValue");
if(isMove){
    set = redisTemplate.opsForSet().members("setValue");
    System.out.print("通过move(K key, V value, K destKey)方法转移变量的元素值到目的变量后的剩余元素:" + set);
    set = redisTemplate.opsForSet().members("destSetValue");
    System.out.println(",目的变量中的元素值:" + set);
}

8.pop(K key)

弹出变量中的元素。

Object popValue = redisTemplate.opsForSet().pop("setValue");
System.out.print("通过pop(K key)方法弹出变量中的元素:" + popValue);
set = redisTemplate.opsForSet().members("setValue");
System.out.println(",剩余元素:" + set)

9.remove(K key, Object… values)

批量移除变量中的元素。

long removeCount = redisTemplate.opsForSet().remove("setValue","E","F","G");
System.out.print("通过remove(K key, Object... values)方法移除变量中的元素个数:" + removeCount);
set = redisTemplate.opsForSet().members("setValue");
System.out.println(",剩余元素:" + set);

10.scan(K key, ScanOptions options)

匹配获取键值对,ScanOptions.NONE为获取全部键值对;ScanOptions.scanOptions().match("C").build()匹配获取键位map1的键值对,不能模糊匹配。

//Cursor<Object> cursor = redisTemplate.opsForSet().scan("setValue", ScanOptions.NONE);
Cursor<Object> cursor = redisTemplate.opsForSet().scan("setValue", ScanOptions.scanOptions().match("C").build());
while (cursor.hasNext()){
    Object object = cursor.next();
    System.out.println("通过scan(K key, ScanOptions options)方法获取匹配的值:" + object);
}

11.difference(K key, Collection otherKeys)

通过集合求差值。

List list = new ArrayList();
list.add("destSetValue");
Set differenceSet = redisTemplate.opsForSet().difference("setValue",list);
System.out.println("通过difference(K key, Collection<K> otherKeys)方法获取变量中与给定集合中变量不一样的值:" + differenceSet);

12.difference(K key, K otherKey)

通过给定的key求2个set变量的差值。

differenceSet = redisTemplate.opsForSet().difference("setValue","destSetValue");
System.out.println("通过difference(K key, Collection<K> otherKeys)方法获取变量中与给定变量不一样的值:" + differenceSet);

13.differenceAndStore(K key, K otherKey, K destKey)

将求出来的差值元素保存。

redisTemplate.opsForSet().differenceAndStore("setValue","destSetValue","storeSetValue");
set = redisTemplate.opsForSet().members("storeSetValue");
System.out.println("通过differenceAndStore(K key, K otherKey, K destKey)方法将求出来的差值元素保存:" + set);

14.differenceAndStore(K key, Collection otherKeys, K destKey)

将求出来的差值元素保存。

redisTemplate.opsForSet().differenceAndStore("setValue",list,"storeSetValue");
set = redisTemplate.opsForSet().members("storeSetValue");
System.out.println("通过differenceAndStore(K key, Collection<K> otherKeys, K destKey)方法将求出来的差值元素保存:" + set);

15.distinctRandomMembers(K key, long count)

获取去重的随机元素。

set = redisTemplate.opsForSet().distinctRandomMembers("setValue",2);
System.out.println("通过distinctRandomMembers(K key, long count)方法获取去重的随机元素:" + set);

16.intersect(K key, K otherKey)

获取2个变量中的交集。

set = redisTemplate.opsForSet().intersect("setValue","destSetValue");
System.out.println("通过intersect(K key, K otherKey)方法获取交集元素:" + set);

17.intersect(K key, Collection otherKeys)

获取多个变量之间的交集。

set = redisTemplate.opsForSet().intersect("setValue",list);
System.out.println("通过intersect(K key, Collection<K> otherKeys)方法获取交集元素:" + set);

18.intersectAndStore(K key, K otherKey, K destKey)

获取2个变量交集后保存到最后一个参数上。

redisTemplate.opsForSet().intersectAndStore("setValue","destSetValue","intersectValue");
set = redisTemplate.opsForSet().members("intersectValue");
System.out.println("通过intersectAndStore(K key, K otherKey, K destKey)方法将求出来的交集元素保存:" + set);

19.intersectAndStore(K key, Collection otherKeys, K destKey)

获取多个变量的交集并保存到最后一个参数上。

redisTemplate.opsForSet().intersectAndStore("setValue",list,"intersectListValue");
set = redisTemplate.opsForSet().members("intersectListValue");
System.out.println("通过intersectAndStore(K key, Collection<K> otherKeys, K destKey)方法将求出来的交集元素保存:" + set);

20.union(K key, K otherKey)

获取2个变量的合集。

set = redisTemplate.opsForSet().union("setValue","destSetValue");
System.out.println("通过union(K key, K otherKey)方法获取2个变量的合集元素:" + set);

21.union(K key, Collection otherKeys)

获取多个变量的合集。

set = redisTemplate.opsForSet().union("setValue",list);
System.out.println("通过union(K key, Collection<K> otherKeys)方法获取多个变量的合集元素:" + set);

22.unionAndStore(K key, K otherKey, K destKey)

获取2个变量合集后保存到最后一个参数上。

redisTemplate.opsForSet().unionAndStore("setValue","destSetValue","unionValue");
set = redisTemplate.opsForSet().members("unionValue");
System.out.println("通过unionAndStore(K key, K otherKey, K destKey)方法将求出来的交集元素保存:" + set);

23.unionAndStore(K key, Collection otherKeys, K destKey)

获取多个变量的合集并保存到最后一个参数上。

redisTemplate.opsForSet().unionAndStore("setValue",list,"unionListValue");
set = redisTemplate.opsForSet().members("unionListValue");
System.out.println("通过unionAndStore(K key, Collection<K> otherKeys, K destKey)方法将求出来的交集元素保存:" + set);

RedisTemplate常用集合-opsForZSet

1.add(K key, V value, double score)

添加元素到变量中同时指定元素的分值。

redisTemplate.opsForZSet().add("zSetValue","A",1);
redisTemplate.opsForZSet().add("zSetValue","B",3);
redisTemplate.opsForZSet().add("zSetValue","C",2);
redisTemplate.opsForZSet().add("zSetValue","D",5);

2.range(K key, long start, long end)

获取变量指定区间的元素。

Set zSetValue = redisTemplate.opsForZSet().range("zSetValue",0,-1);
System.out.println("通过range(K key, long start, long end)方法获取指定区间的元素:" + zSetValue);

3.rangeByLex(K key, RedisZSetCommands.Range range)

 用于获取满足非score的排序取值。这个排序只有在有相同分数的情况下才能使用,如果有不同的分数则返回值不确定。

RedisZSetCommands.Range range = new RedisZSetCommands.Range();
//range.gt("A");
range.lt("D");
zSetValue = redisTemplate.opsForZSet().rangeByLex("zSetValue", range);
System.out.println("通过rangeByLex(K key, RedisZSetCommands.Range range)方法获取满足非score的排序取值元素:" + zSetValue);

4.rangeByLex(K key, RedisZSetCommands.Range range, RedisZSetCommands.Limit limit)

用于获取满足非score的设置下标开始的长度排序取值。

RedisZSetCommands.Limit limit = new RedisZSetCommands.Limit();
limit.count(2);
//起始下标为0
limit.offset(1);
zSetValue = redisTemplate.opsForZSet().rangeByLex("zSetValue", range,limit);
System.out.println("通过rangeByLex(K key, RedisZSetCommands.Range range, RedisZSetCommands.Limit limit)方法获取满足非score的排序取值元素:" + zSetValue);

5.add(K key, Set<ZSetOperations.TypedTuple> tuples)

通过TypedTuple方式新增数据。

ZSetOperations.TypedTuple<Object> typedTuple1 = new DefaultTypedTuple<Object>("E",6.0);
ZSetOperations.TypedTuple<Object> typedTuple2 = new DefaultTypedTuple<Object>("F",7.0);
ZSetOperations.TypedTuple<Object> typedTuple3 = new DefaultTypedTuple<Object>("G",5.0);
Set<ZSetOperations.TypedTuple<Object>> typedTupleSet = new HashSet<ZSetOperations.TypedTuple<Object>>();
typedTupleSet.add(typedTuple1);
typedTupleSet.add(typedTuple2);
typedTupleSet.add(typedTuple3);
redisTemplate.opsForZSet().add("typedTupleSet",typedTupleSet);
zSetValue = redisTemplate.opsForZSet().range("typedTupleSet",0,-1);
System.out.println("通过add(K key, Set<ZSetOperations.TypedTuple<V>> tuples)方法添加元素:" + zSetValue);

6.rangeByScore(K key, double min, double max)

根据设置的score获取区间值。

zSetValue = redisTemplate.opsForZSet().rangeByScore("zSetValue",1,2);
System.out.println("通过rangeByScore(K key, double min, double max)方法根据设置的score获取区间值:" + zSetValue);

7.rangeByScore(K key, double min, double max,long offset, long count)

根据设置的score获取区间值从给定下标和给定长度获取最终值。

zSetValue = redisTemplate.opsForZSet().rangeByScore("zSetValue",1,5,1,3);
System.out.println("通过rangeByScore(K key, double min, double max, long offset, long count)方法根据设置的score获取区间值:" + zSetValue);

8.rangeWithScores(K key, long start, long end)

 获取RedisZSetCommands.Tuples的区间值。

Set<ZSetOperations.TypedTuple<Object>> typedTupleSet = redisTemplate.opsForZSet().rangeWithScores("typedTupleSet",1,3);
Iterator<ZSetOperations.TypedTuple<Object>> iterator = typedTupleSet.iterator();
while (iterator.hasNext()){
    ZSetOperations.TypedTuple<Object> typedTuple = iterator.next();
    Object value = typedTuple.getValue();
    double score = typedTuple.getScore();
    System.out.println("通过rangeWithScores(K key, long start, long end)方法获取RedisZSetCommands.Tuples的区间值:" + value + "---->" + score );
}

9.rangeByScoreWithScores(K key, double min, double max)

获取RedisZSetCommands.Tuples的区间值通过分值。

Set<ZSetOperations.TypedTuple<Object>> typedTupleSet = redisTemplate.opsForZSet().rangeByScoreWithScores("typedTupleSet",5,8);
iterator = typedTupleSet.iterator();
while (iterator.hasNext()){
    ZSetOperations.TypedTuple<Object> typedTuple = iterator.next();
    Object value = typedTuple.getValue();
    double score = typedTuple.getScore();
    System.out.println("通过rangeByScoreWithScores(K key, double min, double max)方法获取RedisZSetCommands.Tuples的区间值通过分值:" + value + "---->" + score );
}

10.rangeByScoreWithScores(K key, double min, double max, long offset, long count)

获取RedisZSetCommands.Tuples的区间值从给定下标和给定长度获取最终值通过分值。

Set<ZSetOperations.TypedTuple<Object>> typedTupleSet = redisTemplate.opsForZSet().rangeByScoreWithScores("typedTupleSet",5,8,1,1);
iterator = typedTupleSet.iterator();
while (iterator.hasNext()){
    ZSetOperations.TypedTuple<Object> typedTuple = iterator.next();
    Object value = typedTuple.getValue();
    double score = typedTuple.getScore();
    System.out.println("通过rangeByScoreWithScores(K key, double min, double max, long offset, long count)方法获取RedisZSetCommands.Tuples的区间值从给定下标和给定长度获取最终值通过分值:" + value + "---->" + score );
}

11.count(K key, double min, double max)

获取区间值的个数。

long count = redisTemplate.opsForZSet().count("zSetValue",1,5);
System.out.println("通过count(K key, double min, double max)方法获取区间值的个数:" + count);

12.rank(K key, Object o)

获取变量中元素的索引,下标开始位置为0。

long index = redisTemplate.opsForZSet().rank("zSetValue","B");
System.out.println("通过rank(K key, Object o)方法获取变量中元素的索引:" + index);

13.scan(K key, ScanOptions options)

匹配获取键值对,ScanOptions.NONE为获取全部键值对;ScanOptions.scanOptions().match("C").build()匹配获取键位map1的键值对,不能模糊匹配。

//Cursor<Object> cursor = redisTemplate.opsForSet().scan("setValue", ScanOptions.NONE);
Cursor<ZSetOperations.TypedTuple<Object>> cursor = redisTemplate.opsForZSet().scan("zSetValue", ScanOptions.NONE);
    while (cursor.hasNext()){
    ZSetOperations.TypedTuple<Object> typedTuple = cursor.next();
    System.out.println("通过scan(K key, ScanOptions options)方法获取匹配元素:" + typedTuple.getValue() + "--->" + typedTuple.getScore());
}

14.score(K key, Object o)

 获取元素的分值。

double score = redisTemplate.opsForZSet().score("zSetValue","B");
System.out.println("通过score(K key, Object o)方法获取元素的分值:" + score);

15.zCard(K key)

获取变量中元素的个数。

long zCard = redisTemplate.opsForZSet().zCard("zSetValue");
System.out.println("通过zCard(K key)方法获取变量的长度:" + zCard);

16.incrementScore(K key, V value, double delta)

修改变量中的元素的分值。

double incrementScore = redisTemplate.opsForZSet().incrementScore("zSetValue","C",5);
System.out.print("通过incrementScore(K key, V value, double delta)方法修改变量中的元素的分值:" + incrementScore);
score = redisTemplate.opsForZSet().score("zSetValue","C");
System.out.print(",修改后获取元素的分值:" + score);
zSetValue = redisTemplate.opsForZSet().range("zSetValue",0,-1);
System.out.println(",修改后排序的元素:" + zSetValue);

17.reverseRange(K key, long start, long end)

索引倒序排列指定区间元素。

zSetValue = redisTemplate.opsForZSet().reverseRange("zSetValue",0,-1);
System.out.println("通过reverseRange(K key, long start, long end)方法倒序排列元素:" + zSetValue);

18.reverseRangeByScore(K key, double min, double max)

倒序排列指定分值区间元素。

zSetValue = redisTemplate.opsForZSet().reverseRangeByScore("zSetValue",1,5);
System.out.println("通过reverseRangeByScore(K key, double min, double max)方法倒序排列指定分值区间元素:" + zSetValue);

19.reverseRangeByScore(K key, double min, double max, long offset, long count)

倒序排列从给定下标和给定长度分值区间元素。

zSetValue = redisTemplate.opsForZSet().reverseRangeByScore("zSetValue",1,5,1,2);
System.out.println("通过reverseRangeByScore(K key, double min, double max, long offset, long count)方法倒序排列从给定下标和给定长度分值区间元素:" + zSetValue);

20.reverseRangeByScoreWithScores(K key, double min, double max)

倒序排序获取RedisZSetCommands.Tuples的分值区间值。

Set<ZSetOperations.TypedTuple<Object>> typedTupleSet = redisTemplate.opsForZSet().reverseRangeByScoreWithScores("zSetValue",1,5);
iterator = typedTupleSet.iterator();
while (iterator.hasNext()){
    ZSetOperations.TypedTuple<Object> typedTuple = iterator.next();
    Object value = typedTuple.getValue();
    double score1 = typedTuple.getScore();
    System.out.println("通过reverseRangeByScoreWithScores(K key, double min, double max)方法倒序排序获取RedisZSetCommands.Tuples的区间值:" + value + "---->" + score1 );
}

21.reverseRangeByScoreWithScores(K key, double min, double max, long offset, long count)

倒序排序获取RedisZSetCommands.Tuples的从给定下标和给定长度分值区间值。

Set<ZSetOperations.TypedTuple<Object>> typedTupleSet = redisTemplate.opsForZSet().reverseRangeByScoreWithScores("zSetValue",1,5,1,2);
iterator = typedTupleSet.iterator();
while (iterator.hasNext()){
    ZSetOperations.TypedTuple<Object> typedTuple = iterator.next();
    Object value = typedTuple.getValue();
    double score1 = typedTuple.getScore();
    System.out.println("通过reverseRangeByScoreWithScores(K key, double min, double max, long offset, long count)方法倒序排序获取RedisZSetCommands.Tuples的从给定下标和给定长度区间值:" + value + "---->" + score1 );
}

22.reverseRangeWithScores(K key, long start, long end)

索引倒序排列区间值。

Set<ZSetOperations.TypedTuple<Object>> typedTupleSet = redisTemplate.opsForZSet().reverseRangeWithScores("zSetValue",1,5);
iterator = typedTupleSet.iterator();
    while (iterator.hasNext()){
    ZSetOperations.TypedTuple<Object> typedTuple = iterator.next();
    Object value = typedTuple.getValue();
    double score1 = typedTuple.getScore();
    System.out.println("通过reverseRangeWithScores(K key, long start, long end)方法索引倒序排列区间值:" + value + "----->" + score1);
}

23.reverseRank(K key, Object o)

获取倒序排列的索引值。

long reverseRank = redisTemplate.opsForZSet().reverseRank("zSetValue","B");
System.out.println("通过reverseRank(K key, Object o)获取倒序排列的索引值:" + reverseRank);

24.intersectAndStore(K key, K otherKey, K destKey)

获取2个变量的交集存放到第3个变量里面。

redisTemplate.opsForZSet().intersectAndStore("zSetValue","typedTupleSet","intersectSet");
zSetValue = redisTemplate.opsForZSet().range("intersectSet",0,-1);
System.out.println("通过intersectAndStore(K key, K otherKey, K destKey)方法获取2个变量的交集存放到第3个变量里面:" + zSetValue);

25.intersectAndStore(K key, Collection otherKeys, K destKey)

获取多个变量的交集存放到第3个变量里面。

List list = new ArrayList();
list.add("typedTupleSet");
redisTemplate.opsForZSet().intersectAndStore("zSetValue",list,"intersectListSet");
zSetValue = redisTemplate.opsForZSet().range("intersectListSet",0,-1);
System.out.println("通过intersectAndStore(K key, Collection<K> otherKeys, K destKey)方法获取多个变量的交集存放到第3个变量里面:" + zSetValue);

26.unionAndStore(K key, K otherKey, K destKey)

获取2个变量的合集存放到第3个变量里面。

redisTemplate.opsForZSet().unionAndStore("zSetValue","typedTupleSet","unionSet");
zSetValue = redisTemplate.opsForZSet().range("unionSet",0,-1);
System.out.println("通过unionAndStore(K key, K otherKey, K destKey)方法获取2个变量的交集存放到第3个变量里面:" + zSetValue);

27.unionAndStore(K key, Collection otherKeys, K destKey)

获取多个变量的合集存放到第3个变量里面。

redisTemplate.opsForZSet().unionAndStore("zSetValue",list,"unionListSet");
zSetValue = redisTemplate.opsForZSet().range("unionListSet",0,-1);
System.out.println("通过unionAndStore(K key, Collection<K> otherKeys, K destKey)方法获取多个变量的交集存放到第3个变量里面:" + zSetValue);

28.remove(K key, Object… values)

批量移除元素根据元素值。

long removeCount = redisTemplate.opsForZSet().remove("unionListSet","A","B");
zSetValue = redisTemplate.opsForZSet().range("unionListSet",0,-1);
System.out.print("通过remove(K key, Object... values)方法移除元素的个数:" + removeCount);
System.out.println(",移除后剩余的元素:" + zSetValue);

29.removeRangeByScore(K key, double min, double max)

根据分值移除区间元素。

removeCount = redisTemplate.opsForZSet().removeRangeByScore("unionListSet",3,5);
zSetValue = redisTemplate.opsForZSet().range("unionListSet",0,-1);
System.out.print("通过removeRangeByScore(K key, double min, double max)方法移除元素的个数:" + removeCount);
System.out.println(",移除后剩余的元素:" + zSetValue);

30.removeRange(K key, long start, long end)

根据索引值移除区间元素。

removeCount = redisTemplate.opsForZSet().removeRange("unionListSet",3,5);
zSetValue = redisTemplate.opsForZSet().range("unionListSet",0,-1);
System.out.print("通过removeRange(K key, long start, long end)方法移除元素的个数:" + removeCount);
System.out.println(",移除后剩余的元素:" + zSetValue);

RedisTemplate实践一

参赛号的自增,利用自增获取参赛号的值,写入对象存库。

//参赛号为空,且起始值大于-1
if (StringUtils.isEmpty(enrollMatch.getSeq()) && match.getSeqStart() > -1) {
        //第一次参赛证生成
        //判断redis里面是否有对应的比赛的key,如果没有,
        //执行redisTemplate.opsForHash().increment("match_seq", match.get_id(), match.getSeqStart());
        //在起始参赛号自增1。
        //redisTemplate.opsForHash().hasKey("match_seq", match.get_id())
        if (!redisTemplate.opsForHash().hasKey("match_seq", match.get_id()))
            redisTemplate.opsForHash().increment("match_seq", match.get_id(), match.getSeqStart());
        //非第一次
        long num = redisTemplate.opsForHash().increment("match_seq", match.get_id(), 1);
        String numStr = "" + num;
        while (numStr.length() < 5) // 凑够长度5
            numStr = "0" + numStr;
        String seq = match.getSeqPrefix() + numStr;
}

参赛证分配

数据结构:系统可以针对比赛设置赛区;赛区里设置考场,如第一考场,第二考场等;考场里设置有座位号,每个考场座位号从1开始,座位号个数可以系统设置。

需求:给每个参赛选手分配座位号。

实现过程:

//查询所有需要分配考场座位的记录。根据比赛阶段(复赛,线下形式),已交报名费,已选赛点,未分配考场。
List<MenrollPhase> menrollPhaseList = mongoTemplate.find(Query.query(Criteria.where("phaseId").is(phaseId).and("state").is(2).and("matchPlaceId").ne(null).ne("").and("matchPlaceRoomId").in(null,"")).with(new Sort("_id", "ASC")), MenrollPhase.class);
//查询比赛是否设置赛区、需要分配考场的记录不为空且size大于0
if(mphase.getMatchPlaceIds()!=null&&menrollPhaseList!=null&&menrollPhaseList.size()>0){
lineNumber=menrollPhaseList.size();//要生成参赛证总数
    //查询该比赛所有赛区
    List<MmatchPlace> mmatchPlaceList = mongoTemplate.find(Query.query(Criteria.where("_id").in(mphase.getMatchPlaceIds())), MmatchPlace.class);
    if(mmatchPlaceList!=null&&mmatchPlaceList.size()>0){
        ListOperations<String, Object> lo = redisTemplate.opsForList();
        //循环遍历赛区,将考场设置到赛区中
        for(MmatchPlace mmatchPlace : mmatchPlaceList){
            List<MmatchPlaceRoom> mmatchPlaceRoomList = mongoTemplate.find(Query.query(Criteria.where("placeId").in(mmatchPlace.get_id())).with(new Sort("order", "ASC")), MmatchPlaceRoom.class);
            mmatchPlace.setMmatchPlaceRooms(mmatchPlaceRoomList);
        }
        //参赛证模板
        String[] temp = mphase.getCertTpl().split("\\|");
        //文件名
        String fileName = temp[2];

        String quchu=null;
        //加锁,分布式环境下只能有一个线程去考场获取座位号
        boolean groupAbsent = redisTemplate.opsForValue().setIfAbsent("placeRoom_" + mphase.get_id(), "roomsuo");
        //groupAbsent为true  可以执行当前代码
        if(groupAbsent){
            //遍历赛区
            for(MmatchPlace mmatchPlace : mmatchPlaceList){
                //比赛和赛区id共同组成key
                String mmatchPlaceIdKey = "matchPlace_" +mphase.get_id()+mmatchPlace.get_id();
                //首次分配
                if(!redisTemplate.hasKey(mmatchPlaceIdKey)&&mmatchPlace.getMmatchPlaceRooms()!=null&&mmatchPlace.getMmatchPlaceRooms().size()>0){
                    //遍历赛区下的考场,考场id和座位号共同组成value,赛区为key,考场和座位为value中间以“,”隔开,存入
                    //redis。
                    for(MmatchPlaceRoom mmatchPlaceRoom : mmatchPlace.getMmatchPlaceRooms()){
                        for(int i=0;i<mmatchPlaceRoom.getCounts();i++){
                            String placeValue = mmatchPlaceRoom.get_id()+","+(i+1);
                            lo.leftPush(mmatchPlaceIdKey, placeValue);//存入redis
                        }
                    }
                }
            }
            checkStatus=1;//准备考场已经结束
            //开始分考场
            for(MenrollPhase menrollPhase : menrollPhaseList){
                if(!StringUtils.isEmpty(menrollPhase.getMatchPlaceId())&&StringUtils.isEmpty(menrollPhase.getMatchPlaceRoomId())){
                    for(MmatchPlace mmatchPlace : mmatchPlaceList){
                        if(mmatchPlace.get_id().equals(menrollPhase.getMatchPlaceId())){
                            //取出分配好的座位
                            quchu = (String) lo.rightPop("matchPlace_" +mphase.get_id()+mmatchPlace.get_id());
                            //redis里面没有说明已经取出了。
                            if(StringUtils.isEmpty(quchu)){
                                continue;
                            }
                            String[] tempNum = quchu.split(",");
                            String roomId = tempNum[0];//考场Id
                            String numBer = tempNum[1];//座位号
                            for(MmatchPlaceRoom mmatchPlaceRoom : mmatchPlace.getMmatchPlaceRooms()){
                                if(mmatchPlaceRoom.get_id().equals(roomId)){
                                    //将取出的考场和座位号存入对象写入数据库
                                    menrollPhase.setMatchPlaceRoomId(mmatchPlaceRoom.get_id());
                                    menrollPhase.setMatchPlaceRoomNum(Integer.valueOf(numBer));
                                    mongoTemplate.save(menrollPhase);
                                    menrollPhase.setMatchPlace(mmatchPlace);
                                    menrollPhase.setMatchPlaceRoom(mmatchPlaceRoom);
                                }
                            }
                        }
                    }
                }
                }
            }
            //删除锁
            redisTemplate.delete("placeRoom_" + mphase.get_id());

            //开始生成参赛证
            try{
                caseCount=0;
                this.getDetails(menrollPhaseList);
                //方法一:使用Windows系统字体(TrueType)  
                BaseFont baseFont = BaseFont.createFont(path+"WEB-INF/template/SIMSUN.TTC,1",BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED); 
                for(MenrollPhase menrollPhase : menrollPhaseList){
                    if(!StringUtils.isEmpty(menrollPhase.getMatchPlaceId())
                            &&!StringUtils.isEmpty(menrollPhase.getMatchPlaceRoomId())
                            &&menrollPhase.getMatchPlaceRoomNum()!=0){
                        boolean isOk = checkedCarState(menrollPhase);
                        if(!isOk){
                            continue;
                        }
                        PdfReader reader = new PdfReader(fileName);
                        ByteArrayOutputStream bos = new ByteArrayOutputStream();
                        PdfStamper ps = new PdfStamper(reader, bos);
                        AcroFields fields = ps.getAcroFields();
                        fields.addSubstitutionFont(baseFont);
                        Map<String, String> map = new HashMap<String, String>();
                        fillMap(map,menrollPhase);
                        fillData(fields, map);
                        ps.setFormFlattening(true);
                        ps.close();
                        byte[] bytes = bos.toByteArray();
                        String base = Base64Util.encode(bytes).trim();
                        redisTemplate.opsForHash().put("cansai_"+phaseId,menrollPhase.get_id(), base);
                        caseCount++;
                    }
                }
                contectStatus=1;//参赛证生成结束
            }catch (IOException|DocumentException e) {
             e.printStackTrace();
         }
       }
    }

Redis分布式锁解决抢购问题

先新建一个RedisLock类:

public class RedisService {

    @Autowired
    private RedisTemplate stringRedisTemplate;

    /***
     * 加锁
     * @param key
     * @param value 当前时间+超时时间
     * @return 锁住返回true
     */
    public boolean lock(String key,String value){
        if(stringRedisTemplate.opsForValue().setIfAbsent(key,value)){//setNX 返回boolean
            return true;
        }
        //如果锁超时 ***
        String currentValue = stringRedisTemplate.opsForValue().get(key);
        if(!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue)<System.currentTimeMillis()){
            //获取上一个锁的时间
            String oldvalue  = stringRedisTemplate.opsForValue().getAndSet(key,value);
            if(!StringUtils.isEmpty(oldvalue)&&oldvalue.equals(currentValue)){
                return true;
            }
        }
        return false;
    }
    /***
     * 解锁
     * @param key
     * @param value
     * @return
     */
    public void unlock(String key,String value){
        try {
            String currentValue = stringRedisTemplate.opsForValue().get(key);
            if(!StringUtils.isEmpty(currentValue)&&currentValue.equals(value)){
                stringRedisTemplate.opsForValue().getOperations().delete(key);
            }
        } catch (Exception e) {
            log.error("解锁异常");
        }
    }
}

首先,锁的value值是当前时间加上过期时间的时间戳,Long类型。首先看到用setiFAbsent方法也就是对应的SETNX,在没有线程获得锁的情况下可以直接拿到锁,并返回true也就是加锁,最后没有获得锁的线程会返回false。

最重要的是中间对于锁超时的处理,如果没有这段代码,当秒杀方法发生异常的时候,后续的线程都无法得到锁,也就陷入了一个死锁的情况。我们可以假设CurrentValue为A,并且在执行过程中抛出了异常,这时进入了两个value为B的线程来争夺这个锁,也就是走到了注释*的地方。currentValue==A,这时某一个线程执行到了getAndSet(key,value)函数(某一时刻一定只有一个线程执行这个方法,其他要等待)。这时oldvalue也就是之前的value等于A,在方法执行过后,oldvalue会被设置为当前的value也就是B。这时继续执行,由于oldValue==currentValue所以该线程获取到锁。而另一个线程获取的oldvalue是B,而currentValue是A,所以他就获取不到锁啦。

业务代码:

private static final int TIMEOUT= 10*1000;
@Transactional
public void orderProductMockDiffUser(String productId){
   long time = System.currentTimeMillions()+TIMEOUT;
   if(!redislock.lock(productId,String.valueOf(time)){
    throw new SellException(101,"换个姿势再试试")
    }
    //1.查库存
    int stockNum  = stock.get(productId);
    if(stocknum == 0){
        throw new SellException(ProductStatusEnum.STOCK_EMPTY);
        //这里抛出的异常要是运行时异常,否则无法进行数据回滚,这也是spring中比较基础的   
    }else{
        //2.下单
        orders.put(KeyUtil.genUniqueKey(),productId);//生成随机用户id模拟高并发
        sotckNum = stockNum-1;
        try{
            Thread.sleep(100);
        } catch (InterruptedExcption e){
            e.printStackTrace();
        }
        stock.put(productId,stockNum);
    }
    redisLock.unlock(productId,String.valueOf(time));
}

HTTP概述和URL

发表于 2018-12-10 | 分类于 https

引言

HTTP协议,一个熟悉又陌生的应用协议。熟悉的是它在各种计算机网络教材中必然会被提及的应用层协议,稍微有点计算机知识的人基本上都会听过这个协议。它是互联网的基础,可以这么说,没有HTTP协议,就没有当前互联网的蓬勃发展。然而,笔者对它又是陌生的。因为虽然参与开发了不少Web应用,但很少直接与HTTP协议直接打交道,因为Web容器隐藏了很多HTTP协议的细节(譬如:请求协议头的解析,响应报文的生成),使得上层开发不用关心HTTP即可以开发出可用的Web应用。不应该仅仅满足可用,还应该让应用更加高效,这促使我不得不去研究HTTP协议。私以为,如果想进一步提升自己的技术能力,必须要深入到协议层级别,一些平时感觉到莫名其妙的问题就可以迎刃而解。

最近阅读了《HTTP权威指南》一书,本文包含书中的核心要点、个人延伸以及个人操作实践。由于本书是2009年出版,距现在已经有6年之久,期间,HTTP协议本身也不断地发展。所以,一些老旧的知识点将不会出现在本文中。希望通过不断阅读和实践,并且记录下来,能够加深对HTTP协议本身的理解!

HTTP协议概述

本节介绍一些HTTP的基础知识,先对HTTP有一个宏观上的了解。

HTTP协议是什么

平常老看到TCP/IP协议、FTP协议,XX协议……不禁要问,到底什么是协议?查阅了百度百科,上面是这么定义的:

基本解释:共同计议;协商
法律范畴:协议是指两个或两个以上实体为了开展某项活动,经过协商后双方达成的一致意见。

两个要点:

1. 两个及两个以上参与者。也就是说,如果只有一方参与,根本就不会涉及到协议。
2. 协商一致。也就是说,所有参与方都必须同意并且遵守,才能使得活动能正常运行下去。

上面讲的一般意义上的协议,在计算机领域中,我们讲的协议一般是指通信协议,它仍然遵循上面的要点。首先,通信必然涉及到多方参与;其次,如果有一方不遵守协议,则根本没法进行有效通信。

下面来看看啥是HTTP协议,百度百科:超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。1960年美国人Ted Nelson构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext),这成为了HTTP超文本传输协议标准架构的发展根基。Ted Nelson组织协调万维网协会(World Wide Web Consortium)和互联网工程工作小组(Internet Engineering Task Force )共同合作研究,最终发布了一系列的RFC,其中著名的RFC 2616定义了HTTP 1.1。

HTTP协议的特点

下面从这篇博客中转过来的HTTP协议的特点,这篇文章对HTTP做了很全面的介绍,值得一读。

  1. 支持客户/服务器模式。支持基本认证和安全认证。
  2. 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
  3. 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
  4. HTTP 0.9和1.0使用非持续连接(无连接性):限制每次连接只处理一个请求,服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。HTTP 1.1使用持续连接:不必为每个web对象创建一个新的连接,一个连接可以传送多个对象。
  5. 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大

特别直接说明的是HTTP的连接性。在我以前的认识里,HTTP最重要的特性是无连接性和无状态性。所谓无连接性,是指每一次请求都使用新的连接,请求完毕后连接关闭。这样做最大的好处时,最大程度上减少空闲连接占用服务端资源,这在系统资源比较昂贵、页面比较简单、仅传输静态页面的年代确实是非常合理的。但是,随着电商、视频等富媒体Web应用的兴起,HTTP的无连接性制约了系统的性能。一个Web应用动辄上百张图片,每一张图片都要占用一个网络连接。要知道,每新建一个连接都需要在TCP协议层进行“三次握手”,效率非常低下。随着在HTTP v1.1版本中默认采用Keep-Alive,多个请求可以使用同一个连接,HTTP的无连接性描述已经变得不准确了。

版本变化

  • HTTP/0.9 已过时。只接受 GET 一种请求方法,没有在通讯中指定版本号,且不支持请求头。由于该版本不支持 POST 方法,所以客户端无法向服务器传递太多信息。
  • HTTP/1.0 这是第一个在通讯中指定版本号的HTTP 协议版本,至今仍被广泛采用,特别是在代理服务器中。
  • HTTP/1.1 当前版本。持久连接被默认采用,并能很好地配合代理服务器工作。还支持以管道方式同时发送多个请求,以便降低线路负载,提高传输速度。

URL与资源

什么是URL

URL(Uniform Resource Location, 统一资源定位符)是Internet上的所有资源的标准化名称。可以把Internet看做一个巨大的正在扩张的城市,里面充满了各种可看的东西,可做的事情。我们需要为这个城市里面的所有景点和服务起一个名字,所有的名字必须在符合统一的标准,这样才能使得我们方便地使用这座城市的宝藏。URL就是其中一类重要的资源命名方式。URL指向每一条电子信息,告诉你它位于何处,如何与之进行交互。URL必须是唯一的,也就是说,一个URL只能对应唯一的资源。

URI、URL和URN

说起URL,就必然要提URI和URN。那么它们之间到底有什么联系和区别呢?首先来看看URI和URN是什么。

  • URI:即Uniform Resource Identifier,统一资源标识符。它是一个通用的概念,理论上,能保证资源全局唯一性的标识符都可以叫做URI;
  • URN:即Uniform Resource Name,统一资源名称。这样的资源名是与资源具体的位置无关的。

URI、URL、URN是相互关联的。URL和URN都是URI的子集,按照集合论的观点,它们之间的关系见下图。

也就说,任何的URL都可以是URI,反之不然。URL是与资源所处的位置密切相关的,如果资源挪动位置,则必然导致URL跟着一起变化。想象一下,如果资源换了位置(虽然这极少发生),则原来开发的软件就会失效。URN主要就是为了解决这个问题而提出来的。它通过给资源命名而不是定位来唯一地确定资源。

假定现在要给我起个独一无二的名称,我可以自己命名为此时我所处的地理位置,假设是(东经36度,北纬36度)。好了,现在请叫我(东经36度,北纬36度),通过地图肯定可以找到我。然而,我并不是时时刻刻都呆着这里的,因为我是个活人。当我移动位置之后,通过原来的位置定位到的已然不是我了,这就很麻烦了。如果用我的姓名来找我(假设是独一无二的),则无论何时何地找到的都是我了。

目前来说,URN貌似还没能到实用阶段,看上去用处也不大。一则资源位置一般不会发生变动;二则URL已经完全普及,为啥要抛弃已经用得溜溜的东西呢?!至少目前还没有理由这么干。所以,在《HTTP权威指南》一书中,并没有将URI和URL区分开来,所有的URI都可以看做是URL。

URL的完整语法

在Web应用中,URL通常是由3各部分构成,以URL地址http://www.joes.com/seanonal/index.html为例来进行说明。

  • 第一部分(http)是方案(schema),可以告知客户端怎样访问资源。
  • 第二部分(www.joes.com)是服务器的位置,告知客户端资源位于何处。
  • 第三部分(/seanonal/index.html)是资源路径。

上面的URL地址只是众多格式的URL地址中的一种,实际上,URL还可以通过HTTP以外的其他协议来进行访问。比如个人E-mail账户:

ikangbow@outlook.com

或者是通过FTP协议获取文件:

ftp://ftp.xxx.com/file.xls

咋一看,这些URL的格式都不太一样,这是不是意味着每种不同的URL方案会有完全不同的语法呢?其实不然。大多数URL语法都建立在9个部分构成的通用格式上:

schema://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag></pre>

每一部分的的意义如下:

  • schema:获取资源时使用何种协议
  • user:访问资源时需要的用户名
  • password:访问资源时与用户名配套的密码
  • host:资源所在的服务器地址
  • port:服务器所监听的端口
  • path:服务器上的本地资源路径,用/分隔
  • params:指定输入参数,用key/value表示
  • query:资源查询参数
  • frag:资源内部的片段名字

URL通常会由其中的某些部分组合而成,下面是一些URL典型示例:

http://www.joes.com/seanonal/index.html
file:///D:/relativeUrl.html
ftp://username:password@ftp.xxx.com/file.xls
http://www.joes.com/seanonal/index.html;type=d
http://www.joes.com/seanonal/goods.html?item=45454
http://www.joes.com/seanonal/goods.html?item=45454#name

相对URL

绝对URL和相对URL是URL的两种不同的表现形式,前面所有的URL示例都是绝对URL,绝对URL包含访问资源所需的全部资源。下面是一个简单的HTML页面代码,其中的page1就是一个包含相对URL的链接。

相对URL是不完整的,要获取资源的全部信息,还要依赖称为基础(base)的URL。基础URL通常来自以下地方:

第一,在资源中显示提供。在HTML文档中,可以由标签定义一个基础URL。下面的代码定义了<base href=”http://www.demo.com/base/“ > ,于是,page2实际的绝对地址就是:http://www.demo.com/base/page2.html。

第二,所属资源的URL作为基础URL。还是以上面的代码为例。假定page1.html的绝对URL是http://www.demo.com/page1.html,那么page1.html属于http://www.demo.com/资源下的。直接用这个地址作为基础URL,则page2的绝对URL是:http://www.demo.com/page2.html。

第三,没有基础URL。注:这里没有看懂,先列在这里……

#URL编码

合格的URL应该满足下面的要求:

  • 可移植性:作为统一的命名,应该要能够通过不同的协议来传送资源。不同的协议可能会有特定的保留字符,在不同的协议中传输时,不应该因为这些特殊字符而丢失信息。
  • 可读性:不可见的、不可打印的(比如空格)字符不应该出现在URL中。
  • 完整性:可能需要通用字符外的二进制数据或字符,因此需要一种转义机制,将不安全的字符编码为安全字符。

从历史来看,计算机应用程序都是用US-ASCII字符集^footnote。由于其历史悠久,所以可移植性很好。但是它不支持数百种非罗马语言中的字符。这就需要一套转义编码机制,用US-ASCII字符集来对任意字符进行编码。目前设计的转义表示法是用一个“%”,后面跟着两个表示ASCII码的十六进制数。下面是一些编码示例:

从历史来看,计算机应用程序都是用US-ASCII字符集^footnote。由于其历史悠久,所以可移植性很好。但是它不支持数百种非罗马语言中的字符。这就需要一套转义编码机制,用US-ASCII字符集来对任意字符进行编码。目前设计的转义表示法是用一个“%”,后面跟着两个表示ASCII码的十六进制数。下面是一些编码示例:

#总结
本文是《HTTP权威指南》学习笔记的第一篇,介绍一些HTTP中的基本概念和概述。重点介绍了URL(统一资源定位符)这一种最重要的Web资源命名方式,将其与URI、URN这类经常混淆的概念进行的比较。概要说明了URL的语法格式、相对URL和URL编码。首先对HTTP有一个整体上的认识,接下来要写的是HTTP中的重要细节内容,与日常的开发密切相关。

1…56
Airthink

Airthink

The Pursuit of Happyness

58 日志
20 分类
24 标签
GitHub
© 2018 - 2022 Airthink