Oracle 在 HotSpot Virtual Machine Garbage Collection Tuning Guide 提到过一个调优策略:
Tuning Strategy
Do not choose a maximum value for the heap unless you know that you need a heap greater than the default maximum heap size. Choose a throughput goal that is sufficient for your application.
The heap will grow or shrink to a size that will support the chosen throughput goal. A change in the application’s behavior can cause the heap to grow or shrink. For example, if the application starts allocating at a higher rate, the heap will grow to maintain the same throughput.
If the heap grows to its maximum size and the throughput goal is not being met, the maximum heap size is too small for the throughput goal. Set the maximum heap size to a value that is close to the total physical memory on the platform but which does not cause swapping of the application. Execute the application again. If the throughput goal is still not met, then the goal for the application time is too high for the available memory on the platform.
If the throughput goal can be met, but there are pauses that are too long, then select a maximum pause time goal. Choosing a maximum pause time goal may mean that your throughput goal will not be met, so choose values that are an acceptable compromise for the application.
It is typical that the size of the heap will oscillate as the garbage collector tries to satisfy competing goals. This is true even if the application has reached a steady state. The pressure to achieve a throughput goal (which may require a larger heap) competes with the goals for a maximum pause time and a minimum footprint (which both may require a small heap).
这个调优的策略比较粗,可以作为我们调优的总纲领,还需要更细化一下才更具可操作性。
Selecting a Collector
Unless your application has rather strict pause-time requirements, first run your application and allow the VM to select a collector.
If necessary, adjust the heap size to improve performance. If the performance still doesn’t meet your goals, then use the following guidelines as a starting point for selecting a collector:
- If the application has a small data set (up to approximately 100 MB), then select the serial collector with the option
-XX:+UseSerialGC
.- If the application will be run on a single processor and there are no pause-time requirements, then select the serial collector with the option
-XX:+UseSerialGC
.- If (a) peak application performance is the first priority and (b) there are no pause-time requirements or pauses of one second or longer are acceptable, then let the VM select the collector or select the parallel collector with
-XX:+UseParallelGC
.- If response time is more important than overall throughput and garbage collection pauses must be kept shorter, then select the mostly concurrent collector with
-XX:+UseG1GC
.- If response time is a high priority, then select a fully concurrent collector with
-XX:+UseZGC
. 这份指南就更具操作性了。
周志明老师在他的《深入理解 java 虚拟机》中介绍了如何权衡收集器:
现在可能有读者要犯选择困难症了,我们应该如何选择一款适合自己应用的收集器呢?这个问题主要受以下三个因素影响:
应用程序的主要关注点是什么?如果是数据分析、科学计算类的任务,目标是尽可能快算出结果,那吞吐量就是主要关注点;如果是 SLA 应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延时就是主要关注点;而如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的。
运行应用的基础设施如何?譬如硬件规格,要涉及的系统架构是x86-32/64、SPAR 还是 ARM/Aarch64;处理器的数量多少,分配的内存大小;选择的操作系统是Linux、Solaris 还是 Windows 等。
使用 JDK 的发行商是什么?版本号是多少?是 ZingJDK/Zulu、OracleJDK、OpenJDK、OpenJ9 抑或是其他公司的发行版? 该 JDK 对应了《 Java 虚拟机规范 》的哪个版本?
一般来说,收集器的选择就从以上这几点出发来考虑。举个例子,假设某个直接面向用户提供服务的 B/S 系统准备选择垃圾收集器,一般来说延迟时间是这类应用的主要关注点,那么:
如果你有充足的预算但没有太多调优经验,那么一套带有商业技术支持的专有硬件或软件解决方案是不错的选择。Azul 公司以前主推的 Vega 系统和现在主推的 Zing VM 是这方面的代表,这样你就可以使用传说中的 C4 收集器。
如果你虽然没有充足的预算去使用商业解决方案,但能掌握软硬件型号,使用较新的版本,同时又特别注重延时,那 ZGC 很值得尝试。
如果你对还处于实验状态的收集器的稳定性有顾虑,或者必须运行在 Windows 操作系统下,那 ZGC 无缘了,试试 Shenandoah 吧。
如果你接手的是遗留系统,软硬件基础设施和 JDK 版本都比较落后,那就根据内存规模衡量一下,对于大概 4GB 到 6GB 以下的堆内存,CMS 一般能处理得比较好,而对于更大的堆内存,可重点考察一下 G1。
当然,以上都是仅从理论出发的分析,实战中切不可纸上谈兵,根据系统实际情况去测试才是选择收集器的最终依据。
这里的建议更具体,结合这两份建议,谈谈我个人的理解。
目前 HotSpot 虚拟机主要有 Serial, Parallel, CMS, Shenandoah 和 ZGC 这几款收集器,Serial, Parallel 主要关注吞吐量; CMS, Shenandoah 和 ZGC 主要关注低延时。
在做 Java 服务端开发时,我们基本可以不考虑 Serial 收集器。而服务端的应用主要是面向用户提供服务的,所以要选择低延时的收集器。根据使用 java 版本和堆内存大小从 CMS, G1 和 ZGC 中选择,然后进行测试,根据测试结果选择。
下面举个 Tomcat 的例子,Tomcat 9 支持 java 8 及以上的版本,可以用来试验各自收集器。
在 Tomcat 的安装包对应的 bin 目录下新建 setenv.sh 文件:
1 2 3 4 5 6 7 8 |
|
取消对应行的注释去试验对应的收集器,可以考虑使用 ab 之类压测工具观察性能变化,gceasy 可以用来辅助分析 gc 日志。
本文我们先具体来看看它与 Spring Boot 集成时的初始化。
MyBatis 官方团队有开发一个 Spring Boot Starter,我们通过它的代码来看下配置加载和初始化。配置的入口是 MybatisAutoConfiguration,它会按需创建 sqlSessionFactory 和 sqlSessionTemplate 这两个 bean 对象,同时它还包含一个内部配置类 MapperScannerRegistrarNotFoundConfiguration:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
MapperScannerRegistrarNotFoundConfiguration 导入了 AutoConfiguredMapperScannerRegistrar,它会向 IoC 容器注册 MapperScannerConfigurer。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
MapperScannerConfigurer 是一个 BeanDefinitionRegistryPostProcessor,IoC 会调用它的 postProcessBeanDefinitionRegistry 方法来处理 bean 定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
从方法中我们可以知道,它使用 ClassPathMapperScanner 来扫描注册 Mapper,ClassPathMapperScanner 覆盖了父类 ClassPathBeanDefinitionScanner 的 doScan 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
它在 processBeanDefinitions 方法中对 bean 定义进行了进一步的处理,把 bean 的 bean class 换成了 MapperFactoryBean,由它来生成创建对应的 Mapper。另外就是设置按类型自动连线 definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
,我在跟代码的过程中一开始很奇怪 ClassPathMapperScanner 注册 MapperFactoryBean 的 bean 定义时 sqlSessionTemplate 为 null,那么是在什么时候设置的 sqlSessionTemplate 呢?经过一番代码追踪,发现是 beanDefinition 设置了 AutowireMode,AbstractAutowireCapableBeanFactory 会帮助自动关联。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
|
MapperFactoryBean 的 getObject 方法如下,它使用上文所述,容器中的 sqlSession bean 来创建 mapper,通常也就是 MybatisAutoConfiguration 定义的 sqlSession bean。
1 2 3 |
|
MybatisAutoConfiguration 是定义 sqlSession bean 的代码如下:
1 2 3 4 5 6 7 8 9 10 |
|
它依赖的 SqlSessionFactory MybatisAutoConfiguration 也有定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
SqlSessionTemplate 是一个代理对象,它的功能依赖 SqlSessionFactory,它的 getMapper 方法如下:
1 2 3 4 |
|
实现依赖 getConfiguration, 它的内容如下:
1 2 3 4 |
|
Configuration 就是使用的 sqlSessionFactory 的配置对象。
现在我们聚焦到 SqlSessionFactory 的 Configuration 对象是怎么来的。 SqlSessionFactory 是由 SqlSessionFactoryBean,MybatisAutoConfiguration 有为它设置一个 Configuration :
1 2 3 4 5 6 7 8 9 10 11 12 |
|
那么最终的 Configuration 就是这个外部设置的对象吗?我们继续看它的 getObject 方法:
1 2 3 4 5 6 7 8 |
|
再看下它的 afterPropertiesSet 方法:
1 2 3 4 5 6 7 8 |
|
是在 buildSqlSessionFactory 方法中创建的 sqlSessionFactory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
|
从这段代码可知,由于 MybatisAutoConfiguration 有设置一个 Configuration ,所以 SqlSessionFactoryBean 使用的就是设置的这个 Configuration。
sqlSessionFactoryBuilder 默认是 SqlSessionFactoryBuilder,它的赋值代码为:
1
|
|
它的 build 方法实现为:
1 2 3 |
|
因此创建的是 DefaultSqlSessionFactory 对象。
现在 SqlSessionFactory, SqlSession, Configuration 的实现来源都清楚了,我们继续来看创建 mapper 的代码,看 mapper 的实现是什么?
1 2 3 |
|
mapperRegistry 的实现是 MapperRegistry,它的 getMapper 方法如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
它是从 knownMappers 取出类型对应的 MapperProxyFactory,那么它是何时如何注册的呢?
前面我们说过, Mapper 对应向 IoC 注册的的 bean 定义是 MapperFactoryBean 对象,MapperFactory 继承自 SqlSessionDaoSupport,而 SqlSessionDaoSupport 继承自 DaoSupport,DaoSupport 实现 InitializingBean 接口,所以在实例化 mapper 时会实例化出 MapperFactory,并先调用它的 afterPropertiesSet 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
MapperFactory 覆盖了 checkDaoConfig 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
也就是在这个方法中向 Configuration 添加了 mapper 对应的 MapperProxyFactory 类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
从上述代码可以看出 mapper 对应的 MapperProxyFactory 类型是用定义的 mapper 接口类型参数化的 MapperProxyFactory,它的 newInstance(SqlSession sqlSession)
方法如下:
1 2 3 4 |
|
最后为 mapper 接口创建向 mapperProxy 实例转发调用的代理对象。
1 2 3 |
|
也就是说,mapper 接口类型对象等效于一个 MapperProxy 对象。至此,MyBatis 的初始化过程就梳理清楚了。
]]>Servlet 规范中有详细的错误处理说明,简单来说就是 Servlet 在处理请求时可能会抛出异常或者调用 sendError
,这时 Servlet-Container 就要产生相应的错误界面,错误界面是允许自定义的。Spring MVC 的核心之一是 DispatchServlet,它是一个前端控制器,所有的请求处理都由它来驱动,从名字可以看出,它也是一个 Servlet,所以它的错误处理机制自然要遵循 Servlet 规范。从完整性的角度来看,还一种错误处理方法,Servlet 可以自己设置 HTTP 的 status code 和 body,也就是不和 Servlet-Container 联动来处理错误,而是完全自主地处理。
我们先来看 DispatcherServlet 的异常处理机制,Spring 团队将异常处理功能集中到 HandlerExceptionResolver 接口的实现类中,DispatcherServlet 在初始化过程会把 IoC 容器中所有的 HandlerExceptionResolver 的实现类排好序后组装起来用于异常处理。
现在我们使用 Spring 来开发 web 应用时应该都会选择 Spring Boot 来配置 Spring,和异常相关的自动配置类为 ErrorMvcAutoConfiguration 和 WebMvcAutoConfiguration, 它们默认配置两个 HandlerExceptionResolver: DefaultErrorAttributes 和 HandleExceptionResolverComposite。
HandleExceptionResolverComposite 默认包含以下三个 HandlerExceptionResolver:
- ExceptionHandlerExceptionResolver matches uncaught exceptions against suitable @ExceptionHandler methods on both the handler (controller) and on any controller-advices.
- ResponseStatusExceptionResolver looks for uncaught exceptions annotated by @ResponseStatus (as described in Section 1)
- DefaultHandlerExceptionResolver converts standard Spring exceptions and converts them to HTTP Status Codes (I have not mentioned this above as it is internal to Spring MVC).
Spring 官方博客帮我们总结了 Spring Boot 默认配置的异常处理流程:
- In the event of any unhanded error, Spring Boot forwards internally to /error.
- Boot sets up a BasicErrorController to handle any request to /error. The controller adds error information to the internal Model and returns error as the logical view name.
- If any view-resolver(s) are configured, they will try to use a corresponding error-view.
- Otherwise, a default error page is provided using a dedicated View object (making it independent of any view-resolution system you may be using).
- Spring Boot sets up a BeanNameViewResolver so that /error can be mapped to a View of the same name.
- If you look in Boot’s ErrorMvcAutoConfiguration class you will see that the defaultErrorView is returned as a bean called error. This is the View bean found by the BeanNameViewResolver.
对于 Servlet-Container 层面的错误处理,Spring 官方博客的介绍如下:
Container-Wide Exception Handling
Exceptions thrown outside the Spring Framework, such as from a servlet Filter, are also reported by Spring Boot’s fallback error page. To do this Spring Boot has to register a default error page for the container. In Servlet 2, there is an
<error-page>
directive that you can add to your web.xml to do this. Sadly Servlet 3 does not offer a Java API equivalent. Instead Spring Boot does the following:
- For a Jar application, with an embedded container, it registers a default error page using Container specific API.
- For a Spring Boot application deployed as a traditional WAR file, a Servlet Filter is used to catch exceptions raised further down the line and handle it.
我们可以按照上述线索在 Spring Boot 的自动配置代码中找到相关的代码。
当开发 REST API 项目时,我希望业务抛出的异常能契合 Spring Boot 默认配置的异常处理机制,让整个异常体系尽量统一,接口返回给终端统一格式的错误信息,这样终端也能统一处理接口错误。那么我们应该如何做?
我们这里需要的是一个全局的异常处理机制,Spring MVC 提供给我们两种配置全局异常处理的方法:
@ControllerAdvice
注解的类相比之下,个人觉得使用 @ControllerAdvice
注解的类会方便一些,能达到感知框架的存在。我们可以定义一个异常处理基类,发布成一个库,然后在需要用到的项目中引入这个库,在项目中继承该基类定义一个 @ControllerAdvice
注解的异常处理类。
选好全局异常处理机制后,那么我们应该如何来设计项目的业务异常类呢?
Spring 官方博客给出了如下建议:
As usual, Spring likes to offer you choice, so what should you do? Here are some rules of thumb. However if you have a preference for XML configuration or Annotations, that’s fine too.
- For exceptions you write, consider adding @ResponseStatus to them.
- For all other exceptions implement an @ExceptionHandler method on a @ControllerAdvice class or use an instance of SimpleMappingExceptionResolver. You may well have SimpleMappingExceptionResolver configured for your application already, in which case it may be easier to add new exception classes to it than implement a @ControllerAdvice.
- For Controller specific exception handling add @ExceptionHandler methods to your controller.
- Warning: Be careful mixing too many of these options in the same application. If the same exception can be handed in more than one way, you may not get the behavior you wanted. @ExceptionHandler methods on the Controller are always selected before those on any @ControllerAdvice instance. It is undefined what order controller-advices are processed.
这里我们重点来看第一条建议,他建议我们自己写的异常类可以考虑加上 @ResponseStatus
注解,这样 service 就可以往上层传递 HTTP 的 status 信息,然后可以根据异常的类型填充 body 信息。这样做当然可以,只是这样一来异常类数量容易膨胀,定义异常类也是很乏味。我觉得可以定义一个能表达 HTTP status, headers 和 body 信息的类,然后抛出它的实例,他的建议作为补充。
设计好异常类层级后,接口出错时应该返回些什么信息给终端?
《Web API 的设计与开发》作者建议的单个和多个错误信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
同样,我还是希望业务异常产生的错误信息能够兼容 Spring Boot 默认产生的错误信息,这样终端可以统一处理错误信息。Spring Boot 默认产生的错误信息包含如下字段:
1 2 3 4 5 6 7 8 |
|
综上所述,我们可以定义 REST API 的错误信息为 RestErrorInfo:
1 2 3 4 5 6 7 8 9 |
|
异常类 ServiceException 为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
异常处理类为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
完整代码在这里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
A: macOS 上的 java 虚拟机基础工具不能附加到 java 进程上,操作系统环境为:
1 2 3 4 |
|
java 版本:
1 2 3 4 |
|
google 了一番之后,问题可能是 macOS 上的 java 8 部分小版本有问题,于是我打算用虚拟机里面的 RockyLinux 来试下,结果是确定可以。
这中间还一个小插曲,也很有意思,这里记一下。虚拟机里面的 RockyLinux 之前并没有安装 java 开发环境,于是我就下载安装 eclipse,2022-06 版本的 eclipse 默认要求 java 11+
的运行环境,它默认选择安装的是 java 17,这也确定是一个好选择,一是 ZGC 是从 java 15 开始 Production Ready;二是 java 17 是一个长期支持版本。
ZGC was initially introduced as an experimental feature in JDK 11, and was declared Production Ready in JDK 15.
RockyLinux 自带的是 java 8,于是我们安装 jdk 8 和 jdk 17,然后使用自带的 alternatives 来切换 java 版本,但是它好像只管理 jre bin 下的命令,并有把 jdk 里面的命令都管理起来。我是使用 jinfo 提示报错发现的:
1
|
|
可以使用 alternatives --display java
去确定一下,从这里也可以看出工具的版本最好对应,所以我们也可以将它们纳入 alternatives 系统来管理。以 jhsdb 为例:
1
|
|
虚拟机里面做开发还是有点卡,最后还是决定在 mac 上安装多个版本的 java,macOS 上并没有自带 centOS 上 alternatives 类似的命令,需要安装第三方软件来管理,之前在 Spring 的源码里看到它是用 SDkMan 来管理多个版本的 java,所以也决定用 SDKMan 来管理。但是 SDKMan 和 alternatives 的实现方式有很大差别,它默认是将软件安装在用户目录下的隐藏目录下,这对 eclipse 之类 IDE 来配置相关的类库会稍微有点不方便,需要开启 Finder 的隐藏文件显示,快捷命令是 cmd+shift+.
,defaults write com.apple.Finder AppleShowAllFiles true
命令暂时没生效。
Reference:
在 MacOS 下 Virtualbox 的 RockyLinux 中安装 oracle-xe-21c,详细的错误信息如下:
1 2 3 4 |
|
参照
The short name of your host is missing from /etc/hosts, only the FQDN is there. It should be:
163.173.24.179 linux.mydomain.com linux
linux (hostname -s) is unreachable due to this.
把机器的主机名加入 hosts,
1
|
|
重新执行配置命令,问题解决了。
1 2 3 4 5 6 7 8 9 10 11 |
|
Reference:[[FATAL]] [[DBT-06103]] The port (5,500) is already in use [duplicate]
]]>Redis is an open source (BSD licensed), in-memory data structure store used as a database, cache, message broker, and streaming engine.
Redis是一个开源的内存数据结构存储,被用作数据库、缓存、消息代理和流媒体引擎。
我们看几个例子(都是使用64位实例得到的。
在过去,Redis的开发者尝试了虚拟内存和其他系统来允许大于RAM的数据集,但最终我们很高兴能做好一件事:数据从内存中提供,磁盘用于存储。所以现在还没有计划为Redis创建一个磁盘上的后端。Redis是当前设计的结果。
可以,一个常见的设计模式是在Redis中保存频繁写的小数据(以及你需要Redis数据结构以有效方式为你的问题建模的数据),而大块的数据保存到 SQL或最终一致的磁盘数据库。同样,有时Redis被用来在内存中获取存储在磁盘数据库中的相同数据子集的另一个副本。这看起来类似于缓存,但实际上是一个更高级的模型,因为通常Redis数据集与磁盘数据库数据集一起更新,而不是在缓存错过时刷新。
如果可以的话,使用Redis的32位实例。同时善用小的哈希值、列表、排序集和整数集,因为Redis能够在少数元素的特殊情况下以更紧凑的方式表示这些数据类型。
Redis有内置的保护措施,允许用户设置内存使用的最大限制,使用配置文件中的maxmemory选项,对Redis可以使用的内存进行限制。如果达到这个限制,Redis将开始对写命令回复错误(但会继续接受只读命令)。
你也可以配置Redis在达到最大内存限制时驱逐键值。
是的,Redis的后台保存过程总是在服务器没有执行命令时创建,所以每个在RAM中报告为原子的命令从磁盘快照的角度看也是原子的。
CPU成为你使用Redis的瓶颈并不是很常见,因为通常Redis的问题不是内存就是网络带宽。例如,当使用 piplelining 时,在一个普通的Linux系统上运行的Redis实例每秒可以提供100万个请求,所以如果你的应用程序主要使用O(N)或O(log(N))命令,它几乎不会使用太多的CPU。
然而,为了最大限度地提高CPU的使用率,你可以在同一个机器里启动多个Redis的实例,并把它们当作不同的服务器。在某些时候,一个机器可能无论如何也不够用,所以如果你想使用多个CPU,你可以提前开始考虑一些方法来分片。
Redis最多可以处理2^32
个键,经实践测试,每个实例至少可以处理2.5亿个键。
每个哈希、列表、集合和排序集,都可以容纳2^32
个元素。
换句话说,你的极限可能是你系统中的可用内存。
如果你使用有效期有限的键(Redis过期),这是正常行为。下面是问题的原因:
正因为如此,对于有很多过期键的用户来说,在复制节点中看到较少的键是很常见的。然而,从逻辑上讲,主节点和复制节点将有相同的内容。
Redis Sentinel provides high availability for Redis when not using Redis Cluster. Redis scales horizontally with a deployment topology called Redis Cluster.
Memory Usage
自推出以来,Spring Boot一直是Spring生态系统中的一个重要角色。这个项目凭借其自动配置能力使我们的生活变得更加轻松。
在本教程中,我们将介绍一些在求职面试中可能出现的与Spring Boot有关的最常见问题。
Spring Boot本质上是一个建立在Spring框架之上的快速应用开发框架。凭借其自动配置和嵌入式应用服务器支持,再加上其享有的大量文档和社区支持,Spring Boot是迄今为止Java生态系统中最受欢迎的技术之一。
这里有几个突出的特点。
译者点评:
官方文档总结的特点:
- Create stand-alone Spring applications
- Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
- Provide opinionated ‘starter’ dependencies to simplify your build configuration
- Automatically configure Spring and 3rd party libraries whenever possible
- Provide production-ready features such as metrics, health checks, and externalized configuration
Absolutely no code generation and no requirement for XML configuration
创建独立的Spring应用程序
直接嵌入Tomcat、Jetty或Undertow(不需要部署WAR文件)
提供有主见的 “启动器 "依赖,以简化你的构建配置
尽可能地自动配置Spring和第三方库
提供生产就绪的功能,如度量、健康检查和外部化配置
完全没有代码生成,也不需要XML配置
Spring框架提供了多种功能,使Web应用的开发更加容易。这些功能包括依赖性注入、数据绑定、面向切面编程、数据访问等等。
多年来,Spring越来越复杂,这种应用所需的配置量可能令人生畏。这就是Spring Boot的用武之地–它使配置一个Spring应用程序变得轻而易举。
从本质上讲,Spring是没有主见的,而Spring Boot对平台和库有主见,让我们快速上手。
下面是Spring Boot带来的两个最重要的好处。
请查看我们的其他教程,了解普通Spring和Spring Boot的详细比较。
我们可以像对待其他库一样,将Spring Boot纳入Maven项目中。不过,最好的方法是从spring-boot-starter-parent项目中继承,并声明对Spring Boot starters的依赖关系。这样做可以让我们的项目重用Spring Boot的默认设置。
继承spring-boot-starter-parent项目很简单,我们只需要在pom.xml中指定一个父元素:
1 2 3 4 5 |
|
我们可以在Maven中心找到最新版本的spring-boot-starter-parent。
使用启动器父项目很方便,但并不总是可行的。例如,如果我们公司要求所有项目都继承自标准POM,那么我们仍然可以通过使用自定义父项目来受益于Spring Boot的依赖性管理。
Spring Initializr是一种创建Spring Boot项目的便捷方式。
我们可以去Spring Initializr网站,选择一个依赖管理工具(Maven或Gradle)、一种语言(Java、Kotlin或Groovy)、一种打包方案(Jar或War)、版本和依赖性,然后下载项目。
这为我们创建了一个骨架项目,节省了设置时间,这样我们就可以集中精力添加业务逻辑。
即使我们使用IDE的(如STS或带有STS插件的Eclipse)新项目向导来创建Spring Boot项目,它也会在底下使用Spring Initializr。
每个启动器都扮演着一站式服务的角色,提供我们需要的所有 Spring 技术。其他所需的依赖项也会被拉进来,并以一致的方式进行管理。
所有启动器都在org.springframework.boot组下,其名称以spring-boot-starter-开头。这种命名模式使我们很容易找到启动器,特别是在使用支持按名称搜索依赖关系的IDE时。
在写这篇文章的时候,有超过50个启动器供我们使用。这里,我们将列出最常见的:
有关启动程序的完整列表,请参见该资源库。
要找到关于Spring Boot启动程序的更多信息,请看Spring Boot启动程序介绍。
如果我们想禁用一个特定的自动配置,我们可以使用@EnableAutoConfiguration注解的exclude属性来设置它。
例如,这个代码片断禁用了DataSourceAutoConfiguration:
1 2 3 |
|
如果我们用@SpringBootApplication注解启用了自动配置–该注解将@EnableAutoConfiguration作为元注解–我们可以用同名的属性禁用自动配置:
1 2 3 |
|
我们也可以用spring.autoconfigure.exclude环境属性禁用自动配置。在application.properties文件中的配置与之前做的设置是做相同的事情:
1
|
|
要注册一个自动配置类,我们必须在META-INF/spring.factories文件的EnableAutoConfiguration键下列出其完全限定名称:
1
|
|
如果我们用Maven构建项目,该文件应放在resources/META-INF目录下,在打包阶段最终会出现在上述位置。
为了指示自动配置类在Bean已经存在时退缩,我们可以使用 @ConditionalOnMissingBean 注解。
这个注解最值得注意的属性是:
当放在一个用@Bean装饰的方法上时,目标类型默认为该方法的返回类型:
1 2 3 4 5 6 |
|
传统上,我们将Web应用打包成WAR文件,然后将其部署到外部服务器上。这样做可以让我们在同一台服务器上安排多个应用程序。在CPU和内存稀缺的时候,这是一个节省资源的好方法。
但事情已经发生了变化。现在计算机硬件相当便宜,人们的注意力已经转向服务器配置。在部署过程中,配置服务器的一个小错误可能会导致灾难性的后果。
Spring通过提供一个插件,即spring-boot-maven-plugin,将网络应用打包成可执行的JAR,来解决这个问题。
要包含这个插件,只需在pom.xml中添加一个插件元素:
1 2 3 4 |
|
有了这个插件,我们在执行打包阶段后会得到一个扁平的JAR。这个JAR包含所有必要的依赖,包括一个嵌入式服务器。因此,我们不再需要担心配置外部服务器的问题。
然后我们可以像运行普通的可执行JAR一样运行该应用程序。
注意,pom.xml文件中的打包元素必须设置为jar来构建JAR文件:
1
|
|
如果我们不包括这个元素,它也默认为jar。
要构建一个WAR文件,我们把包装元素改为war:
1
|
|
并将容器的依赖关系从打包的文件中删除:
1 2 3 4 5 |
|
在执行Maven打包阶段后,我们会有一个可部署的WAR文件。
就像其他Java程序一样,Spring Boot命令行应用程序必须有一个main方法。
这个方法作为一个入口点,它调用SpringApplication#run
方法来启动应用程序:
1 2 3 4 5 6 7 |
|
然后SpringApplication类启动Spring容器并自动配置Bean。
注意我们必须向run方法传递一个配置类,作为主要的配置源。按照惯例,这个参数就是入口类本身。
在调用运行方法后,我们可以像普通程序一样执行其他语句。
Spring Boot提供了对外部配置的支持,使我们能够在不同的环境中运行同一个应用程序。我们可以使用属性文件、YAML文件、环境变量、系统属性和命令行选项参数来指定配置属性。
然后,我们可以使用@Value注解、通过@ConfigurationProperties注解的绑定对象或环境抽象来访问这些属性。
Spring Boot中的宽松绑定适用于配置属性的类型安全绑定。
通过宽松的绑定,属性的键不需要与属性名完全匹配。这样的环境属性可以用camelCase、kebab-case、snake_case,或者用大写字母,用下划线隔开单词。
例如,如果一个带有 @ConfigurationProperties 注解的 bean 类中的一个属性被命名为 myProp,它可以被绑定到这些环境属性中的任何一个:myProp、my-prop、my_prop 或 MY_PROP。
Spring Boot开发者工具,或称DevTools,是一套使开发过程更容易的工具。
为了包括这些开发时的功能,我们只需要在pom.xml文件中添加一个依赖项:
1 2 3 4 |
|
如果应用程序在生产中运行,spring-boot-devtools模块会自动禁用。重新打包的归档文件也默认排除了这个模块。所以,它不会给我们的最终产品带来任何开销。
默认情况下,DevTools应用适合于开发环境的属性。这些属性禁用了模板缓存,启用了网络组的调试日志,诸如此类。因此,我们有了这种合理的开发时配置,而无需设置任何属性。
只要classpath上的文件发生变化,使用DevTools的应用程序就会重新启动。这在开发中是一个非常有帮助的功能,因为它可以快速反馈修改情况。
默认情况下,静态资源,包括视图模板,不会引发重启。相反,资源变化会触发浏览器刷新。请注意,只有在浏览器中安装了LiveReload扩展,以便与DevTools包含的嵌入式LiveReload服务器进行交互时,这才会发生。
关于这一主题的进一步信息,请参见Spring Boot DevTools概述。
在为Spring应用程序运行集成测试时,我们必须有一个ApplicationContext。
为了让我们的生活更轻松,Spring Boot为测试提供了一个特殊的注解–@SpringBootTest。该注解从其classes属性所指示的配置类中创建一个ApplicationContext。
如果classes属性没有设置,Spring Boot会搜索主要的配置类。搜索从包含测试的包开始,直到找到一个用@SpringBootApplication或@SpringBootConfiguration注解的类。
有关详细说明,请查看我们的Spring Boot测试教程。
从本质上讲,Actuator通过启用生产就绪的功能,使Spring Boot应用程序活起来。这些功能使我们能够在应用程序在生产中运行时监控和管理它们。
将Spring Boot Actuator集成到一个项目中非常简单。我们所要做的就是在pom.xml文件中包括spring-boot-starter-actuator启动器:
1 2 3 4 |
|
Spring Boot Actuator可以使用HTTP或JMX端点来暴露操作信息。但大多数应用程序都会选择HTTP,其中端点的身份和/actuator前缀构成了一个URL路径。
下面是Actuator提供的一些最常见的内置端点。
请参考我们的Spring Boot Actuator教程,了解详细情况。
与属性文件相比,YAML有很多优点。
但是,由于它的缩进规则,编写它可能有点困难,而且容易出错。
有关细节和工作样本,请参考我们的Spring YAML与属性教程。
Spring Boot提供的主要注释位于org.springframework.boot.autoconfigure及其子包中。
以下是一些基本的注释:
Spring Boot注解提供了对这一主题的更多见解。
我们可以通过以下方式改变嵌入Spring Boot中的服务器的默认端口。
1
|
|
截至目前,Spring MVC支持Tomcat、Jetty和Undertow。Tomcat是Spring Boot的Web Starter支持的默认应用服务器。
Spring WebFlux支持Reactor Netty、Tomcat、Jetty和Undertow,其中Reactor Netty为默认。
在Spring MVC中,如果要改变默认,比方说改变为Jetty,我们需要排除Tomcat,在依赖关系中包括Jetty:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
同样,要把WebFlux的默认值改为UnderTow,我们需要排除Reactor Netty,并把UnderTow纳入依赖关系。
比较Spring Boot中的嵌入式Servlet容器有更多关于我们可以在Spring MVC中使用的不同嵌入式服务器的细节。
在为企业开发应用程序时,我们通常要处理多种环境,如开发、QA和生产。这些环境的配置属性是不同的。
例如,我们可能在开发中使用嵌入式H2数据库,但生产环境可能有专用的Oracle或DB2。即使DBMS在不同的环境中是相同的,URLs也肯定是不同的。
为了使这个问题简单明了,Spring提供了配置文件,以帮助分离每个环境的配置。因此,可以将这些属性保存在不同的文件中,如application-dev.properties和application-prod.properties,而不是通过编程来维护这些属性。默认的application.properties使用spring.profiles.active指向当前活动的配置文件,这样就可以获得正确的配置。
Spring Profiles给出了关于这个主题的全面观点。
本文介绍了技术面试中可能出现的关于Spring Boot的一些最关键问题。
我们希望这些问题能够帮助你找到理想的工作。
Spring MVC是Spring公司在Servlet API基础上建立的原创Web框架。它提供了模型-视图-控制器架构,可用于开发灵活的Web应用。
在本教程中,我们将重点讨论与之相关的问题,因为它经常是Spring开发者求职面试的一个话题。
关于Spring框架的更多问题,你可以查看我们面试问题系列中另一篇与Spring有关的文章。
Spring MVC实现了清晰的关注点分离,使我们能够轻松开发和单元测试我们的应用程序。
像如下概念:
是完全相互独立的,它们只负责一件事。
因此,MVC给了我们相当大的灵活性。它是基于接口的(有提供的实现类),我们可以通过使用自定义接口来配置框架的每一部分。
另一件重要的事情是,我们并没有被束缚在一个特定的视图技术上(例如JSP),而是可以选择我们最喜欢的技术。
另外,我们不只在Web应用开发中使用Spring MVC,在创建RESTful Web服务时也是如此。
@Autowired
注解的作用是什么?@Autowired
注解可以与字段或方法一起使用,用于按类型注入Bean。这个注解允许Spring解析并将协作Bean注入你的Bean中。
更多细节,请参考关于@Autowired
in Spring的教程。
@ModelAttribute
注解是Spring MVC中最重要的注解之一。它将一个方法参数或方法返回值绑定到一个命名的模型属性上,然后将其暴露给Web视图。
如果我们在方法层面使用它,它表明该方法的目的是添加一个或多个模型属性。
另一方面,当作为方法参数使用时,它表示该参数应该从模型中获取。当不存在时,我们应该首先将其实例化,然后将其添加到模型中。一旦出现在模型中,我们应该从所有具有匹配名称的请求参数中填充参数字段。
关于这个注解的更多信息可以在我们与@ModelAttribute
注解有关的文章中找到。
@Controller
和@RestController
注释的主要区别在于,@RestController
注解会自动包含@ResponseBody
。这意味着我们不需要用@ResponseBody
来标注我们的处理方法。如果我们想直接在HTTP响应体中写入响应类型,在@Controller
类中需要这样做。
我们可以使用@PathVariable
注解处理方法的参数,来提取URI模板变量的值。
例如,如果我们想从www.mysite.com/user/123
,通过id获取一个用户,我们应该把控制器中的方法映射为/user/{id}
:
1 2 |
|
@PathVariable只有一个名为value的元素。它是可选的,我们用它来定义URI模板变量的名称。如果我们省略value元素,那么URI模板变量的名称必须与方法参数名称相匹配。
也允许有多个@PathVariable注解,可以通过一个接一个地声明它们:
1 2 3 |
|
或将它们全部放在一个Map<String, String>
或MultiValueMap<String, String>
中:
1 2 |
|
Spring MVC默认支持JSR-303规范。我们需要在我们的Spring MVC应用中添加JSR-303及其实现的依赖性。例如,Hibernate Validator就是我们可以使用的JSR-303的实现之一。
JSR-303是用于bean验证的Java API规范,是Jakarta EE和JavaSE的一部分,它使用@NotNull
、@Min
和@Max
等注解,确保bean的属性满足特定的标准。关于验证的更多信息,请参见Java Bean验证基础知识一文。
Spring提供了@Validator
注解和BindingResult类。当我们有无效的数据时,Validator实现将在控制器的请求处理方法中触发错误。然后我们可以使用BindingResult类来获取这些错误。
除了使用现有的实现,我们还可以制作自己的实现。要做到这一点,我们首先创建一个符合JSR-303规范的注解。然后,我们实现Validator类。另一种方法是实现Spring的Validator接口,并通过控制器类中的@InitBinder
注解将其设置为验证器。
要查看如何实现和使用你自己的验证器,请看关于Spring MVC中自定义验证的教程。
@RequestBody
和@ResponseBody
注解?@RequestBody
注解,作为处理方法参数使用,将HTTP请求主体与传输或域对象绑定。Spring使用Http消息转换器自动将传入的HTTP请求反序列化为Java对象。
当我们在Spring MVC控制器中的处理方法上使用@ResponseBody
注解时,它表明我们将把该方法的返回类型直接写入HTTP响应体中。我们不会把它放在Model中,Spring也不会把它解释为视图名称。
请查看关于@RequestBody
和@ResponseBody
的文章,了解关于这些注解的更多细节。
Model接口定义了一个模型属性的持有人。ModelMap也有类似的目的,它能够传递一个值的集合。然后,它把这些值当作是在一个Map内。我们应该注意,在模型(ModelMap)中我们只能存储数据。我们把数据放进去并返回一个视图名称。
另一方面,在ModelAndView中,我们返回对象本身。我们把所有需要的信息,比如数据和视图名称,都设置在我们要返回的对象中。
你可以在关于Model、ModelMap和ModelView的文章中找到更多细节。
@SessionAttributes
注解是用来在用户会话中存储模型属性的。我们在控制器类中使用它,如我们关于Spring MVC中的会话属性的文章中所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
在前面的例子中,如果@ModelAttribute
和@SessionAttributes
有相同的名称属性,模型属性 “todos "将被添加到会话中。
如果我们想从一个全局管理的会话中获取现有的属性,我们将使用@SessionAttribute
注解作为方法参数:
1 2 3 4 5 |
|
@EnableWebMVC
的目的是什么?@EnableWebMvc
注解的目的是通过Java配置启用Spring MVC。它等同于XML配置中的<mvc: annotation-driven>
。这个注解从WebMvcConfigurationSupport导入Spring MVC配置。它能够支持@Controller
注解的类,这些类使用@RequestMapping
将传入的请求映射到处理方法。
你可以在我们的Spring @Enable
注解指南中了解更多关于这个和类似注解的信息。
ViewResolver通过将视图名称映射到实际视图,使应用程序能够在浏览器中渲染模型,这样无需将实现与特定的视图技术联系起来。
关于ViewResolver的更多细节,请看我们的Spring MVC中的ViewResolver指南。
BindingResult是org.springframework.validation
包中的一个接口,表示绑定结果。我们可以用它来检测和报告提交表单中的错误。它很容易被调用–我们只需要确保把它作为一个参数放在我们要验证的表单对象之后。可选的Model参数应该在BindingResult之后,这在自定义验证器教程中可以看到:
1 2 3 4 5 6 7 8 9 |
|
当Spring看到@Valid
注解时,它首先会尝试为被验证的对象找到验证器。然后,它将拾起验证注解并调用验证器。最后,它将把发现的错误放在BindingResult中,并把后者添加到视图模型中。
表单后备对象或命令对象只是一个POJO,它从我们要提交的表单中收集数据。
我们应该记住,它不包含任何逻辑,只包含数据。
要了解如何在Spring MVC中使用表单支持对象,请看我们关于Spring MVC中表单的文章。
@Qualifier
注解的作用是什么?它与@Autowired
注解同时使用,以避免一个bean类型的多个实例出现时的混淆。
让我们看一个例子。我们在XML配置中声明了两个类似的Bean:
1 2 3 4 5 6 |
|
当我们试图连接Bean时,我们会得到一个org.springframework.beans.factory.NoSuchBeanDefinitionException
。为了解决这个问题,我们需要使用@Qualifier
来告诉Spring关于哪个Bean应该被连接:
1 2 3 |
|
@Required
注解的作用是什么?@Required
注解用于setter方法,它表示在配置时必须填充具有该注解的bean属性。否则,Spring容器将抛出一个BeanInitializationException异常。
另外,@Required
与@Autowired
不同–因为它只限于 setter ,而@Autowired
则不是。@Autowired
也可以用来与构造函数和字段进行连接,而@Required
只检查该属性是否被设置。
让我们看一个例子:
1 2 3 4 5 6 7 8 |
|
现在,Person Bean的名字需要像这样在XML配置中设置:
1 2 3 |
|
请注意,@Required
默认情况下不能与基于Java的@Configuration
类一起工作。如果你需要确保所有的属性都被设置,你可以在@Bean
注解的方法中创建Bean时这样做。
译者点评:@Required
是如何实现的?
RequiredAnnotationBeanPostProcessor 通过检查 bean 定义中标注了 @Required
的属性是否有赋值来实现的。
在前端控制器模式中,所有的请求将首先进入前端控制器,而不是Servlet。它将确保响应已经准备好,并将它们送回给浏览器。这样,我们就有一个地方可以控制来自外部世界的一切。
前端控制器将识别应该首先处理请求的Servlet。然后,当它从servlet那里得到数据后,它将决定渲染哪个视图,最后,它将把渲染好的视图作为一个响应发送回去:
要查看实现细节,请查看我们的Java中的前端控制器模式指南。
Model1和Model2代表了在设计Java Web应用时经常使用的两种设计模式。
在Model1中,一个请求被送到一个servlet或JSP那里进行处理。Servlet或JSP处理请求,处理业务逻辑,检索和验证数据,并生成响应:
由于这种架构很容易实现,我们通常在小型和简单的应用程序中使用它。
另一方面,它对于大规模的网络应用并不方便。这些功能通常在JSP中重复使用,其中业务和表现逻辑是耦合的。
Model2是基于模型-视图-控制器设计模式的,它将视图与操作内容的逻辑分开。
此外,我们可以区分MVC模式中的三个模块:模型、视图和控制器。模型代表一个应用程序的动态数据结构。它负责数据和业务逻辑的操作。视图负责显示数据,而控制器作为前两者之间的接口。
在Model2中,一个请求被传递给控制器,控制器处理所需的逻辑,以便获得应该显示的正确内容。然后,控制器将内容放回请求中,通常是作为一个JavaBean或POJO。它还决定哪个视图应该渲染内容,最后将请求传递给它。然后,视图就会渲染数据:
根据Spring官方文档,@Component是任何Spring管理的组件的通用定型。@Repository、@Service和@Controller是@Component的特殊化,用于更具体的使用情况,例如,分别用于持久层、服务层和表现层。 让我们来看看后三者的具体使用情况:
简单地说,在前端控制器设计模式中,一个控制器负责将传入的HttpRequests引导到应用程序的所有其他控制器和处理程序。
Spring的DispatcherServlet实现了这种模式,因此,它负责正确协调HttpRequests到正确的处理程序。
另一方面,ContextLoaderListener启动和关闭了Spring的根WebApplicationContext。它将ApplicationContext的生命周期与ServletContext的生命周期联系起来。我们可以用它来定义在不同Spring上下文中工作的共享bean。
关于DispatcherServlet的更多细节,请参考本教程。
MultipartResolver接口是用来上传文件的。Spring框架提供了一个使用Commons FileUpload 的 MultipartResolver实现,另一个使用Servlet 3.0多部分请求解析。
使用这些,我们可以在我们的Web应用程序中支持文件上传。
Spring MVC拦截器允许我们拦截一个客户端请求,并在三个地方进行处理–在处理之前、处理之后或完成之后(当视图被渲染时)。 拦截器可以用于跨领域的关注,避免重复的处理程序代码,如记录、改变Spring模型中全局使用的参数等。
关于细节和各种实现,请看Spring MVC HandlerInterceptor介绍这篇文章。
一个用@InitBinder注解的方法被用来定制一个请求参数、URI模板和后备/命令对象。我们在控制器中定义它,它有助于控制请求。在这个方法中,我们注册和配置我们的自定义PropertyEditors,格式化器和验证器。
注解中有'value'元素。如果我们不设置它,@InitBinder注解的方法将在每个HTTP请求中被调用。如果我们设置了这个值,这些方法将只适用于特定的命令/表单属性,与/或名称与'value'元素对应的请求参数。
重要的是要记住,其中一个参数必须是WebDataBinder。其他参数可以是处理方法支持的任何类型,除了命令/表单对象和相应的验证结果对象。
@ControllerAdvice注解允许我们编写适用于大范围控制器的全局代码。我们可以将控制器的范围与选定的包或特定的注解联系起来。
默认情况下,@ControllerAdvice适用于用@Controller(或@RestController)注释的类。如果我们想更具体一些,我们还有一些属性可以使用。
如果我们想把适用的类限制在一个包内,我们应该在注释中加入包的名字:
1 2 3 |
|
也可以使用多个包,但这次我们需要使用一个数组而不是String。
除了通过包的名字限制到包之外,我们还可以通过使用该包中的一个类或接口来实现:
1
|
|
“assignableTypes"元素将@ControllerAdvice应用于特定的类,而 "annotations"则是针对特定的注释。
值得注意的是,我们应该把它和@ExceptionHandler一起使用。这种组合将使我们能够配置一个全局的、更具体的错误处理机制,而不需要每次都为每个控制器类实现它。
@ExceptionHandler注解允许我们定义一个处理异常的方法。我们可以独立使用该注解,但将其与@ControllerAdvice一起使用是更好的选择。因此,我们可以建立一个全局性的错误处理机制。这样一来,我们就不需要在每个控制器中编写异常处理的代码。
让我们看看Spring的REST错误处理这篇文章中的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
我们还应该注意,这将为所有抛出IllegalArgumentException或IllegalStateException的控制器提供@ExceptionHandler方法。用@ExceptionHandler声明的异常应该与作为方法参数的异常相匹配。否则,异常解析机制将在运行时失败。
这里需要记住的一点是,为同一个异常定义多个@ExceptionHandler是可能的。但我们不能在同一个类中这样做,因为Spring会通过抛出一个异常并在启动时失败来抱怨。
另一方面,如果我们在两个独立的类中定义这些,应用程序就会启动,但它会使用它找到的第一个处理程序,可能是错误的。
在Spring MVC中,我们有三种处理异常的方法。
如果在Web请求处理过程中抛出一个未处理的异常,服务器将返回一个HTTP 500响应。为了防止这种情况,我们应该用@ResponseStatus注解来注释我们的任何自定义异常。这类异常由HandlerExceptionResolver来解决。
当一个控制器方法抛出我们的异常时,这将导致服务器以指定的状态码返回一个适当的HTTP响应。我们应该记住,我们不应该在其他地方处理我们的异常,这种方法才会有效。
另一种处理异常的方法是使用@ExceptionHandler注解。我们在任何控制器中添加@ExceptionHandler方法,用它们来处理从该控制器中抛出的异常。这些方法可以在没有@ResponseStatus注解的情况下处理异常,将用户重定向到一个专门的错误视图,或者建立一个完全自定义的错误响应。
我们也可以传入与Servlet相关的对象(HttpServletRequest, HttpServletResponse, HttpSession, 和Principal)作为处理方法的参数。但是,我们应该记住,我们不能把模型对象直接作为参数。
处理错误的第三个选择是通过@ControllerAdvice类。它将允许我们应用同样的技术,只是这次是在应用层面,而不仅仅是在特定的控制器上。为了实现这一点,我们需要同时使用@ControllerAdvice和@ExceptionHandler。这样,异常处理程序将处理任何控制器抛出的异常。
关于这个话题的更多详细信息,请浏览Spring的REST错误处理文章。
译者点评:Spring 官方博客的这篇Exception Handling in Spring MVC,对 Spring MVC 的异常处理介绍的很全面,值得反复研读。
在这篇文章中,我们已经探讨了一些Spring MVC相关的问题,这些问题可能会在Spring开发者的技术面试中出现。你应该把这些问题作为进一步研究的起点,因为这绝不是一个详尽的列表。
我们祝愿你在即将到来的面试中有好运气!
在本教程中,我们将看看在求职面试中可能出现的一些最常见的与 Spring 有关的问题。
Spring是开发 Java 企业版应用程序最广泛使用的框架。此外,Spring 的核心功能可用于开发任何 Java 应用程序。
我们使用它的扩展功能在 Jakarta EE 平台之上构建各种网络应用。我们也可以在简单的独立应用程序中使用它的依赖注入功能。
Spring 的目标是使 Jakarta EE的 开发更容易,所以我们来看看它的好处:
依赖注入是控制反转(IoC)的一个方面,它是一个一般的概念,即我们不手动创建我们的对象,而是描述它们应该如何被创建。然后IoC容器将在需要时实例化所需的类。
更多细节,请看这里。
为了注入Spring Bean,有几个不同的选择。
配置可以使用XML文件或注解来完成。
更多细节,请查看这篇文章。
推荐的方法是对强制性的依赖使用构造函数参数,对选择性的依赖使用 setter 方法。这是因为构造函数注入允许向不可变的字段注入值,使测试更容易。
译者:问题可以继续拓展:字段注入的缺点?
Reference: What exactly is Field Injection and how to avoid it?
BeanFactory 表示一个提供和管理 Bean 实例的容器接口。默认的实现是在调用 getBean()
时懒惰地将 Bean 实例化。
相比之下,ApplicationContext 表示一个容纳了应用程序中元数据和 bean 等所有信息的容器接口。它也扩展了 BeanFactory 接口,但默认实现是在应用程序启动时迫切地实例化 Bean。然而,这种行为可以为单个 Bean 重写。
关于所有的区别,请参考文档。
译者点评:正如它们的名字一样,BeanFactory 是代表它是 Bean 工厂,它的主要功能是像工厂一样生产管理 Bean;ApplicationContext 则表示它是应用上下文,它是 BeanFactory 的子接口,扩展了很多面向应用的功能,它提供了国际化支持和框架事件体系,更易于创建实际应用。
Spring Bean 是由 Spring IoC 容器初始化的 Java 对象。
译者点评:个人觉得 Spring 官方文档的解释更好:
In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans.
在 Spring 中, 那些被 Spring IoC 容器管理并形成应用骨架的对象称为 beans。
默认情况下,Spring Bean 被初始化为一个单例。
为了设置Spring Bean的作用域,我们可以使用 @Scope
注解或在 XML 配置文件中使用 "scope"
属性。请注意,有五个支持的作用域。
关于差异,请看这里。
单例 Bean 不是线程安全的,因为线程安全是关于执行的,而单例是一种专注于创建的设计模式。线程安全只取决于Bean的实现本身。
首先,Spring Bean 需要根据 Java 或 XML Bean 定义进行实例化。它可能还需要执行一些初始化,使其进入可用状态。之后,当不再需要该 Bean 时,它将被从IoC 容器中删除。
所有初始化方法的整个周期显示在图片中(来源)。
译者点评:作者的回答在实际面试时可能会略显单薄,这里我尝试补充一下:
首先应用上下文会读取配置元数据,然后解析元数据用 BeanDefinition 来表达 Bean 定义;之后先实例化实现了 BeanFactoryPostProcessor 接口的 Bean,排序后调用它们依次处理 Bean 定义信息;
如果 Bean 定义中存在实现了 InstantiationAwareBeanPostProcessor 接口的 Bean,则调用它的 postProcessBeforeInstantiation
方法;然后实例化 Bean,之后又调用 InstantiationAwareBeanPostProcessor 的 postProcessAfterInstantiation
和 postProcessPropertyValues
方法;
如果 Bean 实现了 BeanNameAware 接口则调用它的 setBeanName
方法;
如果 Bean 实现了 BeanFactoryAware 接口则调用它的 setBeanFactory
方法;
如果 Bean 实现了 ApplicationAware 接口则调用它 setApplicationContext
方法;
调用 BeanPostProcessor 的 postProcessBeforeInitialization
方法;
如果 Bean 实现了 InitializingBean 接口则调用它的 afterPropertiesSet
方法;
调用 init-method
属性 或 @postConstruct
注解设置的初始化方法;
调用 BeanPostProcessor 的 postProcessAfterInitialization
方法;
如果 Bean 的作用域是原型则直接将准备就绪 Bean 对象返回给调用者;
如果 Bean 的作用域是单例则要先把它保存到容器的 Bean 对象缓存池中,然后将准备就绪的对象返回给调用者;
之后容器销毁时,如果 Bean 实现了 DisposableBean 的 destroy
接口;
调用 destroy-method
属性或 @preDestroy
注解设置的销毁方法;
它是一种以类型安全的方式配置基于 Spring 的应用程序的方法。它是基于 XML 的配置的替代品。
另外,要把一个项目从 XML 配置迁移到 Java 配置,请参考这篇文章。
是的,在大型项目中,建议拥有多个 Spring 配置以提高可维护性和模块化程度。
我们可以加载多个基于 Java 的配置文件:
1 2 3 |
|
或者我们可以加载一个XML文件,该文件将包含所有其他配置:
1
|
|
在这个XML文件中,我们将有以下内容:
1 2 |
|
Spring Security 是 Spring 框架的一个独立模块,主要是在 Java 应用程序中提供认证和授权方法。它还负责处理大多数常见的安全漏洞,如CSRF攻击。
要在Web应用程序中使用Spring Security,我们可以通过简单的注解 @EnableWebSecurity 来开始。
欲了解更多信息,我们有一系列与安全有关的文章。
Spring Boot 是一个提供了一套预配置框架以减少模板配置的项目。这样,我们就可以用最少的代码来启动和运行一个 Spring 应用程序。
译者点评:可能按照创建型、结构型和行为模式从 GoF 23 个设计模式中匹配会容易记忆点:
创建型模式
结构型模式
行为模式
原型作用域意味着每次我们需要 Bean 的一个实例时,Spring都会创建一个新的实例并返回它。这与默认的单例作用域不同,在单例作用域中,每个 Spring IoC 容器只实例化一个对象实例。
我们可以通过实现 Spring-aware 的接口来做到这一点。这里有完整的列表。
我们也可以在这些 Bean 上使用 @Autowired 注解:
1 2 3 4 5 |
|
简单地说,所有由 DispatcherServlet 处理的请求都会被引导到带有 @Controller
注解的类。每个控制器类都将映射一个或多个请求到方法中,这些方法处理和执行携带输入的请求。
退一步讲,我们建议看一下典型的Spring MVC架构中的前端控制器的概念。
译者点评:个人觉得原文这题给的答案不是很好,题目是问什么是 Spring MVC 中的控制器,答案应该重点解释是什么,而且说所有由 DispatcherServlet 处理的请求都会被引导到带有 @Controller
注释的类太绝对了,例如 BeanNameUrlHandlerMapping 就支持将 URL 映射到对应名字的 bean。
Spring MVC 中的控制器是处理请求的组件,充当模型-视图-控制器模式中的控制器角色,通常是由 @Controller
注解的类。
@RequestMapping
注解是如何工作的?@RequestMapping
注解用于将 Web 请求映射到Spring 控制器方法。除了简单的用例之外,我们还可以用它来映射 HTTP 头,用 @PathVariable
来绑定URI的部分内容,以及用 URI 参数和 @RequestParam
注解来工作。
关于 @RequestMapping
的更多细节可以在这里找到。
更多关于 Spring MVC 的问题,请查看我们关于 Spring MVC 面试问题的文章。
译者点评:个人觉得这个题出得不怎么好,给的答案也有点答非所问。单纯说 @RequestMapping
注解是如何工作的?那答案应该重点说@RequestMapping
注解会将映射请求所需的匹配信息保留到 Java 运行时,出题者更多想考察的应该是 Spring MVC 是如何将请求映射到 @RequestMapping
注解的方法。
Spring JDBC 模板是数据库操作主要的API,我们可以通过它访问我们感兴趣的数据:
为了使用它,我们需要定义 DataSource 的简单配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
如需进一步解释,请查看这篇快速文章。
有两种不同的方式来配置事务–使用注解或使用面向切面编程(AOP)–每种方式都有其优势。
根据官方文档,以下是使用 Spring Transactions 的好处。
Spring 数据访问对象(DAO)是 Spring 为 JDBC、Hibernate 和 JPA 等数据访问技术提供的支持,其工作方式一致且简单。
有一个完整的系列讨论了 Spring 的持久性,提供了一个更深入的解释。
切面使跨领域的关注点模块化,如事务管理,它跨越多种类型和对象,在不修改受影响的类的情况下为已有的代码增加额外的行为。
下面是基于切面的执行时间记录的例子。
译者点评:
In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns. –Wikipedia
在计算机领域,面向方面的编程(AOP)是一种编程范式,旨在通过允许跨领域的关注点分离来提高模块化程度。
根据官方文档,编织是一个将各个切面与其他应用程序类型或对象联系起来以创建一个增强对象的过程。这可以在编译时、加载时或运行时完成。Spring AOP 和其他纯 Java AOP 框架一样,在运行时执行织入。
在这篇长文中,我们已经探讨了一些关于 Spring 技术面试最重要的问题。
我们希望这篇文章能对即将到来的Spring面试有所帮助。祝您好运!
1 2 3 4 5 6 7 8 9 10 |
|
第一个问题是升级 ibus-devel
失败,这是因为 ibus-devel
在 PowerTools 的仓库中,默认可能没有使能这个仓库,我们可以手动使能安装或升级:sudo dnf --enablerepo=powertools install ibus-devel
。
第二个问题是 CentOS 8 上有最新的 marisa-0.2.4-36.el8.x86_64
,但是只有marisa-devel-0.2.4-4.el7.x86_64
, 这个有点奇怪,不知道为什么两个版本没有同步升级,marisa-devel
一般是用于开发,我们可能暂时用不到,可以先尝试卸载它完成升级:sudo dnf remove marisa-devel
。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
第三个问题是 kyotocabinet 的版本问题导致 distro-sync
失败。我查询了一下系统中安装的 kyotocabinet 版本又确实是 1.2.77-1.el7
,也可以查询到迁移成功了:
1 2 3 4 5 6 7 8 9 10 11 |
|
通过 Syncing packages
搜索 migrate2rocky 脚本,这已经是最后三步了,于是尝试手动完成剩下的工作。 先 dnf --allowerasing distro-sync
,然后查看会删除哪些软件,我这里是会删除 kyotocabinet,根据情况确认是否接受删除软件同步。
最后的 Disable Stream repos 和移除 subscription-manage
,因为是 CentOS 8, 所以并不需要。
重启之后又遇到新的问题:
1
|
|
查看 /var/log/vboxadd-setup.log
,
1 2 3 4 5 6 7 8 |
|
经过一番搜索,我意识到可能是 VBoxGuestAdditions.iso
的版本不对,因为之前确实有提示类似 unable to insert the virtual optical disk /usr/share/virtualbox/vboxguestadditions.iso
的错误,原因是我没有把之前版本的 VBoxGuestAdditions.iso
从虚拟光驱中弹出,于是先弹出再插入新版本,果然新版本成功安装。
我这里是用 VirtualBox 搭建 CentOS 7 集群,宿主机是内存 16 GB 加 SSD 1 TB 的 MacBook Pro。集群的核心是选择网络模式,VirtualBox 的网络模式概况如下:
Mode | VM -> Host | VM <- Host | VM1 <-> VM2 | VM -> Net/LAN | VM <- Net/LAN |
---|---|---|---|---|---|
Host-only | + | + | + | - | - |
Internal | - | - | + | - | - |
Bridged | + | + | + | + | + |
NAT | + | Port forward | - | + | Port forward |
NAT service | + | Port forward | + | + | Port forward |
我希望集群的机器可以互相访问并能访问网络,从上面的列表可知,我们可以选择 Bridged,或者 NAT service 搭配 Port forward。在搭建之初,我搜索了相关的资料,但是并没找到特别理想的,这篇 Setting up CentOS 7 nodes 勉强还凑合,于是我主要参考它来搭建,网络模式也同样是选择的 Bridged,它还使用了双网卡,虚拟机之间通信使用的 Internal Networking, 感觉上性能可能会好点,没实测。双网卡并不是必须的,VirtualBox 对 Internal Networking 的说明是其在安全方面更有优势:
Even though technically, everything that can be done using internal networking can also be done using bridged networking, there are security advantages with internal networking. In bridged networking mode, all traffic goes through a physical interface of the host system. It is therefore possible to attach a packet sniffer such as Wireshark to the host interface and log all traffic that goes over it. If, for any reason, you prefer two or more VMs on the same machine to communicate privately, hiding their data from both the host system and the user, bridged networking therefore is not an option.
由于 CentOS 主要是运行服务,我们可以使用 Minimal 安装,这样可以减少资源开销,安装好后,默认是没有启用网络的, 网络相关的配置是在 /etc/sysconfig/network-scripts
目录下,配置文件的命令惯例是 ifcfg-enp0sX
, X
是整数,我这里是 ifcfg-enp0s3
和 ifcfg-enp0s8
,将 /etc/sysconfig/network-scripts/ifcfg-enp0s3
和 /etc/sysconfig/network-scripts/ifcfg-enp0s8
中的 ONBOOT
改成 yes
。
构建 Internal Network 时,手动去指定 ip 会很麻烦,VirtualBox 给了我们另一个选择:
Unless you configure the virtual network cards in the guest operating systems that are partici- pating in the internal network to use static IP addresses, you may want to use the DHCP server that is built into Oracle VM VirtualBox to manage IP addresses for the internal network.
我们可以 10.0.0.0/8
, 172.16.0.0/12
和 192.168.0.0/16
选择一个合适的私有 ip 地址范围来构建 Internal Network,我这里选择 172.16.0.0/12
。
在宿主机下运行命令 /Applications/VirtualBox.app/Contents/MacOS/VBoxManage dhcpserver add --netname=intnet --server-ip=172.16.0.1 --netmask=255.240.0.0 --lower-ip=172.16.0.2 --upper-ip=172.16.255.255 --enable
创建好 DHCP server。
之后我们可以利用 VirtualBox 的克隆功能来扩展我们的集群节点。
构建镜像时问题表现是牵涉到连接网络的命令会失败,google 一圈之后,找到使用 --network=host
的偏方,成功绕过问题。而多容器之间无法通信却一时措手无策。现在就只有 --network=host
这一个线索,于是就想加上这个参数后有什么区别呢?
在官方文档上搜寻一圈之后我没查到有用信息,于是我想相关的书可能会讲讲 docker 网络这块,在微信读书上找了杨保华、戴王剑和曹亚仑合著的《Docker 技术入门与实战(第3版)》。因为 docker 技术在快速发展,所以书中的命令与当前版本有些许差异,但问题不大,不妨碍理解。这本书有两节专门介绍 docker 网络,于是我便先看了这两节,从这里得到一条重要线索:docker 和宿主机的通信是依靠防火墙转发。--network=host
也验证了这条线索,加上这参数会直接使用宿主机的网络配置,这样 docker 和宿主机的通信就不需要防火墙转发了。
问题现在定位到了防火墙,那么为什么防火墙不转发 docker 的网络数据包呢?是不是哪条防火墙规则没配对?另外我还怀疑是不是我的实践环境有问题?我的实践环境是这样的:物理主机是 macOS,上面安装 virtulbox,使用 virtulbox 创建了一个 CentOS 8 的虚拟机, 虚拟机的网络模式是 NAT。这种情况无疑增加了问题排查的难度,怎么来排查呢?
我先看了一下 virtulbox 的用户手册中的网络模式介绍,NAT 是可以连接到宿主机,而且现在虚拟机是可以访问网络的。如果想快速定位问题,我想还得从网络请求的数据包入手,想办法来跟踪数据包。
于是就来查怎么调试 iptables。查到可以通过 TRACE 和 LOG 来输出日志,看介绍也没看出这俩有什么区别。凭经验觉得 TRACE 好像比 LOG 后出来,看起来也高大上一点,先试着用 TRACE 。Google 了一圈,找到都是 CentOS 6 或 CentOS 7 相关的配置,也只能先将就着用吧。
我先是参考的 CentOS implements iptables log output and debugging through raw table
1 2 |
|
输出的结果是 NONE,于是尝试显示设置 sudo sysctl net.netfilter.nf_log.2=ipt_LOG
结果给我报一个 sysctl: setting key "net.netfilter.nf_log.2": No such file or directory
, 为什么没有这个 key 呢?于是我查询一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
明明有这个 key 啊, Google 一圈不得要领。继续搜索, 找到这篇 Tracing iptables on CentOS – Cheat sheet, 它提到的方法如下:
1 2 |
|
因为我对 linux 模块相关命令还是有点了解,知道 modprobe
是用来加载模块, modinfo
可以查看模块的信息,我发现 nf_log_ipv4
这个模块在我的系统中已经加载,所以我就直接尝试 sudo sysctl net.netfilter.nf_log.2=nf_log_ipv4
, 得到的仍然是 sysctl: setting key "net.netfilter.nf_log.2": No such file or directory
,不得不说这很迷,为什么会一直报这么一个不相关的错误。
没办法,继续搜索, 找到 IPTables. Setting nf_log kernel parameter,它其中提到:
I think they split the xt_LOG code in newer kernel versions and you need to modprobe nf_log_ipv4 now and sysctl net.netfilter.nf_log.2=nf_log_ipv4 (assuming you want to trace ipv4 packets)
很奇怪,为什么其他人都可以设置,我这里却不行,而且这三篇文章用的方法都类似,不至于啊,google 到的也就这几篇文章,于是我回过头去再次研读第一篇文章,在想要不要试下作者提供 CentOS 7 系列的配置方法,毕竟版本更接近,而且这三篇文章都提到用 nf_log_ipv4
, 这时我发现他的方法里 modprobe nf_log_ipv4
之后并不需要设置, net.netfilter.nf_log.2
的值便设置了,于是我也照做,终于配置成功。我觉得这里可能是 CentOS 8 有 bug,因为明明有 key 却设置不上,现在设置成功,也就不管那么多了,继续解决问题要紧。
我到 /etc/rsyslog.conf
中开启内核日志输出:
1
|
|
重启日志输出服务 sudo systemctl restart rsyslog.service
,往防火墙里添加 TRACE 规则:
1 2 |
|
监视系统日志 sudo tail -f /var/log/messages
重启 docker 服务 sudo systemctl restart docker.service
, 运行容器 sudo docker run -it busybox
, 然后在 busybox 容器中触发网络请求:
1 2 |
|
系统日志并没有输出, 查看 sudo dmesg
也没有日志输出。真是让挫败啊,没办法,只能退而求其次改用 LOG。
1 2 3 4 5 6 7 |
|
同样重启日志服务 sudo systemctl restart rsyslog.service
, 监视系统日志 sudo tail -f /var/log/messages
,重启 docker 服务 sudo systemctl restart docker.service
, 运行容器 sudo docker run -it busybox
, 然后在 busybox 容器中触发网络请求:
1 2 |
|
日志是成功输出了,内心着实高兴了一把,但由于我的规则设置太宽泛,输出的太多了,很难找到有用的信息,脑中闪过一个念头,那把规则设置更严格一点不就可以了,搓搓小手,兴奋地实践起来,于是我把规则调整成容器发出的网络数据包:
1 2 |
|
果然,相关日志输出少多了,只有两条,具体如下:
1 2 3 4 |
|
这对解决问题帮助不大,摔!这几乎要击垮我了,但还是心有不甘。于是只能把希望再次寄托给 TRACE,我在 CentOS 8 上直接 man iptables
,发现它和网络上找到的 man page 内容确实有些差异, 它的 target 专门独立到 iptables-extensions
:
iptables can use extended packet matching and target modules. A list of these is available in the iptables-extensions(8) man‐page.
继续 man 8 iptables-extensions
,搜索 TRACE, 相关介绍如下:
This target marks packets so that the kernel will log every rule which match the packets as those traverse the tables, chains,rules. It can only be used in the raw table.
With iptables-legacy, a logging backend, such as ip(6)t_LOG or nfnetlink_log, must be loaded for this to be visible. The packets are logged with the string prefix: “TRACE: tablename:chainname:type:rulenum ” where type can be “rule” for plain rule, “return” for implicit rule at the end of a user defined chain and “policy” for the policy of the built in chains.
With iptables-nft, the target is translated into nftables' meta nftrace expression. Hence the kernel sends trace events via netlink to userspace where they may be displayed using xtables-monitor –trace command. For details, refer to xtables-monitor(8).
原来配合 iptables-nft
时, 日志是使用 xtables-monitor --trace
, 似乎又看到了一丝曙光,于是赶紧删除 LOG 规则,添加 TRACE 规则:
1 2 3 4 5 6 7 |
|
准备就绪之后就按前面相关的步骤触发网络请求,果然成功输出日志,而且输出了很多日志,我往屏幕下面翻,最后竟然输出的 “Failed to received netlink message: No buffer space available”,为什么报错了呢?我尝试 man xtables-monitor
,文档上明明说的是: xtables-monitor will run until the user aborts execution, typically by using CTRL-C.我这里为什么报错终止了呢?Google 一圈一无所获,想到之前 LOG 因为规则太宽松输出了很多日志,就想是不是这规则太宽松导致日志太多,缓冲区不够用,赶紧调整 TRACE 规则:
1 2 3 4 5 6 7 |
|
这下好了,日志正常输出,也没有报错了。于是仔细查看日志,终于找到: firewalld:filter_FORWARD:rule:0x93:DROP
,原来是 firewall 将包丢掉了。可是对 firewall 一点也不熟,先找了一遍 firewall 教程看了一下,但是还不知道怎么在 firewall 中添加规则,要学会添加规则也得花点力气,这时脑海中冒出另一个想法:我何不直接停止 firewall,确认下是不是 firewall 导致的问题。
1 2 3 4 |
|
尝试一下,发现可以了,确定确实是 firewall 导致的问题。那么现在是自己研究 firewall 添加规则吗?有点复杂啊,我在想是不是我 docker 版本太低了,检查一下确实比最新的版本要低,新版本可能解决了。在文档也确实找到了对应描述:
If you are running Docker version 20.10.0 or higher with firewalld on your system with –iptables enabled, Docker automatically creates a firewalld zone called docker and inserts all the network interfaces it creates (for example, docker0) into the docker zone to allow seamless networking.
于是升级新版本,新版本也确实解决了这个问题,嗯,这下暂时不用研究 firewall 添加规则了。
这次能成功排查 docker 的网络问题还是挺开心的,说实话一开始我是没信心的,毕竟才开始接触 docker,另一方面 linux 网络这块牵涉到的知识非常多,甚至在心里想实在解决不了就算了,直接在 macOS 上来学好了。得到的启发是不管有没有解决问题,首先解决问题的方法要对,方法对了之后还得要坚持;其次是注意自己软件使用的版本,相关的命令参数可能需要查看对应版本的文档去核实;最后是虽然项目的官方文档是入门的不错材料,但很多时候官方文档写得很浅或者对新手不友好,这时候如果觉得项目值得投入时间学习的话,看相关的书籍是一个很好的选择,对系统学习和精进大有禆益。
Tim Chien 和 Robert Nyman 的这篇 CSS Length Explained 帮了我的大忙,本文就是基于它而写成。
我们经常是用英寸为度量单位来表示手机屏幕尺寸,一英寸相当于2.54厘米或0.0254米。
计算机屏幕显示事物的单位是像素。显示屏上的单个物理 “光点",能够独立于它的邻居显示出完整的颜色,被称为像素(图片元素)。我们把屏幕上的物理像素称为 "设备像素"。
DPI 是 dots per inch 的英文缩写,即每英寸点数; PPI 是 pixels per inch 的缩写,即每英寸像素。 它们都用来表示显示像素密度 (Display pixel density)。
计算机屏幕是由大量发光二极管整齐排列构成的集成电路,由于屏幕制造商工艺水平差异,每英寸集成电路上排列的二极管的数量会不一样,屏幕出厂时我们可以从厂商那里得知屏幕的 PPI。
于是我们可以知道:
1
|
|
例如 MacBook Air(2011) 的 DPI 为 125 , 所以:
1
|
|
CSS像素的尺寸大致可以看成是人的肉眼能够舒适地看到的尺寸,不要太小,这样你就得眯着眼睛,也不要大到让你看到像素化。"看得很舒服 “ 的定义比较笼统,W3C CSS规范中给我们一个推荐的参考。
The reference pixel is the visual angle of one pixel on a device with a pixel density of 96 DPI and a distance from the reader of an arm’s length.
如前所述,观看距离因人而异,因设备而异,这就是为什么我们必须将设备按外形因素分类的原因。推荐的参考观看距离(“一臂之长”)和参考像素密度(“96 DPI”)其实是历史数据。
对于21世纪的日常设备,我们有不同的参考建议:
Device | Baseline pixel density | Width/height of one CSS pixel | Viewing distance |
---|---|---|---|
A 20th century PC with CRT display | 96 DPI | ~0.2646 mm (1/96in) | 28 in (71.12cm) |
Modern laptop with LCD | 125 DPI | 0.2032 mm (1/125in) | 21.5 in (54.61cm) |
Smartphones/Tablets | 160 DPI | ~0.159mm (1/160 in) | 16.8in (42.672cm) |
因此,我们在 CSS 的世界里建立了一个基本的事实:一个 CSS 像素会以不同的物理尺寸显示,但它总是以正确的尺寸显示,让浏览者感到舒适。
随着我们步入未来,现在很多智能手机在出厂时都采用了高密度的显示屏。为了保证 CSS 像素在每一个访问网络的设备(即一切有屏幕和网络连接的设备)上的尺寸一致,设备制造商不得不将多个设备像素映射到一个 CSS 像素上,以弥补它相对更大的物理尺寸。CSS 像素相对于设备像素的尺寸比就是设备像素比(DPPX)。
我们以 iPhone 4 为最著名的例子。它配备了一块 326 DPI 的显示屏。根据我们上面的表格,作为一款智能手机,它的典型观看距离是 16.8 英寸,它的基准像素密度是 160DPI。为了创建一个 CSS 像素,苹果选择将设备像素比设置为 2,这就等于让 iOS Safari 显示网页的方式和 163 DPI 手机上一样。
在我们继续之前,先回头看看上面的数字。其实我们可以做得更好,不把设备像素比设置为2,而是设置为326/160=2.0375
,让一个 CSS 像素与参考尺寸相比完全一样。不幸的是,这样的比例会导致一个意想不到的结果:由于每个 CSS 像素并不是由整个设备像素来显示的,所以浏览器不得不对所有的位图图像、边框等进行反锯齿,因为它们几乎总是被当作整个 CSS 像素来显示。浏览器很难利用2.0375个设备像素来绘制你的1个CSS像素宽的边框:如果比例是简单的2,那就容易多了。
顺带一提,163 DPI恰好是上一代 iPhone 的像素密度,所以网页的工作方式也是一样的,不需要开发者对自己的网站进行任何特殊的"升级"。
设备制造商通常选择1.5,或2,或其他整数作为 DPPX 值。偶尔,有些设备决定不这么玩了,发货时使用1.325 DPPX这样的值;作为开发者,我们也许应该忽略这些设备。
现在我们就比较清楚 CSS pixel 和 device pixel 的关系了。接下来我们看下 iOS 的 point 和 device pixel 的关系。
The coordinate system iOS uses to place content onscreen is based on measurements in points, which map to pixels in the display. A standard-resolution display has a
1:1
pixel density (or@1x
), where one pixel is equal to one point. High-resolution displays have a higher pixel density, offering a scale factor of 2.0 or 3.0 (referred to as@2x
and@3x
). As a result, high-resolution displays demand images with more pixels.
从 Apple 这段描述可知, scale factor (@1x
, @2x
和 @3x
) 就是我们上面据说的设备像素比(DPPX)。point 和 css pixel 是对应的。
那 dp 和 device pixel 又是什么关系呢?
To preserve the visible size of your UI on screens with different densities, you must design your UI using density-independent pixels (dp) as your unit of measurement. One dp is a virtual pixel unit that’s roughly equal to one pixel on a medium-density screen (160dpi; the “baseline” density). Android translates this value to the appropriate number of real pixels for each other density.
Google 这段描述更加直接,dp 是一个虚拟的像素单位,大致相当于中密度屏幕上的一个像素(160dpi;“基线"密度),所以 dp 和 css pixel 也是对应的。而 xhdpi, xxhdpi 和 xxxhdpi 是表示设备像素比(DPPX)2、3 和 4。
现在我们还剩下设计稿的 px。我们回忆一下在前端开发时,如果我们不指定图片尺寸而直接去显示设计师的切图,这时图片是有一个固有尺寸的,在设备像素比为1的设备上,这个固有尺寸就是图片的尺寸,而在设备像素比为2上尺寸是图片的尺寸除以2,所以设计稿的 px是对应设备像素(device pixel)的,这也是为什么我们需要提供多套图片来做适配。假设我们不提供多套图片,现在我们有一个 100 x 100 css pixel
的图片, 在设备像素比为3的设备上也会去加载 100 x 100 device pixel
尺寸的资源图,按上面的分析,实际它应该加载 300 x 300 device pixel
尺寸的资源图,那么相当于资源图上一个像素点会对应显示三个设备像素点,这样可能会出现模糊或锯齿的情况。
理清了各平台尺寸单位的关系以及它们与设备像素的关系后,我们来看下设备尺寸。
我们先看下 iOS 设备尺寸分布:
型号 | points | 物理像素 | 设备像素比(DPPX) |
---|---|---|---|
2G,3G,3GS | 320 x 480 | 320 x 480 | 1 |
4,4S | 320 x 480 | 640 x 960 | 2 |
5,5C,5S,SE | 320 x 568 | 640 x 1136 | 2 |
6,6S,7,8,SE2 | 375 x 667 | 750 x 1334 | 2 |
6+,6S+,7+,8+ | 414 x 736 | 1080 x 1920 | 3 |
11Pro,X,Xs | 375 x 812 | 1125 x 2436 | 3 |
11, Xr | 414 x 896 | 828 x 1792 | 2 |
11Pro Max,Xs Max | 414 x 896 | 1242 x 2688 | 3 |
对于 iOS 来说,现在的主流设备应该是从 6,6S,7,8,SE2
开始,对应的设备像素是750 x 1334 px
。
再来看下 android 这边, Google 有一个 Screen sizes and densities 统计表,本文写作时查询的结果如下:
ldpi | mdpi | tvdpi | hdpi | xhdpi | xxhdpi | Total | |
---|---|---|---|---|---|---|---|
Small | 0.1% | 0.1% | 0.2% | ||||
Normal | 0.3% | 0.3% | 14.8% | 41.3% | 26.1% | 82.8% | |
Large | 1.7% | 2.2% | 0.8% | 3.2% | 2.0% | 9.9% | |
Xlarge | 4.2% | 0.2% | 2.3% | 0.4% | 7.1% | ||
Total | 0.1% | 6.2% | 2.7% | 17.9% | 45.0% | 28.1% |
Small,Normal,Large 和 Xlarge 是屏幕的尺寸分类,具体含义如下:
small: Screens that are of similar size to a low-density QVGA screen. The minimum layout size for a small screen is approximately 320x426 dp units. Examples are QVGA low-density and VGA high density.
normal: Screens that are of similar size to a medium-density HVGA screen. The minimum layout size for a normal screen is approximately 320x470 dp units. Examples of such screens a WQVGA low-density, HVGA medium-density, WVGA high-density.
large: Screens that are of similar size to a medium-density VGA screen. The minimum layout size for a large screen is approximately 480x640 dp units. Examples are VGA and WVGA medium-density screens.
xlarge: Screens that are considerably larger than the traditional medium-density HVGA screen. The minimum layout size for an xlarge screen is approximately 720x960 dp units. In most cases, devices with extra-large screens would be too large to carry in a pocket and would most likely be tablet-style devices. Added in API level 9.
从上表的数据可知,目前 android 设备的主流尺寸分布是从 normal-hdpi 这个分类开始,根据 google 对 normal 的解释,它的大小相当于 medium-density HVGA 屏幕上的 320x470 dp
,换算成设备像素就是 480x705 px
,注意如果我们以这个尺寸去设计的话,那么得到的切图就是对应 hdpi(1.5),要输出xhdpi(2) 的切图则要放大 1.3333 倍,这就有点不方便了,很容易得到奇数的像素尺寸,所以我们将 480x705 px
换算到 xhdpi(2) 的设备像素,得到 640x940 px
。
我们知道宽屏比窄屏能显示更多内容,如果我们以宽屏为其准尺寸设计,那么在窄屏上就可能出现控件放不下、文字截断的情况。反过来,如果我们以窄屏为基准设计,那么在宽屏上布局时会容易处理,控件的宽度增加或者间隔增加就可以了。高度和宽度存在同样的问题,所以也应该选高度小的作为基准。设计时扣除固定元素高度之和后分配给可滚动区域,这样方便界面的元素布局能够动态响应,开发更好做屏幕适配。
所以选择基准尺寸和我们想支持的设备紧密相关,这需要基于多方面的因素考虑。 例如,如果我们希望支持尽可能多的设备,就越有可能获取更多用户,但开发的兼容工作量就相应增加,很多新特性就可能不适合作为应用的主要功能,而只适合作为增强功能。通常可以考虑覆盖 90% 以上,团队资金和人员比较充足的话可以考虑覆盖 95%,98% 甚至更多。
以覆盖 90% 以上为例,如果我们同时支持 iOS 和 android,或只支持 android 时,应该选 640x940 px
作为基准尺寸,而如果只支持 iOS , 我们应该选 750 x 1334 px
作为基准尺寸。
iOS 的设备像素比主要分布在2和3,而 android 这边设备像素比主要分布在 1.5(hdpi), 2(xhdpi)和 3(xxhdpi),所以 iOS 需要输出@2x
和 @3x
两套切图; android 需要输出 hdpi, xhdpi 和 xxhdpi 三套切图。
现在我们知道,设计基准尺寸的选择以及切图的输出是和我们想支持的设备紧密相关,写作本文时:
750 x 1334 px
作为基准尺寸,对应的设备像素比是2640x940 px
作为基准尺寸,对应的设备像素比是2640x940 px
作为基准尺寸,对应的设备像素比是2@2x
和 @3x
两套切图基准尺寸对应的切图是 @2x
和 xhdpi,输出 hdpi(1.5) 则是切图缩小 0.75, @3x
和 xxhdpi(3) 则是切图放大 1.5 。
我们需要需要注意,随着设备的更新换代,我们的基准尺寸和切图会发生变化,就像以前我们可能需要为 android 提供 mdpi 的切图。
另外想说一下,设计师在设计之初就要把屏幕适配这事放在心上,将界面的元素看成水流一样,尽量让它们能自由流动,这样开发者就能更好地也更容易地做屏幕适配。Apple 在屏幕适配这块提出了 auto layout 的解决方案,这是一个设计师视角的解决方案,也是我们日常的生活中的视角,用界面元素的之间的约束来表达布局,推荐设计师用约束这种方式去做设计并最终输出。可以看到 google 实际上也很认可 auto layout 用约束来布局的想法,在新版本的 android 开发中默认的根布局容器就是 ConstraintLayout,它就是用约束来表达布局。最后我们再看 web 开发布局这边,css 布局的核心就是流,为支持屏幕适配,目前的主流方案是响应式布局,而这种布局的核心我认为仍然是约束。可以看到在屏幕适配这块,各平台最终的想法其实是一样的。
A:For a Table:
1 2 3 4 5 6 7 |
|
For a Column:
1 2 3 4 5 6 7 8 |
|
Reference:
]]>The easiest way to upload another project is to use the Open Source Software Repository Hosting (OSSRH), which is an approved repository provided by Sonatype for any OSS Project that want to get their artifacts into the Central Repository.
从 maven 的官方文档可知是使用 Open Source Software Repository Hosting (OSSRH), 于是我们可以参考她的指南。
这份指南勾勒了发布工作的主要流程,分别是:
Sonatype 使用 JIRA 管理请求,所以我们要创建 JIRA 帐号, 然后创建一个新工程工单。
为了确保中央仓库库中可用组件的最低质量水平,部署组件必须满足一些要求。这使组件的用户能够从中央仓库中提供的元数据中找到有关组件的所有相关细节。
这些要求是:
我们可以集成 maven 插件来提供 Javadoc 和源文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
我们需要用 GPG/PGP 对文件签名,以 macOS 为例:
1 2 3 4 5 6 7 8 9 |
|
生成 GPG 公私钥对并发布到 GPG 服务器后,我们还需要将 GPG 提供给 maven, 这是通过 maven settings。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
macOS 上我还遇到了 gpg: signing failed: Inappropriate ioctl for device 这个错误,通过在 ~/.bash_profile
中加入如下配置解决了:
1 2 |
|
元数据信息要包括:
对于 Correct Coordinates, 我这里是使用 github 托管代码,于是我们可以按照 maven 官方文档的建议:
My project is hosted at a project hosting service like SourceForge or Github, what should I use as groupId? If your project name is foo at SourceForge, you can use net.sf.foo. If your username is bar on Github, you can use com.github.bar. You can also use another reversed domain name you control. The group ID does not have to reflect the project host.
至此,准备工作就完成了,接下来可以进入部署环节了。先还是要做些相关配置,主要是:
为了配置 Maven,使其能够通过 Nexus Staging Maven 插件部署到 OSSRH 的 Nexus Repository Manager上,你必须进行如下配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
另外,如果你使用 Maven 部署插件,这是默认行为,你需要添加一个完整的distributionManagement部分。
1 2 3 4 5 6 7 8 9 10 |
|
上述配置将从你的Maven settings.xml文件中获取用户账户的详细信息来部署到OSSRH。认证的最小设置是:
1 2 3 4 5 6 7 8 9 |
|
由于生成javadoc和源代码jars以及使用GPG签署组件是一个相当耗时的过程,这些执行通常从正常的构建配置中分离出来,并转移到一个配置文件中。然后,当通过激活配置文件进行部署时,该配置文件又会被使用。
1 2 3 4 5 6 7 8 9 10 |
|
这些配置做完之后,最终得到的 pom.xml 类似如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
|
做好相关配置之后就可以真正部署了,主要有两种部署方式:
推荐的方式是使用 Nexus Staging Maven Plugin。
Performing a Snapshot Deployment
当你的版本以-SNAPSHOT
结尾时,会进行快照部署。当执行快照部署时,您不需要满足要求,只需在工程上运行 mvn clean deploy
SNAPSHOT版本不同步到中央版本库。如果您希望您的用户使用您的 SNAPSHOT 版本,他们需要将快照库添加到他们的 Nexus Repository Manager、settings.xml 或 pom.xml 中。成功部署的SNAPSHOT版本可以在https://oss.sonatype.org/content/repositories/snapshots/
找到。
Performing a Release Deployment
为了执行发布部署,你必须在所有的POM文件中编辑你的版本,以使用发布版本。这意味着它们不能以-SNAPSHOT
结尾,此外插件和依赖性声明也不能使用快照版本。这保证了你只能依赖其他发布的组件。理想情况下,它们都在中央仓库中可用。这确保了你的用户可以从中央仓库中检索你的组件以及你的过渡性依赖。
在多模块设置中,可以手动或借助Maven版本插件来更改项目的版本和父级引用。
1
|
|
一旦你更新了所有的版本,并确保你的构建没有部署就通过了,你就可以使用发布配置文件进行部署,并使用
1
|
|
Maven发布插件可以用来自动完成对Maven POM文件的修改、健康检查、所需的SCM操作和实际部署执行。
Maven发布插件的配置应该包括禁用Maven super POM 中的发布配置文件,因为我们使用的是我们自己的配置文件,并在激活发布配置文件的同时指定部署目标。
1 2 3 4 5 6 7 8 9 10 11 |
|
在SCM连接配置正确的情况下,您可以通过以下方式向OSSRH进行发布部署。
1
|
|
回答版本和标签的提示,然后是
1
|
|
由于使用了Nexus Staging Maven Plugin,并将autoReleaseAfterClose设置为true,这个执行将一次性部署到OSSRH并发布到中央仓库。
在前面的介绍中,我们提到使用 Nexus Staging Maven Plugin,并将autoReleaseAfterClose设置为true,部署到OSSRH后会发布到中央仓库。我们也可以手动执行 mvn nexus-staging:release
来发布 staging repository。
由于我希望库既能用于多页面,也能用于 Angular 单页面,所以需要支持 UMD 和 ES Harmony。由于 Angular 是使用 TypeScript 开发,最好还能提供用于 TypeScript 的声明文件。最原始的想法自然是手动按要求提供各种文件,但这样工作量比较大,也不容易扩展。那么还有什么容易的办法吗?有的,最核心的想法就是库的源码只写一份,然后用工具生成各种模块系统需要文件。具体的做法可能有差异,但理念是一样的。
从我的需求出发,我最终选择用 TypeScript 来写库的源码,基于脱敏的考虑,这里选择 TypeScript 文档中的示例代码来演示。
首先我们建立好库的源码目录结构并配置好源码管理:
1 2 3 4 5 6 |
|
然后使用 npm init
生成 package.json
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
其次是按需求编辑好 package.json
文件。这是关键步骤,package.json
的 main 字段通常用于指向 UMD 版本的库;module 字段则用于指向 ES 版本的库。我们还需要配置构建脚本生成对应版本的库,最终的 package.json
文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
然后是编写 TypeScript 的配置文件 tsconfig.json
, 先使用命令 npm install @tsconfig/recommended --save-dev
安装推荐的配置,之后根据需求定制,最终的内容如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
其次是测试,测试是库开发的重要环节,它能帮我们验证库是否正常工作,后续迭代重构也要依赖它。通常的做法是使用测试框架,像 Angular 是 Karma test runner 搭配Jasmine test framework , 我们可以参考选择。
我这里还玩了一下用 rollup 打包, 然后在浏览器里运行测试用例。首先在库工程目录外重新创建一个测试工程,然后使用 npm link
命令来安装我们的开发库,具体目录结构和文件内容如下:
1 2 3 4 5 6 7 8 9 10 |
|
Test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
rollup.config.js
1 2 3 4 5 6 7 8 9 10 |
|
tsconfig.json
1 2 3 4 5 6 7 8 9 10 |
|
最后是根据需要选择发布方式,例如发布到 npm 公有仓库,具体做法参考官方文档就好了。
A:为了数据安全,我们需要对系统备份,换新电脑或者更换云服务时我们需要迁移系统。Linux 系统备份和迁移的方法很多,我这里打算使用 tar 。
首先是根据自己的实际情况列出需要备份的目录,通常有:
/etc/
/home/
/var/spool/mail/
/var/spool/cron/
/root
/usr/local/bin
然后使用 tar 命令打包:
1 2 3 |
|
首先可以将备份解压到 /tmp
目录,之后使用 rsync 命令复制到对应目录便可恢复。
1 2 3 4 5 6 7 |
|
换新电脑或者更换云服务时我们可能不想要上面那么麻烦,而可能想直接迁移系统,至少我是这么想的,这时我们可以使用下面的方法:
1 2 3 4 5 6 7 8 |
|
Reference:
A:The information is provided for nearly all command with “-v” option. See:
Loaded plugins: builddep, changelog, config-manager, copr, debug, debuginfo-install, download, generate_completion_cache, needs-restarting, playground, product-id, repoclosure, repodiff, repograph, repomanage, reposync, subscription-manager, uploadprofile Updating Subscription Management repositories.
Reference:
A:We can add by gnome tweaks. Of course you should install it with Software.
Reference:
A:首先是安装 tomcat-native
;其次是注意从日志文件中定位错误。我遇到了证书文件权限导致找不到文件的情况。
$LC_*
and $LANG
are correctA:问题是由于 ssh 终端的 locale 设置导致系统的 locale 设置出现问题,我关闭了 sshd_config 中 locale 相关的设置,使用系统的 locale 设置。
Reference:
A: 在 CentOS 8 上安装 java 包之后不知为什么 alternatives 中的配置居然不对,导致提示 java command not found,于是只好手动配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
另外我们还可以在 /etc/profile.d
目录下新建 java.sh
文件来设置 JAVA_HOME
和 JRE_HOME
:
1 2 3 |
|
Reference:
A:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
~
结尾的文件A:原因是加上了 b 选项,会对文件做备份
1
|
|
Reference:
A: 我们可以利用 ExecStopPost 设置,以 mysql 为例,先准备好邮件发送程序,这里我们可以参考 Arch linux 的做法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
还要配置一下 mysql,还要给 mysql 加上合适执行权限
1 2 3 4 5 6 |
|
这里应该对 msql 的权限作更小的限制,但是设置单个命令的 sudo 规则没有生效,限于时间关系先暂时这样配置。
Referece:
/var/log/mysql/mysqld.log of '/var/log/mysql/mysqld.log '
A:In case the root user has a password, then you
have to create a /root/.my.cnf
configuration file
with the following content:
1 2 3 |
|
where "<secret>"
is the password.
ATTENTION: The /root/.my.cnf
file should be readable _ONLY_
by root !
Reference:
A:
1 2 3 4 5 |
|
Reference:
]]>A: mysql> show procedure status where db = 'db_for_mysql_crash_course'\G;
;
<<Web API 的设计与开发>>
, 我个人觉得这是一本对 Web API 进行全面、细致和深入剖析的书,对 Web API 的设计与开发很有帮助,值得一读。
书的内容是按照整分的逻辑组织,并依先易后难的顺序来讲解相关知识。下面我按自己的理解尝试对书中内容做个简单的总结。
Web API 是用于完成某种需求,由于需求会变化,所以一次就设计出完美 Web API 的想法是不现实的,所以一开始应该要给 Web API 的更改留有余地,这是很容易忽视的地方。推荐的做法是在 URI 中嵌入版本信息,典型的形式是 http://api.linkedin.com/v1/people
。
虽然一次就完美地设计 Web API 的想法不现实,但我们还是想尽量做好,减少 Web API 版本变更的次数,毕竟版本越多维护成本越高,那么我们该如何设计 Web API 呢?
Web API 通过 HTTP 协议来完成通信,在设计时我们应该最大程度地利用 HTTP 协议规范。基于标准协议设计的 API 至少要比使用私有协议设计的 API 更容易理解,还会减少使用时引入的 bug,使你的 API 得到更广泛的使用,提高利用已有的程序库或代码的可能。
有了整体设计原则后,我们来看下具体的请求和响应设计。API 的功能是为了完成项目的需求,最完备的请求会包含请求端点、请求方法、请求参数和请求数据体(Request Body),我们依次来审视请求的每个部分。
端点是指用于访问 API 的 URI,普适又重要的设计原则有:
端点设计的注意事项:
URI 和 HTTP 方法之间的关系可以认为是操作对象和操作方法的关系。如果把 URI 当作 API(HTTP) 的 “操作对象 = 资源”, HTTP 方法则表示 “进行怎样的操作”。通过用不同方法访问同一个 URI 端点,不但可以获取信息,还能修改信息、删除信息等,这样的思想正成为 Web API 设计的主流方式。
方法名 | 说明 |
---|---|
GET | 获取资源 |
POST | 新增资源 |
PUT | 覆盖已有资源 |
DELETE | 删除资源 |
PATCH | 更新部分资源 |
HEAD | 获取资源的元信息 |
有时请求可能还需要传递参数,在设计 URI 时,必须决定是把特定参数放在查询参数里还是路径里,决策的依据有以下两点:
请求数据体,个人认为可以采用面向对象编程的思想来设计,整个处理过程会轻松很多。
说完请求,让我们来看下响应。首先是正确使用状态码,国内由于历史原因遗留下来无论请求是否成功都一律返回 200 的问题,全站切换到 HTTPS 后,我们还是应该最大程度地利用 HTTP 规范,这样我们能受益于通用的 HTTP 程序库,减轻客户端的负担。
其次是数据格式,这里的数据格式是指该用怎样的形式来描述 API 返回的结构化数据,具体而言就是指 JSON、XML 等数据格式。关于这一点,事实上几乎没有可讨论的,因为我们通常就是使用 JSON 作为默认的数据格式,若有需求 API 也可以支持 XML 的格式,这是最贴近现实的做法。
再次是数据内部结构,我们重点看下数据应该以数组还是对象返回,作者更推荐使用对象来封装数据的方式,因为该方式有如下几个优点:
从次是各个数据的格式,各个数据项组成了最终的数据,只有掌握了如何处理单个数据项格式才能设计出合理的响应体数据格式。作者重点介绍了如何描述性别数据、日期格式和大整数,受益匪浅。
最后是出错信息的表示,同样,我们需要选择合适的状态码,出误信息建议以消息体的形式返回,出错信息应该包含详细的错误代码、人们能够读懂的相关信息,以及记载有详细说明的文档页面的 URI,如下所示:
1 2 3 4 5 6 7 |
|
如果想支持描述多个错误同时发生,可以返回出错信息数组,
1 2 3 4 5 6 7 8 9 |
|
以上是基础内容,作者最后还介绍进阶内容,开发牢固的 Web API,对我们把 Web API 设计和开发提高到新高度有非常大的帮助。
]]>Cordova hot code push 插件的原作者已经不维护了,我们可以选择一个可能最好的 fork 来使用。 gitpop2 可以帮助我们选择,我从中选择了当前 star 最多的一个 fork。
Ionic App 使用 cordova hot code push 实现热更新的基本步骤如下:
在 ionic 工程中添加 cordova hot code push plugin
$ ionic cordova plugin add https://github.com/snipking/cordova-hot-code-push.git
安装 Cordova Hot Code Push CLI client
$ npm install -g cordova-hot-code-push-cli
为指定平台编译工程
$ ionic cordova prepare android
执行插件初始化
$ cd /path/to/project/root
$ cordova-hcp init
生成插件配置文件
$ cordova-hcp build
运行到设备上
开发和发布应用新版本的 web
// 1. 开发
// 2. 为指定平台编译工程生成 web
$ ionic build --engine=cordova --platform=android
// 3. 生成新插件配置文件
$ cordova-hcp build
// 4. 部署到服务器
在使用的过程中遇到的第一个问题是更新之后白屏。使用 Chrome 的 remote devices 调试 android webview 找到了问题的原因,ionic 应用中 <base href="http://DamianSheldon.github.io/" />
, cordova hot code push 会将 web 代码拷贝到外部存储上,webview 使用形如 file:///data/user/0/com.tenneshop.liveupdatedemo/files/cordova-hot-code-push-plugin/2020.01.07-16.16.39/www/index.html
的路径来加载应用,此时 document.baseURI = /
,加载其他相对路径的 js 文件时,是相对这个路径,例如 <script src="cordova.js"></script>
,就是以 /cordova.js
去加载,于是就会提示找不到文件。从上面的分析我们也知道,解决问题的一个办法是修正 base href 的值,我们可以在 index.html 的 head 元素加入下面的代码:
1 2 3 |
|
这样我们就修正文件路径的问题,很不巧,虽然文件的路径是对了,但是 ionic 默认不响应 file schema 的请求,我们需要做些工作,先让 WebViewLocalServer.java 支持响应 file schema,将 createHostingDetails 改成如下实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
然后是 isLocalFile 方法:
1 2 3 4 5 6 7 |
|
做完这些工作后 ionic 就可以响应 file schema 请求了。
继续测试,我发现更新后第二次打开还是显示 App bundle asset 中的 web,这有点奇怪。仔细查看日志,确实有加载外部存储的 web , 但却被 http://localhost/
的请求覆盖了,这是什么原因呢?经过对代码逻辑的一番梳理,我发现是 IonicWebViewEngine 中 onPageStarted 方法的原因:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
MainActivity 触发 webview 加载 file:///android_asset/www/index.html
,然后 cordova hot code push plugin 启动工作,它会让 webview 加载外部存储的 web,之后 IonicWebViewEngine 的 onPageStarted 收到 file:///android_asset/www/index.html
的请求的回调,它先停止了 webview 的加载工作,即 cordova hot code push plugin 启动加载外部存储的 web 的请求,再开始 http://localhost/
的请求,也就是打印出来日志的记录。正是这个方法时序的问题导致成功更新之后再重启应用仍然加载 app bundle asset 的 web。一种解决办法是我们直接让 MainActivity 直接加载 http://localhost/
,就像下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这样热更新就可以正常工作了。
我继续做了点测试,又发现一个和 ionic icon 相关的问题,ionic 4 使用了 Fetch API 来请求 ionic icon 的 svg 资源,由于现在是使用 file schema 来指定资源路径,由于 Fetch API 不支持 file schema 所以就报错 Fetch API cannot load file:///xxx/www/svg/md-star.svg. URL scheme "file" is not supported.
我们得想办法来解决这个问题,一个办法替换 fetch 方法的实现,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
在这些测试过程中,我还发现 cordova hot code push 更新时只做了版本字符是否相等的判断,这在服务器端的版本低于本地版本时,插件仍然会做更新,这是有问题的,我们需要严格这里的判断,让它只有在服务端的版本高于本地版本时才做更新。相关代码位于 UpdateLoaderWorker 的 run 方法中。
最后一个要考虑的问题是如何将我们修改的代码和 ionic 的代码很好的整合起来?我现在的想法是创建一个私有的扩展 IonicWebViewEngine 和 WebViewLocalServer,然后借鉴 ionic 通过 config.xml 的 web 偏好设置的方法,像下面的代码:
1
|
|
回头测试下这个想法,好了有时间也许可以整理好代码提个 Pull Request。
Reference:
]]>安装之后打开系统出现闪屏,英语应该是称为 screen flicker,google 之后在 VirtualBox 的论坛找到解决方法:
进入单用户维护模式
a. 重启系统
b. 在菜单选择界面键入 e,进入 grub2 的指令编辑模式
c. 在指定内核和根文件系统这行最后加上 systemd.unit=rescue.target
d. 键入 ctrl + x 进入系统
强制使用 Xorg
a. 用 vim 打开 /etc/gdm3/custom.conf
b. 删除 WaylandEnable=false 前的 # 注释符号
c. 保存文件后,systemctl default 来进入正常模式
解决了闪屏之后,想通过虚拟机菜单中的调整窗口大小来让系统的屏幕全屏发现无用,想起来应该要安装 VirtualBox Guest Additions,于是插入虚拟机提供的光盘来安装。
首先是提示 kernel headers not found for target kernel 的错误,也提示详细的错误信息位于 /var/log/vboxadd-setup.log,我们可以通过查看该错误日志来找到对应解决方法。于是尝试安装对应的内核头文件,命令为 yum install kernel-headers kernel-devel
,之后执行 /sbin/rcvboxadd setup
.
仍然提示 kernel headers not found for target kernel,通过 uname -r
和 rpm -q kernel-headers
发现版本不一致,于是重启系统选择最新的内核版本。
再次尝试安装,提示 Error building the module,查看错误日志提示需要安装 libelf-dev, libelf-devel or elfutils-libelf-devel
,CentOS 上只有 elfutils-libelf-devel ,安装之后再次安装 VirtualBox Guest Additions。
提示
1 2 3 |
|
这个问题暂时没找到解决方法,但是可以让 CentOS 全屏了,就暂时先不管这个问题了。
在虚拟机使用过程中硬盘的空间会慢慢增加,但是即使虚拟机中删除了文件实际占用空间减少,外部的硬盘文件大小仍然没有减少,这对小硬盘电脑可伤不起,于是想办法释放虚拟机磁盘空间。大前提是虚拟机的硬盘类型是 Dynamically allocated storage,主要分为两大步:
下面以 Windows 10 为例:
1 2 |
|
1 2 3 4 |
|
Reference:
]]>