DevOps-Jenkins

Jenkins简介

Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件项目可以进行持续集成。

什么是持续集成(CI)?

CI(Continuous integration,中文意思是持续集成)是一种软件开发实践。持续集成强调开发人员提交了新代码之后,立刻进行构建、编译、(单元)测试等这个过程,每次提交新代码都要进行此类重复操作,为了提高工作效率,避免重复工作及重复工作导致差别化问题。

什么是持续部署(CD)?

CD(Continuous Delivery, 中文意思持续交付)是在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境中,也就是说它是CI后紧跟的一个环节,当代码审核完毕打包成成品后,需要部署到真实的环境中去,这个过程也会根据代码的跟新持续的进行。

Jenkins安装

采用docker形式安装,首先安装好docker服务,然后在安装机器中对要存储Jenkins数据的目录进行赋权:

# groupadd jenkins -g 1000 && useradd jenkins -u 1000 -g jenkins
# chown -R 1000:1000 /jenkins

uid 和 gid必须是1000,和Jenkins镜像中的对应

开始安装:

# docker network create jenkins

# docker run \
--name jenkins-docker \
--detach \
--privileged \
--network jenkins \
--network-alias docker \
--env DOCKER_TLS_CERTDIR=/certs \
--volume /jenkins/certs:/certs/client \
--volume /jenkins/data:/var/jenkins_home \
--publish 8080:8080 \
--publish 50000:50000 \
jenkins/jenkins:latest ## 执行此命令获取初始密码
# docker exec -it jenkins-docker cat /var/jenkins_home/secrets/initialAdminPassword

这个时候就可以通过 serverip:8080 来访问到Jenkins。

流水线(pipeline)概括

想要更好的使用Jenkins,必须掌握其流水线(pipeline)的使用。

流水线概述

  • 默认文件名为 Jenkinsfile
  • 采用 Groovy 语法
  • 其可以实现对代码的整合、编译、质量检测或部署等一系列操作,相当于我们说的“脚本”

流水线脚本的分类包括:脚本式、声明式

  • 脚本式语法结构

    node {
    stage('Example') {
    try {
    sh 'exit 1'
    }
    catch (exc) {
    echo 'Something failed, I should sound the klaxons!'
    throw
    }
    }
    }
  • 声明式语法结构

    pipeline {
    agent any
    stages {
    stage('Example') {
    steps {
    echo 'Hello World' script {
    def browsers = ['chrome', 'firefox']
    for (int i = 0; i < browsers.size(); ++i) {
    echo "Testing the ${browsers[i]} browser"
    }
    }
    }
    }
    }
    }

我们应该用哪种方式来编写 Jenkinsfile?

推荐使用声明式语法结构来编写 Jenkinsfile,声明式语法结构的灵活性及可读性都要比脚本式语法结构要高,并且声明式结构中可以包含脚本式结构,可以说更强大一些,并且便于学习。

如何快速上手Jenkinsfile?

  1. 多看Jenkinsfile示例文件,及官方的语法介绍:Pipeline Syntax (jenkins.io),对语法有一个基础掌握。
  2. 学会使用脚本生成器

声明式常用语法介绍

  1. 最外层的pipeline,整条流水线的开始,里边定义了流水线所有内容

    pipeline{
    
    }
  2. agent,指定了流水线的执行节点,可以在pipeline的下行指定,也可以在stages的下行指定,agent有四个可选参数:

    • any:在任意节点执行pipeline
    • none:未定义,当顶层的agent none时,必须在每个stage下再单独定义agent
    • label:指定运行节点的label
    • node:自定义运行节点配置,例如指定label或customWorkspace,agent { node { label 'labelName' } } 和 agent { label 'labelName' }相同, 但 node支持其他选项 (比如customWorkspace).
    pipeline{
    agent{
    node{
    label "container"
    customWorkspace '/some/other/path' //指定自定义的工作空间,其实就是指定自定义路径
    }
    }
    }
  3. stages(阶段):包含一个或多个stage的序列,大部分执行操作都在这里,将每个离散部分串接起来,比如构建、测试和部署。

  4. stage:包含在stages中,pipeline完成的所有实际工作都需要包含到stage中,每一个阶段都要有一个名字。

  5. steps(步骤):1个stage中,一般都要运行1个steps,里边可以直接运行linux命令。相当于3-5点是一个连续的、互相关联的,例如:

    pipeline {
    agent any
    stages {
    stage('Example') {
    agent { //选择执行节点,可以在此层级指定
    label 'nginx'
    }
    steps { //执行的步骤
    echo 'Hello World'
    }
    }
    }
    }
  6. post:定义Pipeline或stage运行结束时的操作有以下多个参数可选:

    • always:无论Pipeline运行的完成状态如何都会运行

    • changed:只有当前Pipeline或steps运行的状态与先前完成的Pipeline的状态不同时,才能运行

    • failure:仅当当前Pipeline或steps处于“失败”状态时才运行

    • success:仅当当前Pipeline或steps具有“成功”状态时才运行

    • unstable:只有当前Pipeline或steps具有“不稳定”状态才能运行

    • aborted:只有当前Pipeline或steps处于“中止”状态时才能运行

    • unsuccessful:只有当前Pipeline或steps运行为“未成功”状态才能运行

    • cleanup:无论Pipeline或steps运行后为什么状态,只要前边定义的状态都没匹配到,则运行

    示例:

    pipeline {
    agent any
    stages {
    stage('Example') {
    steps {
    echo 'Hello World'
    }
    }
    }
    post {
    always {
    echo 'I will always say Hello again!'
    }
    }
    }
  7. environment:环境变量,可以定义全局变量和特定的阶段变量,取决于其在流水线的位置,该指定支持一个特殊的方法credentials(),此方法可用于在Jenkins环境中通过标识符访问预定义的凭证,其有很多种类型:

    • Secret Text:指定的环境变量将设置为密文内容
    • Secret File:指定的环境变量将设置为临时创建的文件的位置
    • Username and password:指定的环境变量将设置为username:password,并将自动定义为两个环境变量:MYVARNAME_USR和MYVARNAME_PSW
    • SSH with Private Key:指定的环境变量将设置为临时创建的SSH密钥文件的位置,并且可以自动定义为两个环境变量:MYVARNAME_USR和MYVARNAME_PSW

    示例1:

    pipeline {
    agent any
    environment {
    CC = 'clang'
    }
    stages {
    stage('Example Secret Text') {
    environment {
    // 需要提前将凭据创建好Secret Text类型的凭据
    AN_ACCESS_KEY = credentials('my-predefined-secret-text')
    }
    steps {
    sh 'printenv'
    }
    }
    }
    }

    示例2:

    pipeline {
    agent any
    stages {
    stage('Example Username/Password') {
    environment {
    // 提前创建好Username and Password类型的凭据
    SERVICE_CREDS = credentials('my-predefined-username-password')
    }
    steps {
    sh 'echo "Service user is $SERVICE_CREDS_USR"'
    sh 'echo "Service password is $SERVICE_CREDS_PSW"'
    }
    }
    stage('Example SSH Username with private key') {
    environment {
    // 提前创建好SSH Username with private key类型的凭据
    SSH_CREDS = credentials('my-predefined-ssh-creds')
    }
    steps {
    sh 'echo "SSH private key is located at $SSH_CREDS"'
    sh 'echo "SSH user is $SSH_CREDS_USR"'
    sh 'echo "SSH passphrase is $SSH_CREDS_PSW"'
    }
    }
    }
    }
  8. options:此指令允许从Pipeline本身中配置一些特定的选项。Pipeline提供了许多这样的选项,例如buildDiscarder,也可以由插件提供,例如timestamps。分享一些参数:

    • buildDiscarder:保持构建的最大个数,示例:options { buildDiscarder(logRotator(numToKeepStr: '1')) }

    • checkoutToSubdirectory:在workspace的子目录中执行源代码自动管理切换,示例:options { checkoutToSubdirectory('foo') }

    • disableConcurrentBuilds:不允许同时执行管道。可用于防止同时访问共享资源等,示例:options { disableConcurrentBuilds() }

    • disableResume:如果控制器重新启动,则不允许恢复pipeline,示例: options { disableResume() }

    • newContainerPerStage:使用docker或dockerfile agent时。每个stage将在同一节点上的新容器实例中运行,而不是在同一容器实例中运行的所有stages。

    • overrideIndexTriggers:

    • retry:失败时,按指定次数重试整个管道,可以针对Pipeline或者Stage,示例: options { retry(3) }

    • skipStagesAfterUnstable:一旦构建状态变得不稳定,就跳过各个stages。示例: options { skipStagesAfterUnstable() }

    • timeout:设置管道运行的超时时间,此参数可以针对Pipeline或者Stage,例如:options { timeout(time: 1, unit: 'HOURS') }

    • timestamps: 所有控制台输出前加上线路发出的时间,此参数可以针对Pipeline或者Stage,示例:options { timestamps() }

      pipeline {
      agent any
      options {
      timeout(time: 1, unit: 'HOURS')
      timestamps()
      buildDiscarder(logRotator(numToKeepStr: '10'))
      }
      stages {
      stage('Example') {
      options {
      timeout(time: 1200, unit: 'SECONDS')
      }
      steps {
      echo 'Hello World'
      }
      }
      }
      }
  9. parameters:为Pipeline运行前提供参数,有几种参数类型供选择:

    • string:字符串类型,示例:parameters { string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '') }
    • text:文本类型,可以包含多行,示例: parameters { text(name: 'DEPLOY_TEXT', defaultValue: 'One\nTwo\nThree\n', description: '') }
    • booleanParam:布尔类型,示例: parameters { booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '') }
    • choice:选择类型,多个选项任选其一,示例: parameters { choice(name: 'CHOICES', choices: ['one', 'two', 'three'], description: '') }
    • password:密码类型,可以提前设置密码,示例: parameters { password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'A secret password') }
    pipeline {
    agent any
    parameters {
    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
    text(name: 'BIOGRAPHY', defaultValue: '', description: 'Enter some information about the person')
    booleanParam(name: 'TOGGLE', defaultValue: true, description: 'Toggle this value')
    choice(name: 'CHOICE', choices: ['One', 'Two', 'Three'], description: 'Pick something')
    password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'Enter a password')
    }
    stages {
    stage('Example') {
    steps {
    echo "Hello ${params.PERSON}"
    echo "Biography: ${params.BIOGRAPHY}"
    echo "Toggle: ${params.TOGGLE}"
    echo "Choice: ${params.CHOICE}"
    echo "Password: ${params.PASSWORD}"
    }
    }
    }
    }

    使用parameters参数有一个bug,首次构建时不会让你选择参数,第二次才可以选择。

  10. triggers:触发器,定义了Pipeline自动化触发的方式,可触发的方式有:

    • cron:计划任务定期触发,示例: triggers { cron('H */4 * * 1-5') }

    • pollSCM:与cron方式类似,但是必须发现有源码的变化,才会触发,示例: triggers { pollSCM('H */4 * * 1-5') }

      pollSCM触发器仅在Jenkins 2.22或更高版本中可用。

    • upstream:接受以逗号分隔的作业字符串和阈值。当字符串中的任何作业以最小阈值结束时,将会触发,示例:triggers { upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS) }

    pipeline {
    agent any
    triggers {
    cron('H */4 * * 1-5')
    }
    stages {
    stage('Example') {
    steps {
    echo 'Hello World'
    }
    }
    }
    }

    Jenkins cron syntax

    Jenkins cron语法遵循cron公共的语法(略有不同),每行由5个字段组成,由制表符或空格分隔。

    分钟 小时
    Minutes within the hour (0–59) The hour of the day (0–23) The day of the month (1–31) The month (1–12) The day of the week (0–7) where 0 and 7 are Sunday.

    要为一个字段指定多个值,可以使用以下运算符。按优先顺序展示:

    • * 指定所有的值
    • M-N 指定值得范围
    • M-N/X or */X 在指定的范围或者整个范围内,按照 X的值为间隔步长
    • A,B,…,Z指定多个值

    为了允许定期调度的任务在系统上产生均匀负载,应尽可能使用符号H(表示“哈希”),使其执行pipeline的时间分散,更好的利用资源。

    此外,支持使用 @yearly, @annually, @monthly, @weekly, @daily, @midnight, and @hourly 作为别名,他们都等同于使用哈希自动的进行均衡,例如@hourlyH * * * *相同,表示1小时内任意时间执行即可。

    举一些cron例子:

    1. 每15分钟执行一次,可以是 at :07, :22, :37, :52:triggers{ cron('H/15 * * * *') }
    2. 在每小时的前30分钟,每10分钟执行一次:triggers{ cron('H(0-29)/10 * * * *') }
    3. 每周的周一到周五的,9点-16点之间的第45分钟每两个小时执行一次:triggers{ cron('45 9-16/2 * * 1-5') }
    4. 1-11月的每个1号和15号都要随机执行一次:triggers{ cron('H H 1,15 1-11 *') }
  11. tools:工具,目前仅支持三种工具:maven、jdk和gradle。工具的名称必须在系统设置—>全局工具配置中定义。定义好后即可开始调用:

    pipeline {
    agent any
    tools {
    maven 'apache-maven-3.0.1'
    }
    stages {
    stage('Example') {
    steps {
    sh 'mvn --version'
    }
    }
    }
    }

    第一次运行此工具时,如果没有会去下载,或者会根据你指定的获取方式去获取,比如直接从其他机器打包过来解压等方式。

  12. input:stage 的 input 指令允许你使用 input step提示输入。 在应用了 options 后,进入 stage 的 agent 或评估 when 条件前, stage 将暂停。 如果 input 被批准, stage 将会继续。 作为 input 提交的任何参数都将在环境中用于其他 stage 使用。可选配置:

    • message:必需的。 呈现给用户的信息。
    • id:可选的, 默认为 stage 名称。
    • ok:可选的,表单中 ok 按钮的描述文本。
    • submitter:可选的,以逗号分割的用户列表,只有列表用户才可以提交input内容。
    • parameters:可选参数列表,与前文介绍的相同。

    示例:

    pipeline {
    agent any
    stages {
    stage('Example') {
    input {
    message "Should we continue?"
    ok "Yes, we should."
    submitter "alice,bob"
    parameters {
    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
    }
    }
    steps {
    echo "Hello, ${PERSON}, nice to meet you."
    }
    }
    }
    }
  13. when:逻辑判断,可以给定条件决定要不要执行此stage,when指令必须包含至少一个条件,如果when包含多个条件,则必须所有的条件都返回true,stage才会运行。内置条件有:

    • brance:分支匹配,当正在构建的分支与预设给定的分支匹配时,执行此stage,例如: when { branch 'master' },此参数仅适用于多分支流水线。
    • environment:当指定的环境变量是给定的值时,执行此stage,例如:when { environment name: 'DEPLOY_TO', value: 'production' }
    • expression:当指定的Groovy表达式评估为true时,执行此stage,例如: when { expression { return params.DEBUG_BUILD } }
    • not:当嵌套条件是错误时,执行这个stage,必须包含一个条件,例如: when { not { branch 'master' } }
    • allOf:当所有的嵌套条件都正确时(当when中有多个条件时,默认就是此策略),执行这个此stage,必须包含至少一个条件,例如: when { allOf { branch 'master'; environment name: 'DEPLOY_TO', value: 'production' } }
    • anyOf:当至少有一个嵌套条件为真时,执行这个阶段,必须包含至少一个条件,例如: when { anyOf { branch 'master'; branch 'staging' } }
    • beforeAgent:默认为false,默认情况下,如果在stage中包含agent,则会在执行when之前会先执行agent,如果将beforeAgent设置为true,则表示先执行when条件,when条件判断为执行stage后,才执行agent。

    示例:

    pipeline {
    agent any
    parameters {
    choice(name: 'BRANCH_NAME', choices: ['production', 'staging'], description: 'Pick something')
    string defaultValue: 'production', name: 'DEPLOY_TO', trim: true
    }
    stages {
    stage('Example Build') {
    when {
    expression { BRANCH_NAME ==~ /(production|staging)/ }
    }
    steps {
    echo "Start building ${BRANCH_NAME}"
    }
    }
    stage('Example Deploy') {
    when {
    anyOf {
    environment name: 'DEPLOY_TO', value: 'production'
    environment name: 'DEPLOY_TO', value: 'staging'
    }
    }
    steps {
    echo 'Deploying'
    }
    }
    }
    }
  14. Parallel:并行,声明式流水线的stage可以在他们内部声明多个嵌套stage, 它们将并行执行。注意,一个stage必须只有一个 stepsparallel ,嵌套的stage本身不能再进入 parallel 阶段,任何包含parallelstage不能包含agenttools,因为它们没有相关的step

    另外,可以添加 failFast true 到包含 parallelstage 中,表示当其中一个进程失败时,所有的 parallel 阶段都将被终止。

    示例:

    pipeline {
    agent any
    options {
    parallelsAlwaysFailFast() //表示后续所有的parallel阶段中都设置为failFast true
    timestamps()
    }
    stages {
    stage('Build Stage') {
    steps {
    echo 'This stage is Build.'
    }
    }
    stage('Parallel Stage --> Deploy') {
    when {
    not{
    branch 'prod'
    }
    }
    parallel {
    stage('Branch dev') {
    agent {
    label "dev"
    }
    steps {
    echo "On Branch dev deploy"
    }
    }
    stage('Branch int') {
    agent {
    label "int"
    }
    steps {
    echo "On Branch int deploy"
    }
    }
    stage('Branch test') {
    agent {
    label "test"
    }
    stages {
    stage('Nested 1') {
    steps {
    echo "In stage Nested 1 within Branch test"
    }
    }
    stage('Nested 2') {
    steps {
    echo "In stage Nested 2 within Branch test"
    }
    }
    }
    }
    }
    }
    }
    }

    此示例中:只要当branch不是prod时,则将同时在dev/int/test三个环境中deploy,test环境的deploy又分为两个stage,此两个stage并不是并行执行,而是串行执行。

  15. script:脚本式语法,可以在声明式流水线中使用,包含在step里,调用script { }块,示例如下:

    pipeline {
    agent any
    stages {
    stage('Example') {
    steps {
    echo 'Hello World' script {
    def browsers = ['chrome', 'firefox']
    for (int i = 0; i < browsers.size(); ++i) {
    echo "Testing the ${browsers[i]} browser"
    }
    }
    }
    }
    }
    }

脚本式语法

脚本式pipeline也是建立在底层流水线上的,与声明式不同的是, 脚本化流水线实际上是由 Groovy构建的通用 DSL, Groovy 语言提供的大部分功能都可以用于脚本化流水线。

  1. 脚本化流水线从 Jenkinsfile 的顶部开始向下串行执行, 就像 Groovy 或其他语言中的大多数传统脚本一样。 因此,提供流控制取决于 Groovy 表达式, 比如 if/else 条件, 例如:

    node {
    stage('Example') {
    if (env.BRANCH_NAME == 'master') {
    echo 'I only execute on the master branch'
    } else {
    echo 'I execute elsewhere'
    }
    }
    }
  2. 另一种方法是使用Groovy的异常处理支持来管理脚本化流水线流控制。当 步骤 失败 ,无论什么原因,它们都会抛出一个异常。处理错误的行为必须使用Groovy中的 try/catch/finally 块 , 例如:

    node {
    stage('Example') {
    try {
    sh 'exit 1'
    }
    catch (exc) {
    echo 'Something failed, I should sound the klaxons!'
    throw
    }
    }
    }

devops-2:Jenkins的使用及Pipeline语法讲解的更多相关文章

  1. 6.Jenkins进阶之流水线pipeline语法入门学习(1)

    目录一览: 0x00 前言简述 Pipeline 介绍 Pipeline 基础知识 Pipeline 扩展共享库 BlueOcean 介绍 0x01 Pipeline Syntax (0) Groov ...

  2. 7.Jenkins进阶之流水线pipeline语法入门学习(2)

    目录一览: (2) Declarative Pipeline Syntax 2.1) Sections - 章节 2.2) Directives - 指令 2.3) Sequential Stages ...

  3. Jenkins Pipeline 语法

    Pipeline语法 先讲Declarative Pipeline,所有声明式管道都必须包含在pipeline块中: 123 pipeline { /* insert Declarative Pipe ...

  4. jenkins pipeline语法

    目录 一.声明式 二.脚本式 基本 判断 异常处理 Steps node withEnv 一.声明式 声明式Pipeline必须包含在名为pipeline的语句块中,典型的声明式Pipeline语法如 ...

  5. Jenkins 在声明式 pipeline 中并行执行任务

    在持续集成的过程中,并行的执行那些没有依赖关系的任务可以缩短整个执行过程.Jenkins 的 pipeline 功能支持我们用代码来配置持续集成的过程.本文将介绍在 Jenkins 中使用声明式 pi ...

  6. 【07】Jenkins:流水线(Pipeline)

    写在前面的话 个人认为 Pipeline 在 Jenkins 中算是一个优化性功能,它能够将我们的构建服务的整个过程流程化,这意味着当我们在执行到某一步的时候,可以添加询问,提示我们是否继续运行下一步 ...

  7. GitLab集成Jenkins、Harborn构建pipeline流水线任务

    一.计划 在jenkins中构建流水线任务时,从GitLab当中拉取代码,通过maven打包,然后构建dokcer镜像,并将镜像推送至harbor当中.Jenkins中含开发.测试.生产视图,开发人员 ...

  8. AWS DevOps – 配合Jenkins和CodeDeploy实现代码自动化部署

    AWS DevOps – 配合Jenkins和CodeDeploy实现代码自动化部署 Amazon ElastiCache 连接至 Redis 节点 通过 AWS Command Line Inter ...

  9. pipeline语法之environment,dir(),deleteDir()方法,readJSON,writeJSON

    一 environment指令指定一系列键值对,这些对值将被定义为所有步骤的环境变量或阶段特定步骤 environment{…}, 大括号里面写一些键值对,也就是定义一些变量并赋值,这些变量就是环境变 ...

随机推荐

  1. C语言复习_01

    auto 存储类是所有局部变量默认的存储类,auto 只能用在函数内,即 auto 只能修饰局部变量. { int mount; auto int month; } register存储类用于定义存储 ...

  2. Javabean使用实例

    1.login.jsp <%@ page language="java" contentType="text/html; charset=utf-8" p ...

  3. css页面样式初始化

    为什么? 同一个样式,在各个浏览器的默认样式可能不同,所以需要统一初始化,同一个页面在不同浏览器能正常显示. @charset "utf-8"; /*css reset*/ bod ...

  4. 用Arduino玩GM65二维码扫描模块

    目录 用Arduino玩GM65二维码扫描模块 用Arduino玩GM65二维码扫描模块 最近在做Capstone,内容是我们之前实验室参加过的工程训练的物流搬运小车,所以现在来复盘一下我使用Ardu ...

  5. Eclipse For Java开发环境部署

    Eclipse For Java开发环境部署 1.准备工作 jdk安装包 jdk官网下载 Eclipse安装包 Eclipse官网下载 Eclipse下载时选择图中所示的国内镜像地址下载 下载后的文件 ...

  6. AcWing-1022

    题解借鉴两位大佬的解析 墨染空 && 野生铅笔 本题是一道 01背包 的扩展题 -- 二维费用01背包问题 把 野生宝可梦 看做物品,则捕捉他需要的 精灵球 个数就是第一费用,战斗皮神 ...

  7. Java使用FreeMarker模版技术动态生成word实践

    一.序言 在日常开发中,常常有动态word文件生成的需求,通过编制模版,然后动态修改word内容以组合成新的文件.报告单.请假单.发票页等都可以使用动态生成word来解决. 笔者总结归纳出通用技术要点 ...

  8. Javaweb-Servlet学习

    1.Servlet简介 Servlet就是sun公司开发动态web的一门技术 Sun在这些API中提供一个借口叫做:Servlet,如果你想开发一个Servlet程序,只需要完成两个小步骤: 编写一个 ...

  9. 机械硬盘和ssd固态硬盘的原理对比分析

    固态硬盘和机械硬盘的区别 机械硬盘 磁头是不是直接和盘片接触的呢 磁盘中有几个盘片 机械硬盘的工作原理 固态硬盘的寻址方式 SMR叠瓦式真的比PMR优秀吗 固态硬盘 主控芯片 闪存颗粒 缓存单元 固态 ...

  10. cookie 案例 记住上一次的访问时间

    需求:记住上一次的访问时间 第一次访问Servlet 提示 欢迎首次访问 之后的访问 都提示 您上次的访问时间为:"""""""& ...