能力说明:
精通JVM运行机制,包括类生命、内存模型、垃圾回收及JVM常见参数;能够熟练使用Runnable接口创建线程和使用ExecutorService并发执行任务、识别潜在的死锁线程问题;能够使用Synchronized关键字和atomic包控制线程的执行顺序,使用并行Fork/Join框架;能过开发使用原始版本函数式接口的代码。
能力说明:
熟练掌握Linux常用命令、文件及用户管理、文本处理、Vim工具使用等,熟练掌握企业IP规划、子网划分、Linux的路由、网卡、以及其他企业级网络配置技术,可进行Web服务器(Nginx),以及数据库(My SQL)的搭建、配置、应用,可根据需求编写Shell脚本,通过常用工具进行linux服务器自动化运维。
能力说明:
掌握Java开发环境下所需的MySQL高级技巧,包括索引策略、innodb和myisam存储引擎,熟悉MySQL锁机制,能熟练配置MySQL主从复制,熟练掌握日常SQL诊断和性能分析工具和策略。可对云数据库进行备份恢复与监控、安全策略的设置,并可对云数据库进行性能优化。掌握主要NOSQL数据库的应用技术。
暂时未有相关云产品技术能力~
一、什么是函数计算?官方是这么写的:"函数计算是事件驱动的全托管计算服务。使用函数计算,您无需采购与管理服务器等基础设施,只需编写并上传代码。函数计算为您准备好计算资源,弹性地、可靠地运行任务,并提供日志查询、性能监控和报警等功能。借助函数计算,您可以快速构建任何类型的应用和服务,并且只需为任务实际消耗的资源付费。"看一遍没明白咋回事,下面有个图,看了就明白了。流程说明如下:开发者使用编程语言编写应用和服务。函数计算支持的开发语言,请参见代码开发概述。开发者上传应用到函数计算。上传途径包括:(推荐)通过函数计算控制台上传。(推荐)通过Serverless Devs上传。通过API或SDK上传。更多信息,请参见SDK列表。触发函数执行。触发方式包括事件触发和函数计算调用API触发。动态扩缩容以响应请求。函数计算可以根据用户请求量自动扩缩容,该过程对您和您的用户均透明无感知。根据函数的实际执行时间按量计费。函数执行结束后,可以通过账单来查看函数执行产生的费用,收费粒度精确到1毫秒。更多信息,请参见实例类型及使用模式。二、使用流程使用函数计算前,您需要在产品详情页开通服务。函数计算使用流程图如下所示。流程说明如下:创建服务。创建函数,编写代码,将应用部署到函数中。触发函数。查看执行日志。查看服务监控。创建服务服务(Service)是函数计算的基本资源单位。您可以在服务级别上授权、配置日志和创建函数等。更多信息,请参见管理服务。创建函数函数(Function)是调度与运行的基本单位,更是一段代码的处理逻辑。您需要根据函数计算提供的函数接口形式编写代码,并将代码以函数的形式部署到函数计算。函数计算中的服务对应于软件应用架构领域中的微服务。在函数计算平台构建应用时,首先根据需求将业务逻辑抽象为微服务,然后再实现为函数计算中的服务。一个服务下可以创建多个函数,每个函数可以设置不同的内存规格、环境变量等属性,并可以结合您的实际业务场景来决定是否开启Initializer功能。更多信息,请参见Initializer函数。服务是函数层次化的抽象,在系统抽象和实现灵活度上能够取得平衡。例如,实现一个微服务,需要调用阿里云语音合成服务,将文字转成语音,再把这段语音和一系列图片组合为视频。其中文字转语音函数是调用其他服务,可以设置很小的内存规格。而视频合成函数是计算密集型,需要更大的内存。因此您可以组合多个不同规格的函数实现微服务,优化成本。关于函数的创建、更新和删除等,请参见管理函数。触发函数函数计算支持直接触发函数或通过事件触发函数。您可以根据需要选择合适的触发方式:使用函数计算控制台、Serverless Devs或SDK等方式直接触发函数的执行。更多信息,请参见以下文档:使用控制台创建函数使用s invoke的相关命令调用函数使用SDK执行HTTP触发器函数配置函数计算触发器,通过事件触发函数的执行。例如配置OSS触发器后,当OSS对应的Bucket中有对象新增或删除后都会触发函数的执行,方便您处理上传的对象;配置日志服务触发器后,当日志服务对应的Logstore中有新日志写入后可以触发函数的执行,方便您处理写入的日志。您需要设置触发器来设置事件触发的方式。更多信息,请参见以下文档:触发器简介触发器管理查看执行日志查看日志是帮助您调试的一个重要环节。关于使用函数计算配置日志并查看日志的操作步骤,请参见配置日志。查看服务监控您可以在函数计算控制台上查看服务监控。更多信息,请参见以下文档:监控指标监控数据
前言相信大多数人都有自己搭建博客网站的想法,本文就手把手一步一步的进行,最终结果类似如下样式:一、网站软件的选择软件选择成熟而免费的WordPress,WordPress是一款能让您建立出色网站、博客或应用程序的开源软件。可充分利用超过55,000个插件扩展WordPress,以让您的网站满足需求。您可以增加网店、相册、邮件列表、论坛、统计分析等。当然也有非常多的网站模板可用。二、网站空间的选择建网站,首先得有一个网上的空间,用于存放自己的网站。现在各种云都有相应的服务,一年也没多少钱,现在一般都有活动,如果是新用户建议买三年的。1. 建议选择有名、大厂的我用的阿里云的ECS,也推荐你试试,阿里云也有“试用装”:ECS免费试用_ECS试用中心_云服务器ECS_免费试用-阿里云 (aliyun.com)个人的两核4G基本够用了,正好试用的列表里也有,试用一下:目前各种云挺多,选择哪家呢,我目前用的阿里云的,为什么呢?2. 上手容易,丰富而详实的文档3. 要稳定,少出问题所以一定要选择几个大厂之一的,名声在外不易出问题。4. 及时处理并有能力处理问题这是非常重要的,出了问题的处理有足够的技术能力处理,并且处理及时。不止是ECS的使用,还包括其他包括域名备案等一系列问题。例如我提交的一些工单都能很快的响应并给出处理方案。三、 环境准备WordPress,官网地址:https://cn.wordpress.orgPHP 7.4或更高版本MySQL 5.6或更高版本,或MariaDB 10.1或更高版本Nginx或带mod_rewrite模块的ApacheHTTPS支持四、 安装PHP81. 首先更新一下dnf upgrade2. 查看当前dnf库中的php版本dnf list php我这里看到的是7.2版,版本过低,php.x86_64 7.2.24-1.module_el8.2.0+313+b04d0a66 appstream需要安装个新的。3. 首先安装Remi存储库dnf install -y https://rpms.remirepo.net/enterprise/remi-release-8.rpmremi-release-8.rpm 0 kB/s | 26 kB 00:01 依赖关系解决。 ===================================================================================== 软件包 架构 版本 仓库 大小 ===================================================================================== 安装: remi-release noarch 8.4-1.el8.remi @commandline 26 k 安装依赖关系: epel-release noarch 8-11.el8 extras 24 k会自动安装依赖epel-release,无需单独安装。4. 列出PHP modulednf module list php结果类似如下:CentOS Linux 8 - AppStream Name Stream Profiles Summary php 7.2 [d] common [d], devel, minimal PHP scripting language php 7.3 common [d], devel, minimal PHP scripting language php 7.4 common [d], devel, minimal PHP scripting language Remi's Modular repository for Enterprise Linux 8 - x86_64 Name Stream Profiles Summary php remi-7.2 common [d], devel, minimal PHP scripting language php remi-7.3 common [d], devel, minimal PHP scripting language php remi-7.4 common [d], devel, minimal PHP scripting language php remi-8.0 common [d], devel, minimal PHP scripting language php remi-8.1 common [d], devel, minimal PHP scripting language可以看到已经有了8.0和8.1版本,官网现在8.1还是RC版,准备安装8.0。期间会询问导入各种公钥,选择“y”即可。5. 安装php 8.0dnf module enable php:remi-8.0 dnf install php=================================================================================== 软件包 架构 版本 仓库 大小 =================================================================================== 安装: php x86_64 8.0.12-1.el8.remi remi-modular 1.6 M 安装依赖关系: apr x86_64 1.6.3-11.el8 AppStream 125 k apr-util x86_64 1.6.1-6.el8 AppStream 105 k centos-logos-httpd noarch 85.8-1.el8 base 75 k httpd x86_64 2.4.37-39.module_el8 AppStream 1.4 M httpd-filesystem noarch 2.4.37-39.module_el8 AppStream 39 k httpd-tools x86_64 2.4.37-39.module_el8 AppStream 106 k libsodium x86_64 1.0.18-2.el8 epel 162 k libxslt x86_64 1.1.32-6.el8 base 250 k mailcap noarch 2.1.48-3.el8 base 39 k mod_http2 x86_64 1.15.7-3.module_el8 AppStream 154 k oniguruma5php x86_64 6.9.7.1-1.el8.remi remi-safe 210 k php-common x86_64 8.0.12-1.el8.remi remi-modular 1.2 M 安装弱的依赖: apr-util-bdb x86_64 1.6.1-6.el8 AppStream 25 k apr-util-openssl x86_64 1.6.1-6.el8 AppStream 27 k nginx-filesystem noarch 1:1.14.1-9.module_el8 AppStream 24 k php-cli x86_64 8.0.12-1.el8.remi remi-modular 4.7 M php-fpm x86_64 8.0.12-1.el8.remi remi-modular 1.6 M php-mbstring x86_64 8.0.12-1.el8.remi remi-modular 525 k php-opcache x86_64 8.0.12-1.el8.remi remi-modular 768 k php-pdo x86_64 8.0.12-1.el8.remi remi-modular 156 k php-sodium x86_64 8.0.12-1.el8.remi remi-modular 94 k php-xml x86_64 8.0.12-1.el8.remi remi-modular 238 k 启用模块流: httpd 2.4 nginx 1.14 事务概要 ================================================================================ 安装 23 软件包选择y,等待安装完成。6. 安装php-mysql扩展逐一执行下面命令安装扩展dnf install php-mysql dnf install php-gd dnf install php-imagick dnf install php-zip7. 验证安装输入命令查看php版本: php -vPHP 8.0.12 (cli) (built: Oct 19 2021 10:34:32) ( NTS gcc x86_64 ) Copyright (c) The PHP Group Zend Engine v4.0.12, Copyright (c) Zend Technologies with Zend OPcache v8.0.12, Copyright (c), by Zend Technologies五、 安装mysql8.01. 查看dnf库中mysql版本dnf list mysqlmysql.x86_64 8.0.26-1.module_el8.4.0+915+de215114 appstream版本8.0,直接安装。2. 安装Mysql8.0输入安装命令,注意有“@”:dnf install @mysql依赖关系解决。 ======================================================================================== 软件包 架构 版本 仓库 大小 ======================================================================================== 安装组/模块包: mysql-server x86_64 8.0.26-1.module_el8.4.0+915+de215114 AppStream 25 M 安装依赖关系: ...省略.... 安装模块配置档案: mysql/server 启用模块流: perl 5.26 perl-IO-Socket-SSL 2.066 perl-libwww-perl 6.34 事务概要 ======================================================================================== 安装 54 软件包选择y等待安装完成。3.配置启动mysqlsystemctl start mysqld查看运行状态systemctl status mysqld设为开机启动systemctl enable mysqld运行配置向导mysql_secure_installation根据提示进行操作:[root@localhost mysql]# mysql_secure_installation ## 默认已用空密码登录 Securing the MySQL server deployment. Connecting to MySQL using a blank password. ##是否安装密码强度验证模块,看自己需求 VALIDATE PASSWORD COMPONENT can be used to test passwords and improve security. It checks the strength of password and allows the users to set only those passwords which are secure enough. Would you like to setup VALIDATE PASSWORD component? Press y|Y for Yes, any other key for No: no ## 设置root的密码 Please set the password for root here. New password: Re-enter new password: ## 是否删除匿名用户,可删 By default, a MySQL installation has an anonymous user, allowing anyone to log into MySQL without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother. You should remove them before moving into a production environment. Remove anonymous users? (Press y|Y for Yes, any other key for No) : y Success. ## 默认情况下只允许本机访问,是否开启远程访问,按自己需求 Normally, root should only be allowed to connect from 'localhost'. This ensures that someone cannot guess at the root password from the network. Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y Success. ## 是否删除测试数据库,可删 By default, MySQL comes with a database named 'test' that anyone can access. This is also intended only for testing, and should be removed before moving into a production environment. Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y - Dropping test database... Success. - Removing privileges on test database... Success. ## 是否立即重新加载刚才的配置,选择是 Reloading the privilege tables will ensure that all changes made so far will take effect immediately. Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y Success. ## 配置完毕 All done!4. 为WordPress创建数据库登录数据库mysql -uroot -p提示输入密码,输入刚设置的root的密码。创建数据库,名称自己定,例如wordpresscreate database wordpress;查看现有数据库show databases;可以看到新建好的数据库+--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | sys | | wordpress | +--------------------+ 5 rows in set (0.00 sec)输入exit退出。六、 安装Nginx1. 查看dnf库中Nginx版本dnf list nginxnginx.x86_64 1:1.14.1-9.module_el8.0.0+184+e34fea82 appstream版本有点低,安装新版。2. 安装Nginx1.20输入安装命令dnf install http://nginx.org/packages/centos/8/x86_64/RPMS/nginx-1.20.2-1.el8.ngx.x86_64.rpm========================================================================================== 软件包 架构 版本 仓库 大小 ========================================================================================== 安装: nginx x86_64 1:1.20.2-1.el8.ngx @commandline 819 k 事务概要 ========================================================================================== 安装 1 软件包选择y等待安装完成。3. 查看防火墙查看是否已开放80端口,修改防火墙设置。某云需要修改安全组设置。firewall-cmd --query-port=80/tcp若返回no则未开放。开放80端口命令:firewall-cmd --zone=public --add-port=80/tcp --permanent使设置生效: firewall-cmd --reload4. 访问默认网站验证安装启动nginxsystemctl start nginx查看运行状态systemctl status nginx会看到包含“ Active: active (running)”字样的成功提示。设置开机自动启动systemctl enable nginx访问 http://服务器ip, 正常会是如下页面5. 启用php支持修改nginx配置文件cd /etc/nginx/conf.d备份默认的配置文件cp default.conf default.conf.bak修改default.confvi default.conf按"i"键,找到如下代码块,默认是注释状态,去掉#号,并修改为如下配置 # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # location ~ \.php$ { root /usr/share/nginx/html/; fastcgi_pass unix:/run/php-fpm/www.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }fastcgi_pass对应php-fpm的监听配置, 配置文件为: /etc/php-fpm.d/www.conf,可查看验证。; The address on which to accept FastCGI requests. ; Valid syntaxes are: ; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on ; a specific port; ; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on ; a specific port; ; 'port' - to listen on a TCP socket to all addresses ; (IPv6 and IPv4-mapped) on a specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. listen = /run/php-fpm/www.sock设置默认文件为index.php location / { root /usr/share/nginx/html; index index.php index.html index.htm; }修改完毕后,按ESC,输入:wq保存并退出。重启nginx:systemctl restart nginx七、安装WordPress1. 访问网站目录nginx的默认网站目录在/usr/share/nginx/html/,访问并验证一下cd /usr/share/nginx/html/ ll可以看到两个html文件,即默认的“Welcome to nginx!”页面。-rw-r--r--. 1 root root 494 5月 25 09:41 50x.html -rw-r--r--. 1 root root 612 5月 25 09:41 index.html可以删除默认的index.html文件rm -r index.html询问是否删除,输入y回车即可。2. 下载安装包安装下载和解压工具,如果已安装则忽略dnf install wget dnf install tar下载wordpresswget https://cn.wordpress.org/latest-zh_CN.tar.gz下载完成之后,解压tar -zxvf latest-zh_CN.tar.gz拷贝到当前目录cp -R wordpress/* /usr/share/nginx/html/3. 开始安装访问"http://服务器ip/wp-admin/install.php",可以看到如下页面:点击按钮开始配置:输入准备好的数据库相关信息。如果提示无法写入wp-config.php,如下图手动创建wp-config.php文件cd /usr/share/nginx/html/ vi wp-config.php按i键,粘贴网页中给出的文件内容。按ESC,输入:wq回车保存。继续安装,在新页面设置网站的相关信息点击按钮开始安装。安装成功后,访问http://服务器ip即可。管理后台地址:http://服务器ip/wp-admin总结以上就是今天要讲的内容,手把手讲述了一个个人博客网站的搭建,你学会了么。
一、什么是云效? 云效,云原生时代一站式BizDevOps平台,支持公共云、专有云和混合云多种部署形态,通过云原生新技术和研发新模式,助力创新创业和数字化转型企业快速实现研发敏捷和组织敏捷,打造“双敏”组织,实现 10 倍效能提升。1.官方给出的6大核心场景2、云效一站式DevOps工具链二.云效代码管理 Codeup 功能概览基础代码托管:提供企业级代码托管服务,支持企业内部公开(企业内成员可访问)、私有代码库类型;权限管理:数据企业间完全隔离,提供企业、代码组、代码库多级精细化权限管控;代码评审:灵活可配置的代码评审场景支持与合并请求卡点设置;代码检测:开箱即用的代码规范、安全自动化检测;持续集成:无缝连接持续集成流水线,拓展代码检测、构建、部署场景;研发流程:结合需求、测试、构建、部署等产品模块,支持一站式研发流程管控;通知集成:支持通过钉钉、站内信、邮件等方式,通知告警及时触达;可见云效不止事git的功能,还包含了代码评审、持续集成等功能。三、云效流水线 Flow各种覆盖各种工具和技术栈Flow 作为一款企业级的自动化交付流水线,全面覆盖研发场景中涉及的技术栈和工具链。流水线源Flow 支持将业界通用的代码仓库作为流水线的触发源,并且支持通过代码提交、Tag 创建、合并请求等代码仓库事件触发流水线运行。同时,Flow 的流水线也支持通过其他工具直接触发运行,如:自建 Jenkins 服务/Flow 流水线构建工具Flow 作为一款云原生时代的流水线工具,通过容器技术彻底摆脱了对于虚拟机构建环境的依赖,您甚至可以根据您的使用需求,在同一条流水线上使用不同的构建环境。此外,提供了各种语言的容器环境,满足不同的构建使用场景。更多内容可查看构建语言支持。自动化测试为保障企业整体研发流程的质量,Flow 提供代码扫描、 安全扫描和各种自动化测试能力,支持人工测试卡点、自动化验证卡点等多种质量红线,确保业务质量。更多内容可查看代码扫描能力。部署能力Flow 支持不同国家,不同云厂商以及专有云环境发布。无论你的企业使用的虚拟主机 or Kubernetes,都可以使用 Flow 实现轻松发布。另外,通过灰度发布、分批发布的策略,最大限度的避免了不稳定发布对用户的影响, 保障业务交付的稳定。更多内容可查看主机部署和Kubernetes 部署阿里云深度集成Flow 全面对接阿里云 ECS/ACK/ACR/OSS/EDAS/SAE/FC 等多种阿里云云服务,可以通过流水线串联阿里云产品整体使用路径。四、云效应用交付平台 AppStack云效应用交付平台 AppStack 是一款开发者友好的、以应用为核心的云原生应用交付平台,提供应用编排、环境管理、部署运维、资源管理、应用发布等一站式能力,帮助企业建立应用持续交付整体解决方案,加速企业云原生与 DevOps 转型,提升团队研发效能。AppStack 提供以下主要功能应用管理应用编排环境管理部署运维变量管理发布流水线资源管理产品功能五、学习与体验官方提供了一个 学习+体验+抽奖的活动(地址),可以参加一下哦
在阿里云获得了一个4核8G的云桌面,下面来说一下我的使用感受。总体来说,还是很不错的,现在还可以试用,建议试一下。比如以下这些场景:突然远程办公,临时没带电脑,开一台;只是短期需要一个高性能电脑,开一台;有一定的安全要求、外设限制,开一台;长时间的,自己斟酌考虑哈。备注一下:后半部分提了一些问题,因为既然是评测,如果没些问题或建议,怎么好意思说这是一篇评测文。没吹没黑,别打我。本文分为以下几部分:一、创建桌面分享一下如何从头初始化一个云桌面。二、Ubuntu系统遇到的问题先选择了Ubuntu系统,说说使用情况。三、更改为Windows操作系统说说如何更换操作系统。四、使用Windows系统的一些问题和建议换成windows系统再测试一下,提一些问题和建议。五、客户端的使用问题与建议对客户端做个简单的评测,话说装了卸,卸了装,倒腾了5、6次。六、其他建议除了以上情况,提点其他方面的建议,希望越来越好。七、总结评测完,做个简单总结吧。最后说一句,无影的几个相关工作人员都特别给力。一、创建桌面1. 购买首先需要购买,当然也可以参加现在的评测活动。购买页面如下图选择付费类型,如果是评测则选择包年包月。名称自己定义,会显示在链接的时候,图中有样例。选择地域最好选择离自己近的。这里还涉及选择操作系统的问题,选择自己喜欢的操作系统,不过不用纠结,后面还可以改。这里有两个建议,见建议小节。默认状态下,还没有工作区,需要新建一个,如下图,工作区名称就自己定了,地域选择和上一步一致的地域。如果不需要AD,选便捷账号最省事。最终选择好开通时长,购买即可。2. 网络链接默认情况是未开通互联网访问的,需要自行开通,如下图在云桌面菜单中找到互联网访问,点击开通按钮开通地域还是和之前的一致。3. 创建账号之前的行为,相当于是管理员,那么现在需要给云桌面分配具体使用的用户。在云桌面的菜单处找到用户管理菜单,打开如下图点击创建用户按钮输入用户的用户名、邮箱和手机号,创建成功。新用户会提示“未分配桌面”,点击【去分配】,进入下图界面勾选已经建立的桌面并确定即可。此时新用户所在的邮箱会收到登录账号和密码,密码可以自行修改。如下图4. 安装客户端在云桌面总览页面或者用户收到的邮件中,都有客户端下载按钮,点击进入下载界面。无影云桌面客户端下载 (aliyun.com)根据自己所使用的操作系统下载对应的客户端即可,当然也提供了web客户端,我现在用的windows系统,体验一下windows的客户端。下载安装没什么特殊情况,一直下一步即可。安装完成并启动客户端,会自动安装一个插件,如下图安装完插件就进入了登录界面,如下图:工作区比较好理解,如下图我的云桌面地域当时选择的是北京,这里选择即可后面的输入框是什么?用户邮件里有工作区ID的,想起来了。看到工作区ID,懂了么,就是这个,后面填写那一长串数字,真是不好记,填好后下一步,输入邮件中的账号和密码。首次会提示修改密码,正常修改完成即可登录成功。还会再安装插件,继续等待,过一会儿重试发现可以连接了看到ubuntu已安装成功,可以正常使用了。二、 Ubuntu系统遇到的问题1. 系统语言安装问题问题:系统默认语言为英文,想修改为中文,在设置里进行语言安装,勾选中文后安装提示无法下载相关文件,错误信息如下图:这个应该是连接阿里云的服务器下载资源的问题,详细能很快解决。2. 剪切板问题问题:无法复制粘贴文件到无影,但粘贴文本过去是可以的。问题处理:经过与【无影小助手】确认,目前Ubuntu版本暂时还不支持文件的复制粘贴,Windows版本是支持的,可以采用工具进行文件传输。3. 软件安装问题问题:通过图形界面安装失败。在软件商店搜索typora安装,发现安装进度是波动的,比如已经到70%,突然会变成20%、30%这样,进度条也是忽长忽短,但最终安装成功。在腾讯官网下载了QQ,安装一直无法进行,如下图停留在此处一直不动。Ubuntu玩的不怎么好,可能是我哪里配置有问题。4. 升级后无法正常登录问题:升级后无法正常登录。系统版本默认为20.04.1, 通过sudo apt-get upgrade命令进行升级,升级后看到版本已升级到20.04.3,但重启后无法正常登录。输入正确的密码依然无法登录,如下图所示。升级的时候有一些询问yes/no的环节,不知道是不是因为自己某个环节选错了。小提示:安装后需设置root密码安装或修改系统设置,会询问管理员密码,此密码需通过sudo passwd命令设置一下。修改为熟悉的windows系统试一下。三、 更改为Windows操作系统1. 修改镜像登录控制台,在云桌面管理里,找到自己的云桌面,查看它右边的菜单如下:首先需要关机,点击关机后等待关机完成,此时变更镜像按钮变为可用,点击我选择了windows server 2019, 注意红色提示,数据会清空,有重要文件提前备份出来。点击确认变更,根据提示确定即可,此时云桌面列表会提示变更中,继续等到提示为“运行中”。再次通过客户端连接,和之前的操作一样,此时已正常连接到windows server 20192.查看配置查看一下计算机的属性:下载安装宇宙第一IDE试一下网络及计算机响应情况网速基本上恒定在2MB/秒,安装后新建项目等操作,反应速度尚可,和使用实体机没什么差别。正常开发应该没问题。四、使用Windows系统的一些问题和建议整体来说,无影桌面对windows的支持较强,基本上和使用自己的实体机差不多。1.无法更新问题(这可能不算是一个问题)这可能不算是一个问题,或许只是为了整体控制系统的版本,但有点疑惑,如果想升级小版本,包括打补丁怎么处理,没有在控制台和帮助文档中找到方案。目前点击更新会看到如下错误:2.本地磁盘访问建议现在第一次连接进入云桌面,会有如下提示:首先,感觉看了这个提示不是很明白什么意思第二,即使选择了无权限,并勾选了不再询问,也会出现相应的图标显示,如下图建议可以像Windows远程桌面连接的时候,选择设置是否映射磁盘。五、客户端的使用问题与建议客户端功能还是很强大的,反复安装卸载几次后,总结出如下问题和建议。1. 登录时工作区ID输入建议这个登录界面,有如下建议:对于终端用户,如果是IT研发人员,或许上面的输入框(_dir__)还好理解,如果是其他行业的人员看起来可能会有点懵,并且有的用户可能只关注了用户名密码,没有注意到邮件里还有这个。即使明白什么意思,dir后面的一串数字也不好记,需要去查邮件,是否可以使用工作区别名(这涉及到唯一问题)。不过这只需要输入一次,再次登录会默认跳过这一步,直接进入用户名密码的输入界面,所以是个小问题。除非有多高工作区要切换使用?估计很少。2. 建议客户端主界面可以缩小到系统托盘如题,当连接后,希望下图红框内的主界面可以缩小到系统托盘,或者点击关闭按钮时询问是关闭还是到托盘。3. 卸载客户端重新安装连接失败问题装卸了几次客户端,卸载重新安装后,第一次登录并连接桌面会出现如下提示关闭这些提示,再次点击连接(偶遇一次需要退出客户端),就不出现这个问题了,以后也都正常,仅限于卸载重新安装后第一次连接出现这样的情况。4. 端口被禁问题问题出现过程:有一天上午打开云桌面的客户端,客户端一闪就没,通过printscreen抓到一个截图,如下图,双击客户端快捷方式,会打开这样这样一个空白对话框,然后迅速被关掉,给人感觉是一闪就没了。与【无影小助手】反映此问题(话说这位老兄非常给力,响应问题、处理问题非常快而且有耐心),按提示提交日志过去最终诊断结果是55556端口被禁了。原因分析:仔细想我未进行过禁端口操作,查看杀毒软件也没有黑名单记录,想不到如何复现这个问题。后来在另一台电脑上安装客户端,在弹出防火墙问题时明白了出现问题的原因,如下图安装的时候当时采用的是域网络,如上图默认只勾选了域网络并设置了允许访问。后来采用非域网络的时候就出现了上面的问题。期望解决方案期望能再次弹出如上图提示,允许新的网络环境访问。或者在检测到端口五权限访问的时候,弹出对于的提示,可以让用户手动在防火墙允许访问。直接闪退导致用户不知道错误的原因,不知道如何处理。六、其他建议1. 帮助文档细化个人感觉目前的帮助文档还不够细,或者说覆盖的不全,有些问题在帮助文档里找不到。还得去群里麻烦【无影小助理】。2. 可选操作系统建议在选择操作系统的时候,既然有些定位为普通办公而不是服务器,对于Windows系统,应该不只server版,建议增加一些win10、win11等供选择。3. 多显示器设置对于一个码农,多显示器应该是比较常见的,目前无影支持了打印机、U盘等外设,希望将来能支持扩展显示器。4. 问题反馈看到客户端有问题反馈功能,不知道对应的反馈列表在哪,希望可以看到别人提出的反馈或者已处理的反馈,快速搜索自己遇到的问题是否已经有处理方案。七、总结总体来说,无影还是非常棒的,特别是Windows系统,能满足日常办公的需求。提了一些小问题和建议,相信很快就能得到处理,因为:无影小助理更棒,对于反馈的问题响应的非常及时、耐心,而且沟通很顺畅。无影更新很快,能感觉到一直在做优化。
第一天学习了,创建一台ECS实例。首先,远程登陆ECS实例,并部署应用。然后,登陆管理控制台,并对这台ECS实例进行管理操作。第二天,学习了搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。第三天,学习了Alibaba Cloud Linux 3操作系统的ECS实例上安装、配置以及远程访问MySQL数据库。第四天,学习了《通过workbench远程登录ECS,快速搭建Docker环境》及 《从零搭建Spring Boot的Hello World》。第五天,学习了安装WordPress,并快速搭建自己的云上博客。
三、Catalina的启动与停止从图一可以看出,当Catalina收到Bootstrap的启动要求之后,会调用根组件Server的启动方法,Server再调用Service的启动方法,依次类推,这其实就是对组件这棵树的深度优先遍历。Catalina的start方法部分代码如下: public void start() { // 此处省略了部分代码 // Start the new server try { getServer().start(); } catch (LifecycleException e) { log.fatal(sm.getString("catalina.serverStartFail"), e); try { getServer().destroy(); } catch (LifecycleException e1) { log.debug("destroy() failed for failed Server ", e1); } return; } // 此处省略了部分代码 }通过getServer()获取到对应的Server,然后调用其start方法。Server对应的实现类为StandardServer,它没有重写start方法,直接在startInternal方法中写了对应的启动逻辑: @Override protected void startInternal() throws LifecycleException { fireLifecycleEvent(CONFIGURE_START_EVENT, null); setState(LifecycleState.STARTING); globalNamingResources.start(); // Start our defined Services synchronized (servicesLock) { for (Service service : services) { service.start(); } } if (periodicEventDelay > 0) { monitorFuture = getUtilityExecutor().scheduleWithFixedDelay( () -> startPeriodicLifecycleEvent(), 0, 60, TimeUnit.SECONDS); } }核心就是遍历所有的Service,调用每个Service的start方法。对于其他组件也是依次类推,可以看出,上级组件对下级组件的调用是直接调用其start方法,因为这些组件都实现了生命周期接口,这就是组合模式的优点之一。可以把所有组件一视同仁的调用,而不用管其中的实现细节。这也方便了对组件的增减,使系统设计更加弹性、易于扩展。对于组件的停止逻辑也类似,就不逐一介绍了,有兴趣的可以看一下对应的源码。总结到本篇为止,我们整体的了解了Tomcat的组件结构及初始化、启动的流程。而这些都是以概览的方式进行的,并没有对具体组件的功能进行学习。在接下来的章节会对这些组件进行详细介绍。如果想看这些组件的具体代码实现,可以看一下这些接口对应的实现类,位置图下图的core文件夹:图三这个文件夹下存放了一些以Standard开头的实现类,例如StandardServer、StandardService等。图四可见内容还是很多的,有兴趣的可以提前看一下。
一、 组件之间的关联关系图一上一篇有这样一幅图,Bootstrap类相当于是Tomcat的开关机模块,实际操作的是Catalina,而Catalina的核心组件由Server、Service等组成。在前面的章节讲过,Tomcat的Server、Service等这些组件实际上是按照Server.xml文件的配置组合在一起的,包括它们之间的关联关系。以一个XML文档表示的关联关系、Server组件是root,这两点说明了什么?说明这些组件实际上是组成了一个“树”状结构。这样的树状结构,每个节点又都实现了Lifeycle接口,这是典型的【组合模式】。二、 ☆ 组合模式(COMPOSITE)1. 概念与结构组合模式是结构型设计模式之一。将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。-引用自:《设计模式:可复用面向对象软件的基础》组合模式适用于这样的场景:可以由多个元素组成一个大的组件,就像多个Host组成Engine,而多个大小组件又可组成更大的组件,就像Engine和多个Executor组成Service。无论单一元素还是组合组件,都可以认为是一致的。组合模式的结构如下图(引用自:《设计模式:可复用面向对象软件的基础》):图二2. 优点及使用场景优点:定义了包含基本对象和组合对象的类层次结构:基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断的递归下去。在客户代码中,任何用到基本对象的地方都可以使用组合对象。简化客户代码:客户可以一致地使用组合结构和单个对象。通常用户不知道(也不关心)处理的是一个叶节点还是一个组合组件。这就简化了客户代码,因为在定义组合的那些类中不需要写一些充斥着选择语句的函数。使得更容易增加新类型的组件新定义的Composite或Leaf子类自动地与已有的结构和客户代码一起工作,客户程序不需因新的Component类而改变。使你的设计变得更加一般化:可以很容易的增加新组件。 但这也会产生一些问题,那就是很难限制组合中的组件。有时你希望一个组合只能有某些特定的组件。使用组合模式时,你不能依赖类型系统施加这些约束,而必须在运行时刻进行检查。场景:系统的树状菜单、组织或部门的树状关系等。常见的算法:而伴随组合模式常用的算法就是递归,一般可以通过递归方式遍历树状的节点。
四、快速整理打开的应用将鼠标悬停在窗口的"最大化"按钮上,或按 Windows 徽标键 + Z,然后选择对齐布局以优化屏幕空间和工作效率。总结总的来说,WIn11和Win10差别不太大,确实是漂亮一点。至于稳定性,我从预览版用到现在大半年了,感觉也还好。
2 通过命令修改如果觉得这样比较麻烦,那么可以通过命令来完成:Windows+R键,输入cmd,打开命令窗口,输入如下命令,回车:reg add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve提示操作完成,即完成了添加注册表的操作。再次执行如下命令:taskkill /f /im explorer.exe & start explorer.exe即重启explorer,结果如下图:二、触控手势可以在 11 设备的触摸屏上使用这些Windows手势。 若要打开触摸手势,请选择"开始">设置>蓝牙 &设备>触摸> 三指和四指触摸手势,并确保它已打开。注意: 启用触摸手势后,应用中的三指和四指交互可能不起作用。 若要继续在应用中使用这些交互,请关闭此设置。三、触摸板手势在 11 台笔记本电脑的触摸板上Windows手势。 以上手势中,一部分仅适用于精确式触摸板。 若要了解你的笔记本电脑是否具有,请选择"开始**>设置>蓝牙 &** 触摸>设备。
一、修改右键菜单Win11的默认右键菜单是这样的:1 手把手修改Windows+R键,输入regedit,打开注册表管理器:在HKEY_CURRENT_USER\SOFTWARE\CLASSES\CLSID下右键点击新建项“{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}”,如下图:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ExrBAiTy-1642151322714)(https://2i3i.com/wp-content/blogimages/win11/3.png)]右键点击新建的项“{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}”,新建一个名为“”的项,完成后如下图双击右侧的“默认”项,会进入编辑界面:什么都不需要输入,直接点确定按钮即可。注意此步骤不可少。重启explorer.exe按下ctrl+shift+esc打开任务管理器,找到Windows资源管理器,也就是explorer.exe右键选择重启即可。再次看右键菜单,是不是恢复到win10的样子了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7TtQ3TrM-1642151322716)(https://2i3i.com/wp-content/blogimages/win11/7.png)]
四、 总体的调用流程通过init方法的例子,我们可以大概明白Tomcat的这些核心组件之间的初始化流程。其实对于Lifecycle接口的其他生命周期方法也是类似的,启动(start)、停止(stop)、销毁(destory)等方法也是这样从根节点逐级传递到叶子节点的。也同样存在对应的startInternal()、stopInternal()、destroyInternal()方法。当然,并不是每个组件都会重写这些实际的生命周期方法 XXXInternal(), 比如 StandardHost就没有重写 initInternal()方法,但重写了startInternal()方法。 这完全由按照逻辑需要决定,反过来说,这也给后期逻辑扩展预留了位置。所以,对于Tomcat的启动停止等操作,实际上逻辑是这样的:(图四)五、 Bootstrap、Catalina与组件的生命周期方法的对应关系用户对Tomcat发出Start、Stop等指令,Tomcat的Bootstrap中的main方法接到对应的指令参数后,通过反射方式调用Catalina的对应方法,再由Catalina调用Server的对应方法。至此,这个核心组件树由根部开始逐步调用下一级的对应方法,直至叶子节点。用户指令参数、Catalina的方法以及核心组件的生命周期方法的对应关系如下图:(图五)总结通过本文我们总览了核心组件之间的关系以及逐级调用的逻辑。也以此为例学习了一个设计模式:模板方法模式。思考:设计模式之间经常关联使用,在这个例子里,还用到了什么设计模式或者原则呢?
二、☆模板方法模式(TEMPLATE METHOD)定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。– 《设计模式:可复用面向对象软件的基础》结合上一节的例子:由LifecycleBase定义了一个算法骨架,来实现Lifecycle接口的init()方法。这个算法骨架就是模板方法。LifecycleBase类是一个通用的类,所以其中的逻辑只能是通用的逻辑。这些逻辑写在init()方法中,即通过setStateInternal方法触发相应状态的事件等功能逻辑。调用一个抽象方法initInternal(),这个方法交由子类去重写,来实现具体业务逻辑。1. 总结一下模板方法模式适用的场景:一个算法逻辑由多个类实现,不同的类之间的逻辑有通用的也有个性化的。创建一个抽象类,完成算法骨架。抽象类中实现通用逻辑,并调用一个空方法,具体逻辑交由子类实现。继承抽象类的子类实现自身的个性化逻辑。2. 模板方法模式的好处:代码复用,子类不用再写一遍通用的逻辑。定义了算法骨架,对算法实现进行了约束。权限隔离,抽象类和子类可能由不同角色完成,子类的修改不会影响通用逻辑,也就不会影响其他子类。类图如下:(图二)(引自《设计模式:可复用面向对象软件的基础》)这也是非常惯例的一种实现方式,如果一个接口会被多个不同类实现,那么常见的操作就是使用一个类去实现这个接口,在这个实现类中编写通用的方法,并调用需要子类实现的抽象方法。子类直接或间接继承这个抽象类,并根据自身需要实现具体逻辑。三、所有核心组件的Init方法传递继续上一篇的Catalina类的load()方法,此时完成了对Server.xml文件的解析,并将其赋值给了Catalina的server属性。接下来就是调用getServer().init();方法进行初始化。Server组件作为最上层组件,我们已经知道了其初始化是如何进行的,那么其他子组件是如何统一 管理的呢?看一下第一节中StandardServer类的initInternal()方法的代码,在最后一部分通过循环遍历的方式调用了所有Service的init方法。// 初始化定义的 Services for (Service service : services) { service.init(); }同理,由图一可知,Service同样是继承了LifecycleBase类,所以Service和Server的上层通用逻辑是一样的,那么看一下StandardService的initInternal()方法:@Override protected void initInternal() throws LifecycleException { super.initInternal(); //Engine 初始化 if (engine != null) { engine.init(); } // 初始化 Executors for (Executor executor : findExecutors()) { if (executor instanceof JmxEnabled) { ((JmxEnabled) executor).setDomain(getDomain()); } executor.init(); } // 初始化 mapper listener mapperListener.init(); // 初始化 our defined Connectors synchronized (connectorsLock) { for (Connector connector : connectors) { connector.init(); } } }其中进行了Engine、Executors、mapperListener、Connectors的初始化,以此类推,其他的组件的initInternal()方法我们就不逐一研究了,因为此处暂不关心这几个组件的功能,仅关注组件之间的关系和调用逻辑。具体每个组件的功能及处理逻辑在单独学习该组件的时候再进行分析。大概流程如下(只画了部分组件):(图三)
一、Server的初始化在Catalina.createStartDigester()方法中指定了由哪些实现类去实现对应的接口,那么这些组件是如何实现Lifeycle接口的?1. 总览生命周期接口的实现方式下图在上一篇图二的基础上补充了实现逻辑,见下图中的紫色部分(仅用于展示结构关系,未画所有Lifeycle相关组件):(图一)在Lifeycle接口中,定义了初始化(init)、启动(start)、停止(stop)、销毁(destory)、获取当前状态(getState)等方法,从Lifeycle的名字也可以知道,这个接口用于定义对象的生命周期,即生老病死的过程。public interface Lifecycle { public void init() throws LifecycleException; public void start() throws LifecycleException; public void stop() throws LifecycleException; public void destroy() throws LifecycleException; public LifecycleState getState(); public String getStateName(); }2. 通用抽象类直接实现Lifeycle接口的是LifecycleBase类,这是一个抽象类。以其实现init()方法为例: @Override public final synchronized void init() throws LifecycleException { if (!state.equals(LifecycleState.NEW)) { invalidTransition(Lifecycle.BEFORE_INIT_EVENT); } try { // 触发相应状态的事件 setStateInternal(LifecycleState.INITIALIZING, null, false); initInternal(); setStateInternal(LifecycleState.INITIALIZED, null, false); } catch (Throwable t) { handleSubClassException(t, "lifecycleBase.initFail", toString()); } } /** * 子类实现此方法以执行所需的任何实例初始化。 * * @throws LifecycleException If the initialisation fails */ protected abstract void initInternal() throws LifecycleException;在LifecycleBase类中有两个对应的init相关方法,首先init()方法Override父类的方法,通过setStateInternal方法触发相应状态的事件(具体后文描述,不是此处重点),然后调用另一个抽象方法initInternal()。3. 子类的实现逻辑这里预留的initInternal()方法是做什么用的呢,看一下StandardServer类中对此方法的具体实现: @Override protected void initInternal() throws LifecycleException { // 执行父级的逻辑 super.initInternal(); // 初始化 utility executor reconfigureUtilityExecutor(getUtilityThreadsInternal(utilityThreads)); register(utilityExecutor, "type=UtilityExecutor"); // 注册全局字符串缓存注意虽然缓存是全局的,但如果JVM中存在多个服务器(嵌入时可能会发生),那么相同的缓存将以多个名称注册 onameStringCache = register(new StringCache(), "type=StringCache"); // 注册 MBeanFactory MBeanFactory factory = new MBeanFactory(); factory.setContainer(this); onameMBeanFactory = register(factory, "type=MBeanFactory"); // 注册并初始化 naming resources globalNamingResources.init(); //此处省略了加载器相关的代码 // 初始化定义的 Services for (Service service : services) { service.init(); } } 可以看到这里是具体Server相关的代码,也就是说,initInternal()是预留给子类实现的,由子类通过重写此方法来实现自己的个性逻辑。为什么要这样设计呢?这就是模板方法模式。
3. 1 获取所有打卡贴首先关键字搜索,获取打卡贴列表// 获取所有链接的JS string script = @"Array.from(document.getElementsByClassName('user-tabs user-tabs-search')[0].getElementsByClassName('long-text-title')).map(x => ( x.href));"; // 执行JS代码,返回一个JavascriptResponse JavascriptResponse response1 = await browser.EvaluateScriptAsync(script); dynamic arr = response1.Result; // 遍历结果集合,将所有URL存储到一个静态集合中 foreach (dynamic row in arr) { RecordManager.Urls.Add(row.ToString()); }为了直观,弹出一个对话框,将URL显示出来:GetCSDN getCSDN = new GetCSDN(); getCSDN.browser = browser; getCSDN.ShowDialog();这里定义了一个静态类来控制分析操作。 public static class RecordManager { // 是否开始分析 public static bool IsStart { get; set; } = false; // 分析到第几页 public static int Index { get; set; } = -1; // 获取下一页地址 public static string GetNextUrl() { Index = Index + 1; if (Urls.Count == Index) { return ""; } return Urls[Index]; } // URL列表 public static List<string> Urls = new List<string>(); // 获取到的打卡记录列表 public static List<Record> RecordList { get; set; } = new List<Record>(); // 分析完之后的回调函数 public static Action callback; }3.2 逐一打开各个页面并获取结果上一图右边做了个开始按钮,点击将逐一开始分析。系统提供了多种Handler用于浏览器各种操作的处理,这里要分析请求操作,所以用到RequestHandler public class CustomRequestHandler: RequestHandler { protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) { return new CustomResourceRequestHandler(); } }主要是指定了另一个专门用于处理请求的CustomResourceRequestHandler,二者关系如下图,后者才是分析的主角: public class CustomResourceRequestHandler : ResourceRequestHandler { private readonly MemoryStream memoryStream = new MemoryStream(); protected override IResponseFilter GetResourceResponseFilter(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response) { // 将请求响应结果放到MemoryStream中 return new CefSharp.ResponseFilter.StreamResponseFilter(memoryStream); } protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength) { // 只分析打卡列表的请求结果 if (!RecordManager.IsStart || !(request.Url.ToLower().StartsWith("https://bizapi.csdn.net/community-cloud/v1/community/task/list") && request.Method.ToLower().Equals("get"))) { return; } var bytes = memoryStream.ToArray(); string pages = string.Empty; string page = request.Url.Substring(request.Url.IndexOf("page=") + 5, 1); var str = System.Text.Encoding.UTF8.GetString(bytes); JObject obj = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(str); List<Record> list = (List<Record>)obj["data"]["finish"]["list"].ToObject(typeof(List<Record>)); pages = obj["data"]["finish"]["pages"].ToString(); if (list.Count > 0) { RecordManager.RecordList.AddRange(list); } if (pages.Equals(page)) { string url = RecordManager.GetNextUrl(); if (string.IsNullOrEmpty(url)) { RecordManager.callback(); } else { // 休息5秒,再请求下一篇文章 Thread.Sleep(5000); browser.MainFrame.LoadUrl(url); } } else { // 休息5秒,再请求下一页 Thread.Sleep(5000); string js = $"document.getElementsByClassName('number')[{int.Parse( page)}].click();"; browser.MainFrame.ExecuteJavaScriptAsync(js); } } }3.3 展示结果3.4 导出结果讲结果导出到Excel://创建工作薄 var workbook = new HSSFWorkbook(); //创建表 var table = workbook.CreateSheet("data"); int i = 0; RecordManager.RecordList.ForEach(record => { var row = table.CreateRow(i); var cell = row.CreateCell(0); cell.SetCellValue(record.finishTime); var cell1 = row.CreateCell(1); cell1.SetCellValue(record.userName); var cell2 = row.CreateCell(2); cell2.SetCellValue(record.nickName); i++; }); using (var fs = File.OpenWrite(@"d:/test/1.xls")) { workbook.Write(fs); //向打开的这个xls文件中写入mySheet表并保存。 Console.WriteLine("生成成功"); }
第二个就是浏览器组件,可以拖一个到Form中进行配置。具体可配置项比较多,可以参考GitHub中相关的配置文档:https://github.com/cefsharp/CefSharp。在GitHub库中,也提供了相应的Example,如果想快速学习一下,这是非常好的例子。访问一下CSDN,大概如下图:2. 功能体验在Chrome浏览器中,可以通过安装扩展插件进行一些“特殊操作”。比如CSDN的浏览器插件就很强大,可以参考我的另一篇文章:油#猴是什么猴?又一门新的编程语言?卷不动了呀。浏览器插件都这么强大了,直接使用Chromium则能获得更大的主动权。2.1 DevTools要处理网页,怎么能少了DevTools,平时做前端开发,F12键肯定不少按。在这里也依然方便。在上图的地址栏中可以看到我自定义了三个按钮,其中一个就是DevTools,按钮对应的代码也很简单:browser.ShowDevTools();2.2 执行Js语句来个最简单的例子:browser.ExecuteScriptAsync("alert('sdsdd')");2.3 模拟按键有时候通过Js模拟输入、点击按钮等操作比较复杂,因为你不知道实际在页面中输入的时候,页面背地里又有什么操作,所以如果简单的给字段赋值可能是没用的。那么怎么模拟键盘输入呢?比如想输入这样一句话:“没事看看CSDN”string str = "没事看看CSDN"; foreach (char item in str) { browser.GetBrowserHost().SendKeyEvent((int)258u, (int)item, 0); }这些都是基础功能,通过CefSharp,你可以控制请求数据,修改响应结果,发挥你的想象,你还想干什么。下面通过一个简单例子看一下。3. 统计CSDN社区的打卡记录需求分析:每一篇都有如下图这样的几页打卡记录而每个月都有差不多30篇这样的文章,如何快速的统计出来呢?思路就是:读取每一篇打卡贴的链接逐一打卡每一篇打卡贴获取打卡贴第一页的打卡记录逐一翻到下一页,获取各页的打卡记录
自研、掌握核心科技?这我可不敢吹,我老老实实说我用了个Chromium内核组件。为了统计一些数据,一条条复制粘贴肯定是够累的。用爬虫吧,自己还不精通,而且现在好多数据都需要登录才能请求,或者有些需要滑滚动条才显示。比如csdn社区的打卡记录,一个月的如何快速的统计出来呢?我想控制网页的请求我想控制请求结果我想给网页中硬塞点JS我想模拟输入、模拟按键我想自动翻页、拉滚动条、自动抓取数据在Chrome浏览器中,可以通过安装扩展插件进行一些“特殊操作”。比如CSDN的浏览器插件就很强大,可以参考我的另一篇文章:油#猴是什么猴?又一门新的编程语言?卷不动了呀。自己弄个浏览器,将这些都实现。☆☆☆一定要注意,通过自动方式请求,一定要控制频率,我一般每个请求之间都会停顿5秒以上,文明抓数据,不能给别人和自己造成麻烦。☆☆☆曾经号称打破漂亮国垄断的而大火的“hongxin”浏览器,最终被爆出实际是基于Chromium内核。其实我们也可以弄一个。做桌面软件,微软的Winform和Wpf那肯定是很方便的,也有对应的.Net组件方便将Chromium应用在Winform和Wpf程序中。CefSharp 允许您在 .NET 应用程序中嵌入 Chromium。 它是 Marshall A. Greenblatt 围绕 Chromium 嵌入式框架 (CEF) 的轻量级 .NET 包装器。 大约 30% 的绑定是用 C++/CLI 编写的,这里的大部分代码是 C#。 它可以在 C# 或 VB 或任何其他 CLR 语言中使用。 CefSharp 提供 WPF 和 WinForms Web 浏览器控件实现。CefSharp 是 BSD 许可的,因此它可以在专有和免费/开源应用程序中使用。1. 新建项目CefSharp 提供 WPF 和 WinForm支持,所以新建哪种项目都行,当然Wpf的可以做的更加漂亮一些。本例以WinForm为例,新建一个WinForm项目:设置项目名称,例如MyChrome添加CefSharp组件,在Nuget中搜索CefSharp.Winforms, 由于本例是建的.Net core项目, 所以选择CefSharp.Winforms.NETCore, 安装即可在Program.cs中进行初始化: public static int Main(string[] args) { #if ANYCPU CefRuntime.SubscribeAnyCpuAssemblyResolver(); #endif //For Windows 7 and above, best to include relevant app.manifest entries as well Cef.EnableHighDPISupport(); var settings = new CefSettings() { //By default CefSharp will use an in-memory cache, you need to specify a Cache Folder to persist data CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\Cache") }; //Perform dependency check to make sure all relevant resources are in our output directory. Cef.Initialize(settings, performDependencyCheck: true, browserProcessHandler: null); var browser = new BrowserForm(); Application.Run(browser); return 0; }新建一个Form,例如MainForm,进入设计器:在工具箱里可以看到已经有了相应组件:
3.3 HTTP报文样式一个http请求可以分为请求地址和请求方法,请求头和请求体。如下图所示这是已POST请求的例子,请求体中的⑤请求内容是一段JSON,目的是新建一个用户。请求头是由一些key-value组成的集合,常见的元素有请求的长度、③请求内容格式和④期望的返回结果格式等。③请求内容格式注明了当前请求体中的内容的格式,本例是JSON。④期望的返回结果格式,告诉服务器,自己期望返回JSON格式的内容。服务器端如果支持多种格式的返回,例如支持JSON、XML等,这里可以根据客户端的期望返回对应的格式。所以这里只是期望,服务器如果不支持,还是有可能返回其他格式的响应数据。响应结果也分为头和体两部分,大概结构如下图:响应头部主要包括HTTP协议的版本、状态码、响应内容的格式等,响应体一般为响应的具体内容。所以,后端服务器就是实现了接收HTTP请求并进行处理,然后返回相应结果的功能。而后端服务一般由HTTP服务器(IIS、Apache、Tomcat等,Tomcat比较特殊,既实现了HTTP服务器的功能,又实现了servlet容器的功能。)+处理程序组成。3.4 HTTPSHTTPS不是一种新协议。而是 HTTP 通信接口部分用 SSL和 TLS协议代替而已。在上文中,HTTP 直接和 TCP 通信。当使用HTTPS时,中间加了一层SSL,需要先和 SSL通信,再由 SSL和 TCP 通信了。大概的区别如下图:具体HTTPS相关的知识比较多,就不在此赘述了。4.参考资料:《计算机网络》《图解HTTP》
3. TCP/IP协议TCP/IP协议,英文为Transmission Control Protocol/Internet Protocol,是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇,当然本文的重点HTTP协议也在其中, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。所以HTTP协议是TCP/IP协议簇中的一员。3.1 五层的体系结构?四层结构:TCP/IP是一个四层的体系结构,它包含应用层、运输层、网际层和网络接口层,见下图中的b。那么TCP/IP的四层的体系结构和OSI 的七层协议有什么关系呢?OSI 的体系结构的概念清楚、理论也较完整,但它是一个概念模型,相对既复杂又不实用。所以TCP/IP采用了四层结构。不过从实质上讲,TCP/IP只有最上面的三层,因为最下面的网络接口层并没有什么具体内容。五层结构:五层结构又是什么呢?因此在学习计算机网络的原理时往往采取对上面两个协议的折中的办法,即综合OSI和TCP/IP的优点,采用一种只有五层协议的体系结构(下图中的c),这样既简洁又能将概念阐述清楚。有时为了方便,也可把最底下两层称为网络接口层。下图说明了这几种结构的关系:3.2 HTTP协议与TCP/IP协议的关系常见的HTTP服务有Apache、 Nginx 、IIS等,比较通用的机制就是创建一个进程监听80(443)接口,及时发现客户端发来的连接请求。当监听到连接请求并成功建立TCP连接之后,客户端就向服务器发送HTTP请求,服务器解析该请求并返回对应的结果,最后,释放TCP连接。从三次握手的角度说一下HTTP协议与TCP/IP协议的关系:用户在发出一个请求后,HTTP协议首先要和Web服务器建立 TCP连接。这需要使用三次握手。当握手两次之后(即过了一个RTT时间后),客户端就把HTTP请求报文,作为建立TCP连接的第三次握手的数据,发送给HTTP服务器。服务器收到HTTP请求报文后,就把所请求的文档作为响应报文返回给客户端。大概的机制如下图:从图中可以看出,向服务器请求一个文档所需的总时间等于该文档的传输时间(与文档大小成正比)加上两倍的RTT 时间(一个RTT用于连接TCP连接,另一个RTT用于请求和接收文档。)。这也体现了HTTP/1.0的主要缺点,就是每个请求都要有两倍的RTT 的开销。若一个主页上有很多链接的对象(如图片等)需要依次进行链接,那么每一次链接下载都导致2倍RTT的开销。另一种开销就是客户端和服务器每一次建立新的TCP连接都要分配缓存和变量。特别是HTTP服务器往往要同时服务于大量客户的请求,所以这种非持续连接会使HTTP服务器的负担很重。好在浏览器都能够打开5~10个并行的TCP连接,而每一个TCP连接处理客户的一个请求。因此,使用并行TCP连接可以缩短响应时间。从这个痛点出发,HTTP/1.1协议较好地解决了这个问题,它使用了持续连接(persistent connection)。所谓持续连接就是HTTP服务器在发送响应后仍然在一段时间内保持这条连接,使同一个客户(浏览器)和该服务器可以继续在这条连接上传送后续的HTTP请求报文和响应报文。这并不局限于传送同一个页面上链接的文档,而是只要这些文档都在同一个服务器上就行。
1. 简单的请求处理过程用一幅图简单描述一下用户发送请求、服务器接收并处理请求直至返回最终结果的过程。当客户端向Web服务器发送一个请求后,服务器收到HTTP请求,如果是静态网站,则会直接返回对应的请求文件。如果是动态语言的网站,HTTP服务器会将请求经过处理后转给对应的动态语言程序处理,例如Java、C#、Go、PHP和Python等,这些程序根据用户发过来的请求,进行处理后返回结果给HTTP服务器,HTTP服务器再讲结果处理并返回给用户。这里用到的就是我们熟悉的HTTP协议,再看下OSI七层模型。2. 回顾一下OSI七层模型开放式系统互联通信参考模型,即Open System Interconnection Reference Model,缩写为 OSI, 是一种概念模型,由国际标准化组织提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。**注意:**常见的光纤、同轴电缆、双绞线、Rj45接口等传输介质也被称为传输媒体。**但传输媒体并不是在物理层,**而是在物理层的下一层,因为物理层是模型的第一层,所以也称传输媒体为第0层。在传输媒体中传输的是信号,但传输媒体并不知道所传输的信号代表的是什么意思。物理层则因为规定了电气特性,因此能够识别所传输的比特流,这是考试经常出现的问题之一。下图所示为数据DATA从发送端到达接收端的中间处理过程:当发送端需要发送数据(DATA)该接收端时,发送端大概流程:用户操作应用层的应用,产生的发送数据通过应用层的接口进入应用层。从应用层开始,逐级对DATA进行包装,加上对应层的报头,例如图中的AH、PH、SH、TH、NH等。表示层并不“关心”应用层的数据格式,而是把整个应用层递交的数据报看成是一个整体进行封装,即加上表示层的报头(PH),然后递交到会话层。数据链路层除了要添加报头DH外,还会添加一个报尾DT还, 最终的一帧数据。接收端:接收端相当于是对发送过程的逆向,逐级抽丝剥茧,去掉各种报头报尾,最终回到接收端的应用层。应用层对接收到的报文进行处理。感觉HTTP协议和这个七层模型都是用于网络传输,那么二者有什么关系呢?这就必须要说一下TCP/IP协议。
3. 作为方法的参数在JavaScript中,经常会用到类似callback的回调方法,那么箭头函数是不是也可以呢?3.1 JavaScript:let func = (s)=> { console.log(s); }; var showLog = function(str,action){ action(str); } showLog("hello world",func);3.2 Java本例用Consumer代替了第一节中的自定义的Operate接口。其实Consumer就是框架帮我们预定义的泛型接口,避免我们总需自定义一个接口:public static void main(String[] args) { Consumer<String> func = (String s)->{ System.out.println(s); }; showLog("hello world",func); } public static void showLog(String str, Consumer<String> action){ action.accept(str); }3.3 C#本例用Action代替了第一节中的自定义的delegate。其实Action就是框架帮我们预定义的泛型接口,避免我们总需自定义委托:public static void Main(string[] args) { var func = (string s) => { Console.WriteLine(s); }; showLog("hello world", func); } public static void showLog(string str ,Action<string> action) { action(str); }4. 总结总体来说,三种语言的使用方法还是比较类似的。可能是都源于C的原因?其实对于面向对象语言来说,好多都是相通的,个人感觉经常对比一下,有助于加深记忆。另外,如果有机会,学一门风格和自己擅长的开发语言差异比较大的,更有利于对编程语言的了解。最后,圣诞节已经过了,祝大家元旦快乐。ps,你们要求写年终总结了么?
1. 举个简单的栗子:1.1 JavaScript:let func = (s)=> { console.log(s); }; func("hello world");1.2 Java:interface Operate { void doSomething(String str); // void doSomething1(); 不可以有两个方法 } public static void main(String[] args) { Operate func = (String s)->{ System.out.println(s);}; func.doSomething("hello world"); }1.3 C#:var func = (string s)=> { Console.WriteLine(s); }; func("hello world");1.4 分析可以看到,写法非常类似,尤其是Js和C#。 变量func可以被当做一个函数来使用。那么用于承接这个匿名方法的变量实际是什么?JavaScript: 就是一个js中的functionJava: 在例子中,有点容易迷惑,明明是将lambda赋值给了一个接口类型。但最终调用的时候又要调用该接口的doSomething方法。而且这个接口只能有一个对应的方法,多了会报错。Java10中也提供了var关键字,但遗憾的是也不能被用于这样lambda赋值的情况。C#: 实际上是一个委托类型,例如:delegate void doSomething(string str); public static void Main(string[] args) { doSomething func = (string s) => { Console.WriteLine(s); }; func("hello world"); } 这样看和Java有点像了,但定义的仍然是一个方法,而不是一个接口中有一个同样类型的方法。如果在c语言中我们会用一个指向函数的指针。2. 对函数外变量的引用在上一节的例子中,“hello world”是以参数的形式传递到方法中的,那么,是否可以直接引用外部的方法呢?当然是可以的,改造一下上面的例子:2.1 JavaScript:let str = ",圣诞快乐。"; let func = (s)=> { console.log(s + str); str = ",春节快乐。" }; str = ",元旦快乐。" func("hello world"); func("hello world");2.2 Java: interface Operate { void doSomething(String str); // void doSomething1(); 不可以有两个方法 } public static void main(String[] args) { final String str = ",圣诞快乐"; Operate func = (String s)->{ System.out.println(s + str); //str = ",春节快乐。"; }; //str = ",元旦快乐。" func.doSomething("hello world"); }2.3 C#:var str = ",圣诞快乐。"; var func = (string s) => { Console.WriteLine(s + str ); str = ",春节快乐。"; }; str = ",元旦快乐。"; func("hello world"); func("hello world");2.4 分析JavaScript 和C# 的结果是一样的,输出结果为:hello world,元旦快乐。 hello world,春节快乐。可见,在函数执行的时候,会取当时str的值。在函数定义的时候,虽然引用了变量str,但不是此时固定了str的值。在函数中改变了str的值,会改变外部str的值。Java的例子中,要求str是final的才行,所以是无法对str改变的。
对于大多数面向对象语言来说,都会提到一个词,那就是对象的生命周期。同时这也是面试常考的内容。简单来说,生命周期也就是一个对象的创建、初始化、直至销毁的过程。在这期间,如果把创建、初始化这些当做一个个事件,那么在这些事件发生期间,一般会预留供用户自定义的方法,也就是生命周期钩子的函数。看一下vue官方提供的生命周期图:beforeCreate:vue实例初始化之前调用created:vue实例初始化之后调用beforeMount:挂载到DOM树之前调用mounted:挂载到DOM树之后调用beforeUpdate:数据更新之前调用updated:数据更新之后调用beforeDestroy:vue实例销毁之前调用destroyed:vue实例销毁之后调用比如在created的时候,可以向服务器请求数据。但在此时如果对dom进行操作,很可能不会成功。因为在mounted的时候才是挂载到DOM树之后。
3. 按配置创建核心组件daemon.load(args)方法会调用Catalina类d的load(String args[])方法进行参数处理,进而调用load()方法,其代码如下:/** * 创建并初始化一个新的 server 实例. */ public void load() { if (loaded) { return; } loaded = true; long t1 = System.nanoTime(); // 初始化Naming,因为在使用 digester解析server.xml时或许会被使用到 initNaming(); // 解析 server.xml parseServerXml(true); Server s = getServer(); if (s == null) { return; } //将当前对象(catalinaDaemon)赋值给新创建的Server的catalina属性 getServer().setCatalina(this); //将Bootstrap类中初始化的CatalinaHome和CatalinaBase赋值给Server的对应属性 getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile()); getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile()); // Stream 重定向 initStreams(); // 初始化新的Server try { getServer().init(); } catch (LifecycleException e) { if (throwOnInitFailure) { throw new java.lang.Error(e); } else { log.error(sm.getString("catalina.initError"), e); } } // 省略写日志的代码 }3.1 initNaming()方法用于设置附加的环境变量的值。主要涉及key为javax.naming.Context.URL_PKG_PREFIXES和javax.naming.Context.INITIAL_CONTEXT_FACTORY的两个变量。通过调用System.setProperty方法设置这两个Key对应的值。javax.naming.Context.URL_PKG_PREFIXES该常量的值为“java.naming.factory.url.pkgs”。它对应的Property值应该是一个以冒号分隔的包前缀列表,用于创建 URL 上下文工厂的工厂类的类名。在initNaming()方法中,将"org.apache.naming"添加到冒号分隔的列表中。javax.naming.Context.INITIAL_CONTEXT_FACTORY保存环境属性名称的常量,用于指定要使用的初始上下文工厂。 该属性的值应该是将创建初始上下文的工厂类的完全限定类名。 该属性可以在传递给初始上下文构造函数的环境参数、系统属性或应用程序资源文件中指定。在initNaming()方法中,将"org.apache.naming.java.javaURLContextFactory"设置为此Key的值。3.2 parseServerXml方法此方法的作用是解析Server.xml文件,采用的解析工具为Digester。方法里有两个出现非常多的变量generateCode和useGeneratedCode:generateCode:从配置文件生成Tomcat内嵌代码。useGeneratedCode:使用生成的代码替代配置文件。这两个变量可以在启动的时候通过main方法的args参数设置,默认状态下都是false,删掉相关代码,精简后的代码如下:protected void parseServerXml(boolean start) { // 设置对应的配置文件 即Server.xml ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile())); File file = configFile(); File serverXmlLocation = null; String xmlClassName = null; ServerXml serverXml = null; if (serverXml != null) { serverXml.load(this); } else { try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getServerXml()) { // 创建并执行 Digester // createStartDigester 配置解析规则 Digester digester = start ? createStartDigester() : createStopDigester(); InputStream inputStream = resource.getInputStream(); InputSource inputSource = new InputSource(resource.getURI().toURL().toString()); inputSource.setByteStream(inputStream); // 设置当前catalina对象为root节点 digester.push(this); digester.parse(inputSource); } catch (Exception e) { log.warn(sm.getString("catalina.configFail", file.getAbsolutePath()), e); if (file.exists() && !file.canRead()) { log.warn(sm.getString("catalina.incorrectPermissions")); } } } }大概处理流程总结如下:A. 设置并读取Server文件。B. 创建Digester,并调用createStartDigester()方法设置解析规则。注意:设置解析规则的时候,指定了各个XML节点对应的接口的实现类,截取部分createStartDigester()方法中的代码如下// server节点 digester.addObjectCreate("Server","org.apache.catalina.core.StandardServer","className"); digester.addSetProperties("Server"); digester.addSetNext("Server","setServer","org.apache.catalina.Server"); //service节点 digester.addObjectCreate("Server/Service","org.apache.catalina.core.StandardService","className"); digester.addSetProperties("Server/Service"); digester.addSetNext("Server/Service","addService","org.apache.catalina.Service");实现类多以StandardXXX命名,例如StandardServer、StandardService、StandardThreadExecutor等。关系图如下:(图二)C. 设置当前catalina对象为root节点。D. 执行解析操作,此时会根据配置的规则,对应XML的节点创建对应的实现类的实例,注意此时只是执行了构造方法,未进行其他初始化操作。3.3 Server的初始化下一章继续进行getServer().init();方法的源码阅读,看一看都做了哪些操作,由图二所示,这些组件都实现Lifeycle接口,有什么想法?
1. 概述本章来分享一下Tomcat都有哪些核心组件,以及它们之间的关系是什么样的。其实这和我们熟悉的配置文件“server.xml”的结构基本是一致的。 此文件位置为“conf/server.xml”,部署Tomcat的时候经常会修改其中的配置。按照此XML文件的结构画了一副结构图(各组件详细的功能不止于此,随着后期内容的深入会继续丰富这幅图。),大家看着更直观一些:(图一)2. 从main方法开始上文最后讲到,main方法的最后一部分是根据启动Tomcat时传入的args进行判断的语句,若传入的参数为空,则默认为“start".,进入对应的语句块:if (command.equals("start")) { daemon.setAwait(true); daemon.load(args); daemon.start(); if (null == daemon.getServer()) { System.exit(1); } }核心就是调用daemon的setAwait、load和start三个方法,从上一篇我们已经知道,这些方法都会以反射的方式找到catalinaDaemon(对应Catalina类)的对应方法,传入参数进行调用,此处不再重复。setAwait会调用Catalina类的setAwait方法,对await变量赋值,这个变量的具体作用后文会解释。/** * Use await. */ protected boolean await = false; public void setAwait(boolean b) { await = b; } public boolean isAwait() { return await; }下面看一下load方法。
3.1 创建类加载器首先通过initClassLoaders()方法创建了三个类加载器,对应为以下的三个变量赋值:ClassLoader commonLoader = null; ClassLoader catalinaLoader = null; ClassLoader sharedLoader = null;对应的配置在conf/catalina.properties文件中,配置如下common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar" server.loader= shared.loader=common.loader是另外两个类的父级,默认情况下,server.loader和shared.loader未作配置,返回结果同common.loader。3.2 创建并初始化catalinaDaemon在init()的后半部分,通过反射方式根据"org.apache.catalina.startup.Catalina"创建了Catalina对象,并调用其“setParentClassLoader”方法将sharedLoader赋值给它的parentClassLoader属性。为什么要通过反射的方式来进行创建?Bootstrap类的注释已经说明,主要是为了类的隔离。但这也导致了调用Catalina对象的方法也需要通过getMethod的方法获取并调用。4. 控制Tomcat的启动状态main方法的后半部分,通过传进来的arg值来确定执行的操作String command = "start"; if (args.length > 0) { command = args[args.length - 1]; } if (command.equals("startd")) { args[args.length - 1] = "start"; daemon.load(args); daemon.start(); } else if (command.equals("stopd")) { args[args.length - 1] = "stop"; daemon.stop(); } else if (command.equals("start")) { daemon.setAwait(true); daemon.load(args); daemon.start(); if (null == daemon.getServer()) { System.exit(1); } } else if (command.equals("stop")) { daemon.stopServer(args); } else if (command.equals("configtest")) { daemon.load(args); if (null == daemon.getServer()) { System.exit(1); } System.exit(0); } else { log.warn("Bootstrap: command \"" + command + "\" does not exist."); }arg是调用tomcat传的参数,若未传则默认是“start”,根据这个命令来控制tomcat的状态。看起来简单的start、stop方法,背后的逻辑比较复杂,下文单独作为一个专题讲解。
3.按功能看处理流程将Bootstrap类按其代码分为三部分:初始化部分,主要是初始化CATALINA_HOME 和CATALINA_BASE变量;main方法部分一:创建和初始化daemon和catalinaDaemon、创建三个重要类加载器;main方法部分二:控制Tomcat的启动与停止。对应流程图如下:2. 初始化CATALINA_HOME 和CATALINA_BASE首先看一下Bootstrap类,最早会通过一段代码确定CATALINA_HOME 和CATALINA_BASE两个重要值:private static final File catalinaBaseFile; private static final File catalinaHomeFile; private static final Pattern PATH_PATTERN = Pattern.compile("(\"[^\"]*\")|(([^,])*)"); static { // Will always be non-null String userDir = System.getProperty("user.dir"); // Home first String home = System.getProperty(Constants.CATALINA_HOME_PROP); //省略部分代码 catalinaHomeFile = homeFile; System.setProperty(Constants.CATALINA_HOME_PROP, catalinaHomeFile.getPath()); // Then base String base = System.getProperty(Constants.CATALINA_BASE_PROP); if (base == null) { catalinaBaseFile = catalinaHomeFile; } else { File baseFile = new File(base); try { baseFile = baseFile.getCanonicalFile(); } catch (IOException ioe) { baseFile = baseFile.getAbsoluteFile(); } catalinaBaseFile = baseFile; } System.setProperty( Constants.CATALINA_BASE_PROP, catalinaBaseFile.getPath()); }**CATALINA_HOME:**代表Tomcat安装的根目录,例如/home/tomcat/apache-tomcat-9.0.10或C:\Program Files\apache-tomcat-9.0.10。**CATALINA_BASE:**表示特定 Tomcat 实例的运行时配置的根。如果您想在一台机器上拥有多个 Tomcat 实例,请使用 CATALINA_BASE 属性。**默认情况下,CATALINA_HOME 和 CATALINA_BASE 指向同一目录。**如果将属性设置为不同的位置,则 CATALINA_HOME 位置包含静态源,例如 .jar 文件或二进制文件。 CATALINA_BASE 位置包含配置文件、日志文件、部署的应用程序和其他运行时要求。3. 创建并初始化守护进程daemon和catalinaDaemon初始化完毕,接着执行的就是main方法,这个方法功能分为两部分:synchronized (daemonLock) { if (daemon == null) { // 在初始化完成之前,不要对daemon赋值 Bootstrap bootstrap = new Bootstrap(); try { bootstrap.init(); } catch (Throwable t) { handleThrowable(t); t.printStackTrace(); return; } daemon = bootstrap; } else { //当作为服务正在运行时,如果调用停止方法,这将在一个新线程上进行,以确保使用正确的类加载器,防止出现未找到类的异常。 Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); } }首先创建一个Bootstrap类型的对象,并调用其init()方法进行初始化,直至其初始化完毕,将其赋值给daemon对象。重点在init方法:public void init() throws Exception { //创建并初始化三个ClassLoader initClassLoaders(); Thread.currentThread().setContextClassLoader(catalinaLoader); SecurityClassLoad.securityClassLoad(catalinaLoader); // Load our startup class and call its process() method if (log.isDebugEnabled()) { log.debug("Loading startup class"); } //通过反射方式创建 Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.getConstructor().newInstance(); // Set the shared extensions class loader if (log.isDebugEnabled()) { log.debug("Setting startup class properties"); } String methodName = "setParentClassLoader"; Class<?> paramTypes[] = new Class[1]; paramTypes[0] = Class.forName("java.lang.ClassLoader"); Object paramValues[] = new Object[1]; paramValues[0] = sharedLoader; Method method = startupInstance.getClass().getMethod(methodName, paramTypes); method.invoke(startupInstance, paramValues); catalinaDaemon = startupInstance; }
1. 一切从main方法开始以main方法作为程序的起点,这可以说是大多数语言的惯例。上文已介绍了,Tomcat的启动main方法在“java/org/apache/catalina/startup/Bootstrap.java”文件中。那么我们就以这个Bootstrap类作为源码阅读的起点。Bootstrap类的注释翻译如下:Bootstrap类是Catalina 的引导加载程序。该应用程序构建了一个类加载器,用于加载 Catalina 内部类(通过累积在“catalina.home”下的“server”目录中找到的所有 JAR 文件),并启动容器的常规执行。这种迂回方法的目的是将 Catalina 内部类(以及它们所依赖的任何其他类,例如 XML 解析器)保留在系统类路径之外,因此对应用程序级类不可见。这涉及到了此类中的两个重要变量(下文会用到):/** * daemon: main方法使用的守护进程对象. */ private static volatile Bootstrap daemon = null; /** * 守护程序引用的catalina对象。 */ private Object catalinaDaemon = null;一个是Bootstrap类型的守护进程,另一个则是Catalina ,Tomcat的核心。但Tomcat并没有直接创建它,而是通过一个Bootstrap类型的守护程序来创建和初始化Catalina ,并管理其启动、停止等。2. 源码分析心得用快捷键“ctrl+shift+加号”键折叠所有代码,先整体看一下Bootstrap类的方法的层级关系,通过方法名、注释等简要了解方法的作用。简要画一下关系图,类似这样: 以上图为例,代码是逐级细化的,就像看地图一样,首先看到的是整个地球,放大一下,也就是进入了下一个层级,可以看到所有国家。针对某个国家再放大,可以看到相应的省。通过这样先整体后局部的方式把握整体层级架构,然后再按需分析一些主要的方法。在调试的时候,首先尽量少Step Into,了解完本级的大概功能后,再按需求进入子方法阅读。 然后,要明确自己的目的。了解的方法的大概作用后,就要根据自己的目的进行取舍。例如此处的replace方法,已知道它的作用是替换属性中的占位符。如果是想了解框架的关键流程,一些细枝末节的辅助方法就简要过一下就行了,这样的replace方法知道作用就可以跳过了;如果想学习框架的代码技巧、算法、或者验证某功能的实现机制等,可以深入的分析一下,配合逐步调试。按程序的执行流程来说,是对上图这棵树进行深度优先遍历的过程。但从源码分析角度,建议通过广度优先的方式,逐级进行分析。
5. 尝试开始源码阅读之旅从现在起,尝试开始阅读一门语言的基础框架源码,如何开始呢?第一步肯定是下载源码并搭建调试环境,但接下来,面对一个庞大的框架,从哪个类或者方法开始阅读呢?建议按照“事情发展的顺序”来进行,对于一个后端应用来说,主要的功能就是相应客户端的请求。为了避免前端语言的干扰,我们以JSON请求为例,比如以POST方式提交一个JSON到后端服务,服务接收到请求并进行处理,最终将处理结果返回给客户端,基本上就是这样的处理流程。而这个流程一般以HTTP协议的请求为主。5.1 简要回顾一下HTTP相关知识一个请求可以分为请求地址和请求方法,请求头和请求体。如下图所示响应头部主要包括HTTP协议的版本、状态码、响应内容的格式等,响应体一般为响应的具体内容。所以,后端服务器就是实现了接收HTTP请求并进行处理,然后返回相应结果的功能。而后端服务一般由HTTP服务器(IIS、Apache、Tomcat等,Tomcat比较特殊,既实现了HTTP服务器的功能,又实现了servlet容器的功能。)+处理程序组成,所以与计算机网络相关的内容一般由HTTP服务器进行处理,如果不想深入学习这部分的同学可以跳过。那么,我们只需要实现接收到HTTP服务传过来的请求数据并处理返回给HTTP服务器即可。5.2 从哪开始阅读客户端发出的请求就像唐僧取经一样,携带通关文牒(可能存在的Token等)经过层层磨难(多层组件的处理)到达西天取得真经(HTML、JSON、XML等)返回。那么可以将整个故事分为两部分:框架的构建及启动:取经路的构建过程,八十一难不是瞬间构成的。请求的处理流程:请求是都经过了那些环节,被做了哪些验证和处理。对框架源码的阅读也可以从这两条线出发。6. 思考一下从框架设计的角度,先思考一下,都会有哪些组件,组合关系如何,大概会涉及哪些设计原则和设计模式?也可以大概画个流程图。涉及不同请求格式的处理,例如XML、JSON等,那么每种格式就可能会有对应的一个或一组处处理类,对于单独的处理类,会用到工厂模式?对于多个处理类的管理,策略模式?庞大的框架构建工程,建造者模式?各种状态的监听,观察者模式?现在可以选一门目标语言开始学习了,下一篇:如何在Idea中调试最新版本的Tomcat10源代码,以Tomcat为例,开启新的源码阅读。
1.你是一个什么程序员经常听到有人说,我是Java程序员、我是.NET程序员、我是PHP程序员(PHP是世界上最好的语言),现在编程语言越来越多,不说全栈,只算后端开发常用的语言就有好几种。但随着工作年限增长,接触的产品、项目越来越多,“跨语言”的情形经常发生。以后端程序员为例,如何快速的“跨界"到另一门编程语言?工作这些年,做过PHP、.NET、Java这样的后端服务,也做过PB、Delphi、Winform(.NET)这样的桌面软件,以及iOS 的移动端APP,前端相关技术也有一定的了解。个人感觉同类型的技术跨语言学习还是比较容易的,现在通过几篇文章分享一下我对于一门新语言的学习心得。2. 现状现在的情况是,程序员的分工越来越细。除了一些特定场景的软件,比如桌面软件、上位机等,做web相关开发的程序员应该是最多的。虽然现在招人的都想招个“全栈程序员“,但那估计也只能是一厢情愿了。随着涉及到的技术种类和技术方向越来越多,分工反而向着越来越细的方向发展。比如现在流行的“前后端分离”,拆分出来了“前端程序员”和“后端程序员”,“后端程序员”只要专注的给前端提供好API即可,原来的HTML、CSS完全可以抛掉。虽然是拆掉了一大部分需要关注的技术,但后端需要涉猎的技术也在膨胀。以数据库为例,多年前只是一门编程语言加上熟悉一两种数据库即可。但现在数据库除了关系型数据库,还有MongoDB等NoSql数据库,以及现在常用的Redis等。其他需要学习的还有MQ、Docker、Elasticsearch等等相关知识。如下图:其实反过来说,这样的条件下,换一门语言也很容易。因为通过这样的图也可以看到,在目前新技术越来越多的情况下,需要变化的只是复杂的技术版图中的一个小方块。从PHP、Java、.NET这些语言互换,如果只是做后端API,大部分“技术版图”都是不变的。网络、数据结构及算法是基础,数据库、Git、docker、k8s这些又都是支持多种语言的,完全都是通用的。而在这些后端语言内部,大部分都是面向对象语言,那么面向对象的思想、设计模式、SOLID和KISS等法则这些程序设计思想也是通用的,可以说这些程序设计思想相当于“内功”,具体的编程语言是表现形式,也就是“招式”。张无忌:内功深厚,学啥都快。3.简单入门新的语言很容易对于面向对象编程范畴的语言来说,工作过几年了“老鸟”都有这样的体会:学习一门新语言并快速的做一些CRUD的功能很容易。快速的浏览一下新语言的基本语法,网上好多基础教程,找本实体书看看也是个很不错的选择。而且好多语言都脱胎于C/C++, 变量定义、循环和判断语句都或多或少的类似。特别是从Java或C#转到对方来说,那更是太方便了,好多语法都一样,甚至好大一段代码拷过去都不带报错的。这里需要说一下,各个语言的官方文档一般都是学习的好材料。这里需要表扬一下.NET,官方文档这块估计是做的最棒的。着重看一下新语言的一些特殊语法,这样看别人的代码的时候不至于懵掉。网上找一些CRUD的例子、开源代码等,如果是往现有项目中添加功能那就更简单了,照着写几个功能就差不多成为初级选手了。4. 如何提高简单入门容易,如果想继续提高,个人的经验是看一些开源项目,尤其以相应语言的基础开源框架的源码为最好,例如Java相关的Tomcat、Spring Framework,.NET的.NET Core、ASP.NET Core源码,为什么呢?学习“最佳实践”。同样实现一个功能,可能会有N种方式,比如判断一个字符串是否有内容,第一想法是它不为空并且长度大于0。但也会想,这么新语言是否有更优雅的方法,自己这样实现会不会太Low?学习架构思想和设计模式的运用。可以思考一下,如果让自己实现这样的一个框架, 该如何设计?个人认为供人使用的框架类项目更容易体现设计模式的使用,因为一般要考虑对多种场景的支持,易用、易扩展。如果是学习的基础框架,那还有一些额外的好处。比如知道框架的处理逻辑,能准确的选择框架预留的扩展点,添加自己的逻辑。还有就是个性化配置及性能调优,基础框架一般都有一些默认的配置,但对于不同的业务系统场景,例如功能的增减、处理模式的选择、性能相关的比如并发量等等,需要对一些配置进行改变,从而达到更好的效果。
这个Webapps文件夹就是我们平时正常使用Tomcat的时候,用于放置网站的目录。里面现有的文件也是平时安装Tomcat之后存在的默认网站。出问题的原因是这个默认网站也是源代码状态,未编译,所以导致找不到对应的类。如果下载Tomcat的安装文件(64-bit Windows zip),对比一下examples文件夹的内容,如下图,左侧为源代码目录,右侧为安装包中的文件目录,可以看到对右侧编译后的文件。处理这个问题,一种方案是删掉examples文件夹,但这个文件夹其实提供了我们用来调试的样例,想办法利用起来。将右侧的内容拷贝到左侧(文件夹复制,跳过存在的文件),再次运行,问题已解决。4. 源代码调试上文说到webapp里的examples文件夹即默认网站的examples菜单对应内容.examples提供了一些样例,正好可以用于调试Tomcat源码:例如我在CoyoteAdapter的service方法中添加了一个断点点击Hello World后面对应的Execute链接,断点被激活,一个请求的处理就这么开始了。可以根据自己的调试需求去添加断点调试了。
2.2 Build项目点击Build按钮开始构建项目,会提示:“java: 程序包trailers不存在。”和“找不到符号 ResponseTrailers”,两个错误都出现在test文件夹。缺少的文件都在webapps/examples/WEB-INF/classes文件夹,拷贝文件夹“webapps/examples/WEB-INF/classes/trailers”到“test”文件夹下。拷贝文件“webapps/examples/WEB-INF/classes/util/CookieFilter.java”到“test/util”文件夹下。此时再次Build不再出错。3. 排查启动问题找到文件“java/org/apache/catalina/startup/Bootstrap.java”,其中的main方法是Tomcat的启动起点。Run这个方法,启动Tomcat,默认地址为“http://localhost:8080/“。3.1 端口被占用默认会监听8080端口,如图提示被占用,可以去“conf/server.xml”文件中修改: <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />3.2 提示Jsp相关的如下错误修改“java/org/apache/catalina/startup/ContextConfig.java”文件,在configureStart方法中添加一行代码,位置如下面所示: protected synchronized void configureStart() { // 此处省略一部分代码 webConfig(); // 添加下面一行代码 context.addServletContainerInitializer(new JasperInitializer(), null); if (!context.getIgnoreAnnotations()) { applicationAnnotationsConfig(); }再次启动,可以看到熟悉的页面了。3.3 输出窗口提示找不到“XXXListener”可以看到如下错误:和2.2中遇到的错误类似,对应的文件还是存在于“webapps/examples/WEB-INF/classes”文件夹中:而“webapps/examples/WEB-INF/web.xml”中对这几个Listener做了引用。
1. 下载源代码可以到Tomcat官网下载源码压缩包,也可以去GitHub仓库Clone。目前最新的是2021-12-08发布的10.0.14版。1.1 下载压缩包前往网址Tomcat官网:Apache Tomcat® - Apache Tomcat 10 Software Downloads1.2 GitHub下载仓库地址:apache/tomcat: Apache Tomcat (github.com)如果想在代码中做一些注释,可以fork到自己的账号。执行如下命令,下载10.0.14版本:git clone -b 10.0.14 https://github.com/apache/tomcat.git2. 采用Maven方式构建Tomcat默认采用的是Ant方式构建,官网中也对用其他方式做了提示:重要提示:这不是构建 Tomcat 的支持方法; 此信息不提供任何保证:-)。 构建 Tomcat 的唯一支持方法是使用上述 Ant 构建。 但是,一些开发人员喜欢使用 Java IDE 处理 Java 代码。为了方便调试,我们采用Maven方式在Idea中调试源码。2.1 创建pom.xml文件首先在下载的代码根目录创建一个Maven的pom.xml文件,添加需要的依赖:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat10</artifactId> <version>10.0.14</version> <name>tomcat10</name> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> <version>4.3</version> </dependency> <dependency> <groupId>org.apache.ant</groupId> <artifactId>ant</artifactId> <version>1.10.12</version> </dependency> <dependency> <groupId>wsdl4j</groupId> <artifactId>wsdl4j</artifactId> <version>1.6.3</version> </dependency> <dependency> <groupId>javax.xml</groupId> <artifactId>jaxrpc-api</artifactId> <version>1.1</version> </dependency> <dependency> <groupId>org.eclipse.jdt</groupId> <artifactId>org.eclipse.jdt.core</artifactId> <version>3.28.0</version> </dependency> <dependency> <groupId>org.eclipse.jdt</groupId> <artifactId>ecj</artifactId> <version>3.28.0</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>jakartaee-migration</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>biz.aQute.bnd</groupId> <artifactId>biz.aQute.bndlib</artifactId> <version>6.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>6.0.2</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>Tomcat10.0</finalName> <sourceDirectory>java</sourceDirectory> <testSourceDirectory>test</testSourceDirectory> <resources> <resource> <directory>java</directory> </resource> </resources> <testResources> <testResource> <directory>test</directory> </testResource> </testResources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.1</version> <configuration> <encoding>UTF-8</encoding> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project>右键点击代码文件夹,选择用Idea打开。或者再Idea中通过File->New->Project from Sources的方式打开代码文件夹:点击Maven的Reload按钮开始加载依赖:等待加载完成。
3.开始调试点击菜单【调试】->【附加到进程】,打开如下页面① 选择连接类型如果开启了身份验证,则默认即可,否则选择【无身份验证】。② 填写服务器的IP及调试服务的端口填写完后直接回车,不要点击后面的【查找】按钮。在可用进程中会列出服务器端的进程列表。③筛选进程可选,如果服务器端进程较多,可以在此筛选,支持模糊查询。找到HelloWorld.exe,双击改进程或点击附加按钮,和附加本地进程一致。随便输入什么字符,回车,可以看到进入了断点可以正常进行调试了。4. 调试IIS中的网站如果是调试发布到IIS中的网站,则需要附加的进程为w3wp.exe。可能会遇到如下情况:如果看不到该进程,则需考虑如下操作,①访问该网站,休眠状态有时候找不到该进程。②勾选“显示所有用户的进程”,再刷新。如果看到多个w3wp.exe,这是在IIS中部署了多个网站,不知道该附加到哪个上面,可以以管理员权限运行 cmd,执行如下命令:cd %windir%\system32\inetsrv appcmd list wp在执行结果中可以看到网站的名称和对应的进程ID,在附加进程的时候根据进程ID区分。
1. 下载远程调试工具网址:https://visualstudio.microsoft.com/zh-hans/downloads/下载后直接安装至完成。启动Remote Debugger:如果只是短时间测试,可以选择无身份验证,为了安全,工具自动设置了空闲时间,即多久没有操作会自动关闭此功能。2.新建测试项目建了一个简单的HelloWorld项目,简单几句代码,加上断点。发布debug到远程服务器。双击HelloWorld.exe,运行程序
如下图设置,只是用于发布,只给repo权限即可生成token之后,记得拷贝设置token,在gitpages库的设置中,选择secrets,点击new repository secret按钮。在新打开的页面中设置创建的Token,名称设为ACCESS_TOKEN。4.设置Actions,自动部署在gitpages,选择Action菜单,点击set up a workflow yourself链接。默认创建了一个main.yml文件,如下图name: Deploy GitHub Pages # 触发条件:在 push 到 master 分支后 on: push: branches: - master # 任务 jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@master with: persist-credentials: false # - name: Build # run: npm install && npm run build - name: Deploy uses: JamesIves/github-pages-deploy-action@releases/v3 with: ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} BRANCH: gh-pages FOLDER: public BUILD_SCRIPT: npm install && npm run build # The build script the action should run prior to deploying.5. 发布首先clone设置好的repositorygit clone https://github.com/FlyLolo/gitpages.git将上文创建好的VuePress文件拷贝到clone的gitpages文件夹git add . git commit -m "first" git push将文件push到repository。此时在Action菜单中可以看到发布的进度点击箭头所示记录,在新页面中可以看到具体的发布进度。如果发现发布失败,可以在此处查看错误信息。6.一个错误此时访问发布好的页面,可能会有以下错误,样式没有正常加载。当发布到子目录会出现如下问题,在gitpages\docs.vuepress目录下创建config.js,并进行如下设置即可。module.exports = { title: 'Hello VuePress', description: 'Just playing around', base:'/gitpages/' }
默认的主题提供了一个首页(Homepage)的布局 (用于 这个网站的主页)。想要使用它,需要在你的根级 README.md 的 YAML front matter 指定 home: true。以下是一个如何使用的例子:--- home: true heroImage: /hero.png heroText: Hero 标题 tagline: Hero 副标题 actionText: 快速上手 → actionLink: /zh/guide/ features: - title: 简洁至上 details: 以 Markdown 为中心的项目结构,以最少的配置帮助你专注于写作。 - title: Vue驱动 details: 享受 Vue + webpack 的开发体验,在 Markdown 中使用 Vue 组件,同时可以使用 Vue 来开发自定义主题。 - title: 高性能 details: VuePress 为每个页面预渲染生成静态的 HTML,同时在页面被加载的时候,将作为 SPA 运行。 footer: MIT Licensed | Copyright © 2018-present Evan You ---设置完后,可以看到出现了我们想要的页面。其他设置可参考其他帮助页面:https://vuepress.vuejs.org/zh/theme/default-theme-config.html#%E9%A6%96%E9%A1%B53. 发布到GitHub Pages下一步就是将生成的网页发布到GitHub。直观的想法就是把项目build后的文件push到上文创建的Repository。实际上的做法却可以更“巧妙”一些。将源文件提交到main分支。通过GitHub的Actions,在上一步完成之后,自动build,并将结果提交到另一个分支(gh-pages)。GitHub Pages指向gh-pages分支。3.1 创建gh-pages分支如下图,在下拉菜单中输入gh-pages,并点击下面的Create branch提示,会创建gh-pages分支。3.2 将GitHub Pages指向gh-pages分支如下图方式修改:3.3 设置自动发布的Token因为需要自动发布,设置用于发布的Token。首先创建Token,依照下图逐步创建即可。点击右上角的头像,选择Setting:在打开的页面中点击Developer setting选择Personal access tokens, 点击Generate token按钮创建一个新的Token
2. 配置VuePress一直比较喜欢vue的官方文档的样式,也提供了开源的VuePress,地址为https://vuepress.vuejs.org/zh/,也是类似风格的文档样式。根据指南菜单里的说明,创建一个以VuePress为框架模板的网站。VuePress 需要 Node.js >= 8.62.1 创建并进入一个新目录mkdir vuepress-starter && cd vuepress-starter2.2 使用包管理器进行初始化yarn init # npm init2.3 将 VuePress 安装为本地依赖yarn add -D vuepress # npm install -D vuepress2.4 创建第一篇文档mkdir docs && echo '# Hello VuePress' > docs/README.md2.5 在 package.json 中添加一些 scripts{ "scripts": { "docs:dev": "vuepress dev docs", "docs:build": "vuepress build docs" } }2.6 在本地启动服务器yarn docs:dev # npm run docs:devVuePress 会在 http://localhost:8080 启动一个热重载的开发服务器。这肯定不是我们想要的漂亮页面,我们再设置一下。
1. 配置GitHub PagesGitHub新建一个repository。填写repository的名字,命名规则如下:采用“用户名.github.io"作为名称, 这样生成的网站地址为”https://用户名.github.io",。如果用其他名称,例如本例的"gitpages", 最终对应的网址为“https://flylolo.github.io/gitpages/”。设置为Public,选择添加一个README.md文件。因为是vue项目,.gitignore 文件选择Node。点击Create按钮开始创建。创建成功后,页面如下图,点击Settings进行设置点击Save按钮,会看到页面出现如下提示点击该链接,可以看到能正常访问。显示的内容即README.md的内容。如此就配置成功了,可以自己制作静态网站签入到这个repository即可。
3.3 硬盘大于64G3.4 选择下载好的ISO文件3.5 cpu至少双核3.6 开启TPM支持这个默认是未勾选的,一定要选中。设置完毕,正常安装即可。
1. 下载Win11的ISO文件Windows11的下载地址:https://www.microsoft.com/zh-cn/software-download/windows112. 系统安装要求win11安装的时候会检测系统是否符合要求,具体要求如下若不符合要求,会出现“这台电脑无法运行Windows 11。这台电脑不符合安装此版本windows所需的最低系统要求。”的提示。3. Hyper-v中新建虚拟机3.1 选择第二代,支持UEFI3.2 内存要大于等于4G
常见的键盘按键与键码的对照表。
浏览器版VSCode来了,打开网址https://vscode.dev,即可正常使用,如下图点击Open Folder,选择项目文件夹,开始coding吧!
6.2 Java的内部类再看一下Java的内部类:public class OuterClass { public String outerClassName = "outerClass's name"; public void getNestedClassName() { String staticString = NestedStaticClass.staticString; //无法直接调用非静态内部类的变量 //String str = NestedClass.nestedClassName; } public NestedClass getNestedClass() { //可以直接new return new NestedClass(); } class NestedClass { public String nestedClassName = "nestedClass's name"; public void printOuterClassName() { //可以直接调用外部类的对象 System.out.println(outerClassName); } public OuterClass getOuter() { //返回外部类实例 return OuterClass.this; } } static class NestedStaticClass { public String nestedClassName = "NestedStaticClass's name"; public static String staticString = "staticString"; public void printOuterClassName() { //error 不可以直接调用外部类的对象 //System.out.println(outerClassName); } //error 无法返回外部类实例 // public OuterClass getOuter(){ // return OuterClass.this; // } } } class Test{ public static void main(String[] args) { //不允许直接通过new的方式创建OuterClass.NestedClass //OuterClass.NestedClass nestedClass1 = new OuterClass.NestedClass(); //只能通过外部类的实例创建内部类 OuterClass outerClass = new OuterClass(); //通过方法返回内部类实例 OuterClass.NestedClass nestedClass = outerClass.getNestedClass(); //通过.new关键字 OuterClass.NestedClass nestedClass1 = outerClass.new NestedClass(); //通过内部类实例获取外部类实例 System.out.println(nestedClass1.getOuter().outerClassName); nestedClass.printOuterClassName(); String staticString = OuterClass.NestedStaticClass.staticString; OuterClass.NestedStaticClass nestedStaticClass = new OuterClass.NestedStaticClass(); System.out.println(nestedStaticClass.nestedClassName); } }可见,Java的内部类“玩法比较多,完全写来下可以说是一个比较大的专题了,简要列举一下与C#的内部类的不同之处。6.3 非静态内部类总结外部类都无法访问内部类的的方法和属性,但Java的内部类可以访问外部类的方法和属性,C#的不可以,Java内外部类互相访问提供了“.New”和“.this"关键字。创建内部类,new的对象不同,C#通过“new 外部类.内部类() ”方式创建,Java不允许这样,需要外部类的实例,即:”外部类实例.new 内部类()“。除了上述的内部类定义方式,Java的内部类可以出现在外部类的方法、语句块中。6.4 静态内部类总结C#的静态类中不允许有非静态方法和成员属性,Java的静态内部类中可以有。C#和Java的内部类可以直接通过“外部类.内部类”的方式访问,具体要考虑内部类对应的访问修饰符。C#的内部类不允许被new出新实例,Java的可以。Java通过直接的方式访问内部类,只允许访问静态方法和成员属性。通过new的方式产生的实例,即可以访问静态成员也可以访问非静态成员。但不建议通过这种方式访问静态成员。6.5 其他Java还可以通过内部类的方式实现匿名类、多重继承等。Java8之后,一些情形可以通过lamda简化内部类的写法。
1. 包(Package)、命名空间(NameSpace)1.1 概念在Java中常用的是包(Package),较少提到NameSpace的概念。Java官方文档中这样说:为了使类型更易于查找和使用,避免命名冲突并控制访问,程序员将相关类型的组捆绑到包中。定义:包是一组提供访问保护和名称空间管理的相关类型。 请注意,类型是指类、接口、枚举和注释类型。 枚举和注解类型分别是特殊类型的类和接口,因此在本课中通常将类型简称为类和接口。根据这里的概念,Package基本上是对应C#的NameSpace的。无论是Java还是C#,每个类都有属于一个包/命名空间:Java:package cn.flylolo.entity; public class Pig extends Animal{ }C#:namespace cn.flylolo.entity; public class Pig : Animal{ }1.2 命名规则Java一般用域名倒序的方式来作为包名,多个单词用“.”分隔,同时这也对应着目录的层级关系。C#中也可以用这样的规则来命名NameSpace,也见过这样的命名方式,但不强制;并且与目录也可以没有关联关系。1.3 引用方式Java引用包:import cn.flylolo.entity.Pig;1C# 引用命名空间:using cn.flylolo.entity.Pig;1C#的命名空间别名:若要引用同名的不同类,处理方式都是写全包/命名空间的名称。C#中觉得较长不美观可以在using的时候设置别名:using entityPig = cn.flylolo.entity.Pig;1在代码中可以直接使用别名引用。2.访问修饰符上一节,Java的包与C#的命名空间类似,但针对访问修饰符,包又与C#的程序集类似。3.类与文件Java中,一个.java文件中,只允许有一个Public的类,并且文件名与此类名一般相同。C#中则无上述限制。4.继承,sealed与final4.1 继承一个类或实现接口:C#用“:" 符号。Java继承类用extends关键字,实现接口用implements关键字。4.2 不想让一个类被继承:Java 用final关键字:public final class Shape1C# 用sealed关键字:public sealed class Shape1注意: JDK15的时候,Java也提供了sealed关键字,用于限制继承,例如下列代码public sealed class Shape permits Circle, Square, Rectangle {}12通过sealed+permits两个关键字,限制了子类只能是Circle, Square, Rectangle这三个。5.StaticC#,有静态类和静态方法。Java,有静态类和静态方法,但静态类只能是内部类,详见下一节。6. 内部类、嵌套类6.1 C#的内部类C#的内部类比较简单,类似如下代码:namespace cn.flylolo.nestedclass; /** * @author luozhichao * @date 2021/10/15 17:50 */ public class OuterClass { public String outerClassName = "outerClass's name"; public void printNestedClassName() { //无法直接调用内部类的变量 //Console.WriteLine(NestedClass.nestedClassName); Console.WriteLine(NestedStaticClass.nestedClassName); } public class NestedClass { public String nestedClassName = "nestedClass's name"; public void printOuterClassName() { //error 不可以直接调用外部类的对象 //Console.WriteLine(outerClassName); } } public static class NestedStaticClass { public static String nestedClassName = "NestedStaticClass's name"; public static void printOuterClassName() { //error 不可以直接调用外部类的对象 //Console.WriteLine(outerClassName); } } } class Test { public static void main(String[] args) { OuterClass.NestedClass nestedClass = new OuterClass.NestedClass(); //可以直接调用静态内部类的方法。 string str = OuterClass.NestedStaticClass.nestedClassName; } }代码中做了一些注释,可以看到,对于非静态的内部类,外部类就像给其加了一层“命名空间”,可以通过new OuterClass.NestedClass()的方式进行创建。对应静态内部类,可以通过OuterClass.NestedStaticClass的方式直接调用其方法和属性,当然这也由对应的访问修饰符决定,例如将NestedStaticClass设置为private,则OuterClass可以直接调用NestedStaticClass,而上例中的Main方法则无法调用NestedStaticClass了。
3.5 配置Tomcat服务打开Run/Debug Configuration, 点击左上角的加号,选择Tomcat Server->Local。HTTP prot默认为8080,若已被使用则改为其他的端口。选择artifacts,点击右下角的Fix按钮,跳转到Deployment标签,选择刚刚配置的flylolo-readcode。保存并启动项目,访问UserController,地址:http://localhost:8099/flylolo_readcode/user3.6 添加json解析:如果只是返回String类型是没问题了,但大多数需要返回的时候Json类型。新建一个User类:package cn.flylolo.model; import lombok.Data; /** * @author FlyLolo * @date 2021/10/11 11:18 */ @Data public class User { private String userId; private String userName; }这里用到了lombok,需要在build.gradle中添加引用。implementation 'org.projectlombok:lombok:1.18.20' annotationProcessor 'org.projectlombok:lombok:1.18.20'注意需要添加第二行,否则在调用对应的get和set方法的时候会出现 “错误: 找不到符号”的错误。在UserController中添加新的方法:@GetMapping("/{userId}") public User getName(@PathVariable String userId){ User user = new User(); user.setUserId(userId); user.setUserName(userId + "的名字"); return user; }将返回一个User对象。访问http://localhost:8099/flylolo_readcode/user/testid,返回了406,不可接收错误。因为返回Json类型,需要添加对应的message-converters,本例采用FastJson。用下面代码替换springmvc.xml中的<mvc:annotation-driven /><mvc:annotation-driven> <mvc:message-converters register-defaults="true"> <!-- 配置Fastjson支持 --> <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>application/json</value> <value>text/html;charset=UTF-8</value> </list> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>这需要在build.gradle中添加FastJson的引用:implementation 'com.alibaba:fastjson:1.2.78'再次访问http://localhost:8099/flylolo_readcode/user/testid,得到了期望的结果。至此,源码阅读环境准备完毕。4. 遇到的问题4.1 gradle进行build的时候,中文出现乱码:Help->Edit Custom VM Options, 添加如下代码:-Dfile.encoding=UTF-84.2 gradle项目,用了lombok,调用setXXX提示“找不到符号"的错误,需在build.gradle中做如下方式引用//添加annotationProcessor,否则会出现找不到符号的错误annotationProcessor 'org.projectlombok:lombok:1.18.20'implementation 'org.projectlombok:lombok:1.18.20'//添加annotationProcessor,否则会出现找不到符号的错误 annotationProcessor 'org.projectlombok:lombok:1.18.20' implementation 'org.projectlombok:lombok:1.18.20'4.3 服务启动报错问题服务无法正常启动,报错“org.apache.tomcat.util.modeler.BaseModelMBean.invoke 调用方法[manageApp]时发生异常 java.lang.IllegalStateException: 启动子级时出错”,详细错误如下:Connected to server [2021-10-11 03:30:50,531] Artifact flylolo-readcode: Artifact is being deployed, please wait... 11-Oct-2021 15:30:50.793 严重 [RMI TCP Connection(3)-127.0.0.1] org.apache.tomcat.util.modeler.BaseModelMBean.invoke 调用方法[manageApp]时发生异常 java.lang.IllegalStateException: 启动子级时出错 at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:729) at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:698) at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:696) at org.apache.catalina.startup.HostConfig.manageApp(HostConfig.java:1783) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:293) at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:814) at java.management/com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:802) at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:460) at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:408) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:293) at java.management/com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:814) at java.management/com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:802) at java.management/com.sun.jmx.remote.security.MBeanServerAccessController.invoke(MBeanServerAccessController.java:472) at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.doOperation(RMIConnectionImpl.java:1472) at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation.run(RMIConnectionImpl.java:1310) at java.base/java.security.AccessController.doPrivileged(AccessController.java:712) at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.doPrivilegedOperation(RMIConnectionImpl.java:1412) at java.management.rmi/javax.management.remote.rmi.RMIConnectionImpl.invoke(RMIConnectionImpl.java:829) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360) at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) at java.base/java.security.AccessController.doPrivileged(AccessController.java:712) at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:587) at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828) at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:705) at java.base/java.security.AccessController.doPrivileged(AccessController.java:399) at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:704) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at java.base/java.lang.Thread.run(Thread.java:833) Caused by: org.apache.catalina.LifecycleException: 无法启动组件[StandardEngine[Catalina].StandardHost[localhost].StandardContext[/flylolo_readcode]] at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:440) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:198) at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:726) ... 42 more Caused by: java.lang.NoClassDefFoundError: jakarta/servlet/ServletContainerInitializer at java.base/java.lang.ClassLoader.defineClass1(Native Method) at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012) at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150) at org.apache.catalina.loader.WebappClassLoaderBase.findClassInternal(WebappClassLoaderBase.java:2478) at org.apache.catalina.loader.WebappClassLoaderBase.findClass(WebappClassLoaderBase.java:870) at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1371) at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1215) at java.base/java.lang.Class.forName0(Native Method) at java.base/java.lang.Class.forName(Class.java:467) at org.apache.catalina.startup.WebappServiceLoader.loadServices(WebappServiceLoader.java:226) at org.apache.catalina.startup.WebappServiceLoader.load(WebappServiceLoader.java:197) at org.apache.catalina.startup.ContextConfig.processServletContainerInitializers(ContextConfig.java:1840) at org.apache.catalina.startup.ContextConfig.webConfig(ContextConfig.java:1298) at org.apache.catalina.startup.ContextConfig.configureStart(ContextConfig.java:986) at org.apache.catalina.startup.ContextConfig.lifecycleEvent(ContextConfig.java:303) at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:123) at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5135) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ... 43 more Caused by: java.lang.ClassNotFoundException: jakarta.servlet.ServletContainerInitializer at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1407) at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1215) ... 61 more通过错误信息中的“Caused by: java.lang.ClassNotFoundException: jakarta.servlet.ServletContainerInitializer”可以看出是缺少对应的包,网上搜了有类似的错误,少的却不是这个包,后来尝试把Tomcat改为10.0.12(出错时为Tomcat 9),此问题解决,应该是最新的Tomcat中存在此包。5 GitHub地址https://github.com/FlyLolo/spring-framework
1. 获取spring-framework源码地址:spring-projects/spring-framework: Spring Framework (github.com)目前看到最新的Tag是v5.3.10。可以直接将最新代码clone到本地,如果想在代码做一些注释,也可以Fork到自己的仓库。本文采用Fork的方式,并添加了测试module。2. 导入到IDEA项目的wiki中给出了导入到 Eclipse 和 IntelliJ IDEA的方式:Ensure JDK 17 is configured properly in the IDE. Follow instructions for Eclipse and IntelliJ IDEA.要求安装 JDK17,根据自己的需求选择导入到 Eclipse 或 IntelliJ IDEA。对应的文档在下载的代码根目录也有,分别为import-into-eclipse.md和import-into-idea.md。本文为IDEA方式。2.1 预编译spring-oxm在代码目录打开cmd,输入命令gradlew :spring-oxm:compileTestJava(windows系统无需输入“./”),开始编译。若出现如下错误,需检查JAVA_HOME是否已正确配置了JDK17:若未安装配置gradle,会自动下载安装。默认情况下,下载的包会存放在C:\Users\用户名\.gradle文件夹下。若C盘空间比较紧张想放到别的目录,可以配置一下名为GRADLE_USER_HOME的环境变量,将其值设置为新的目录。gradle的安装配置和maven类似,如果自己安装最好按照源码中的版本。可以查看spring-framework\gradle\wrapper文件夹下的gradle-wrapper.properties文件中的distributionUrl的配置,例如目前代码采用的Gradle版本为7.2.distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists等待编译完成即可,最终结果类似如下情况。连接Github经常出现网络问题,若出现错误重新执行这个命令几次。2.2 导入到Idea依次点击菜单File->New->Project From Existing sources,出现如下对话框选择Gradle,提示信任此项目,选择Trust Project点击下图箭头所示的Reload All Gradle Projects可以在Idea的Build日志中看到如下输出第一次会下载很多依赖包,比较慢,慢慢等待。网上有说用阿里云Maven服务的,会快一些。但有时候个别包下载失败,不着急就慢慢等吧。直至Build完成,如果中途失败可以多试几次。
1. 基本数据类型Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型。C#提供的类型更细一些。Java没有基本的decimal类型,有个BigDecimal类型,是一个继承于Number类的类。2.结构类型C#提供像C语言一样的结构体(struct )类型,Java没有此类型,一般通过类实现类似功能。3.枚举类型二者都有枚举类型enum。C#的枚举类型时值类型,默认情况下,枚举成员的关联常数值为类型 int;它们从零开始,并按定义文本顺序递增 1。 可以显式指定任何其他整数数值类型作为枚举类型的基础类型。enum HttpCode : ushort { OK = 0, BadRequest= 400 }Java的枚举类型是通过类实现的,所以可以为其添加属性和方法等。public class Simple { enum HttpCode { OK("请求成功!", 200), BadRequest("请求失败!", 400); // 成员变量 private String message; private int code; // 构造方法 private HttpCode(String message, int code) { this.message = message; this.code = code; } // 覆盖方法 @Override public String toString() { return this.message; } } public static void main(String[] args) { System.out.println(HttpCode.BadRequest.toString()); } }Java的枚举成员都是通过 Class 在内部实现的,且所有的枚举值都是 public static final 的。上面的枚举类代码可以理解为:public final class HttpCode extends Enum{ public static final HttpCode OK; public static final HttpCode BadRequest; }4.元组类型C#7.0之后提供类元组类型。Java中JDK原生不支持元组,但有框架javatuples支持元祖。5.可空值类型与包装类型C#为值类型提供了对应的可空值类型,例如int->int?,本质是通过结构体实现的。Java为值类型提供了对应的包装类型,例如int->Integer,包装类型为引用类型。6.Object、String二者都以Object类型作为所有类型的基类,都提供了String类(以及StringBuilder),注意首字母大小写不同。
1. Idea新建Grande项目依次点击菜单File->New->Project, 新建项目,选择Gradle,如下图勾选Java和Web两个选项,点击Next按钮进行下一步设置项目的名字,本例名为gradle_mvc点击Finish完成设置,项目开始创建,等待项目创建完成。2.添加依赖编辑build.gradle文件, 在dependencies内添加spring-webmvc的依赖:implementation 'org.springframework:spring-webmvc:5.3.10'保存并点击Gradle面板中的Reload按钮,重新加载依赖,最终可以看到如图所示依赖情况。3.添加mvc相关文件文件结构如下:首先添加一个Controller:package cn.flylolo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author FlyLolo * @date 2021/10/9 16:42 */ @RestController @RequestMapping("user") public class UserController { @GetMapping("") public String helloWorld(){ return "Hello World!"; } }②在resources目录下新建springmvc.xml文件:<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <context:component-scan base-package="cn.flylolo"/> <mvc:annotation-driven /> <mvc:default-servlet-handler /> </beans>③webapp目录下新建WEB-INF文件夹,其中新建web.xml文件:<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!--配置springmvc核心servlet--> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>4. 项目设置打开File->Project Struture设置,选择带"exploded"后缀的,修改图中方框中的内容:Name比较长,可以自行修改,不改也可以,本例改为gradlemvcOutput directory自动生成的路径有问题,去掉"exploded", 例如本例改为:F:\gradle_mvc\build\libs\gradle_mvc-1.0-SNAPSHOT.war。5.配置Tomcat配置Tomcat服务,打开Run/Debug Configuration, 点击左上角的加号,选择Tomcat Server->Local。HTTP prot默认为8080,若已被使用则改为其他的端口。选择artifacts,点击右下角的Fix按钮,跳转到Deployment标签,选择刚刚配置的gradlemvc。保存并启动项目,访问HelloController,地址:http://localhost:8081/gradlemvc/user
View Code这个文件的结构和Setting.json文件的结构基本上是一样的,只不过系统提供了比较多的默认配置。 我们如果想写配置也可以用作参考。1.系统提供的配色方案可以看到在default.json"schemes"数组中提供了多种配色方案,例如"Campbell"、"Campbell Powershell"、"Vintage"等。Campbell:Solarized Light:2. 配色方案的使用使用方法就是在"profiles"节点中进行设置,例如Setting文件中做如下配置: //profiles配置 "profiles": { "defaults": { // 设置通用配置 "colorScheme": "Solarized Light" }, "list": [ { // 针对 cmd.exe 这个profile进行配置. "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", "name": "命令提示符", "commandline": "cmd.exe", "hidden": false, "colorScheme": "Tango Light" } // 。。。。。。。。。。。。。。 ] },可以看到代码中分别针对“default"和”cmd.exe“做了"colorScheme": "Solarized Light"和"colorScheme": "Tango Light"的配置,这使每个tab均默认采用名为"Solarized Light"的配色方案,cmd的tab除外,因为专门在cmd的节点中定义的优先级要高于”default“的默认配置。这也使我们简单了解了"profiles"的配置方式。我们可以像配置配色方案一样配置其他属性。例如可以通过如下代码将默认的背景颜色设置成红色(挺丑的)。 "defaults": { // 设置通用配置 "background": "#FF0000" },当然这只是例子,关于这样的样式设置还是通过建议自定义colorScheme来实现(见下节)。不只是样式,还可以设置光标、键盘、tab的标题等,这里就不一一介绍了,详见本文底部的官方链接。3. 自定义colorScheme 本节我们自定义一个colorScheme。 //自定义的color schemes放在这里 "schemes": [ { "name": "FlyLolo Test", "cursorColor": "#5F04B4", "background": "#FFFFFF", "selectionBackground": "#D8F781", "black": "#3C5712", "blue": "#17b2ff", "brightBlack": "#749B36", "brightBlue": "#27B2F6", "brightCyan": "#13A8C0", "brightGreen": "#89AF50", "brightPurple": "#F2A20A", "brightRed": "#F49B36", "brightWhite": "#741274", "brightYellow": "#991070", "cyan": "#3C96A6", "foreground": "#6A0888", "green": "#6AAE08", "purple": "#991070", "red": "#8D0C0C", "white": "#6E386E", "yellow": "#991070" } ]设置了Windows PowerShell的背景和毛玻璃效果: { // 针对 powershell.exe 这个profile进行配置. "guid": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", "name": "Windows PowerShell", "commandline": "powershell.exe", "hidden": false, "colorScheme": "FlyLolo Test", "useAcrylic": true, "acrylicOpacity": 0.7, "backgroundImage": "D://1.png", "backgroundImageStretchMode": "none", "backgroundImageAlignment": "bottomRight", "backgroundImageOpacity": "0.6" },最终效果如下图。 背景图个人建议不要弄全屏的炫图,好看,但打字时就不实用了。
2022年07月
2022年04月
2022年03月
2022年02月
2022年01月
6
6
6
错过了
默认的里面没有
期待更多好文
centos停服了吧,虽然7还有一段时间,新的还用它么
应该不会吧
elasticjob