分类 技术 下的文章

通过管理一套图书的完整代码示例,来探索轻量级的 RESTful 服务。

Web 服务,以这样或那样的形式,已经存在了近二十年。比如,XML-RPC 服务出现在 90 年代后期,紧接着是用 SOAP 分支编写的服务。在 XML-RPC 和 SOAP 这两个开拓者之后出现后不久,REST 架构风格的服务在大约 20 年前也出现了。REST 风格(以下简称 Restful)服务现在主导了流行的网站,比如 eBay、Facebook 和 Twitter。尽管分布式计算的 Web 服务有很多替代品(如 Web 套接字、微服务和远程过程调用的新框架),但基于 Restful 的 Web 服务依然具有吸引力,原因如下:

  • Restful 服务建立在现有的基础设施和协议上,特别是 Web 服务器和 HTTP/HTTPS 协议。一个拥有基于 HTML 的网站的组织可以很容易地为客户添加 Web 服务,这些客户对数据和底层功能更感兴趣,而不是对 HTML 的表现形式感兴趣。比如,亚马逊就率先通过网站和 Web 服务(基于 SOAP 或 Restful)提供相同的信息和功能。
  • Restful 服务将 HTTP 当作 API,因此避免了复杂的软件分层,这种分层是基于 SOAP 的 Web 服务的明显特征。比如,Restful API 支持通过 HTTP 命令(POST-GET-PUT-DELETE)进行标准的 CRUD(增加-读取-更新-删除)操作;通过 HTTP 状态码可以知道请求是否成功或者为什么失败。
  • Restful Web 服务可以根据需要变得简单或复杂。Restful 是一种风格,实际上是一种非常灵活的风格,而不是一套关于如何设计和构造服务的规定。(伴随而来的缺点是,可能很难确定哪些服务不能算作 Restful 服务。)
  • 作为使用者或者客户端,Restful Web 服务与语言和平台无关。客户端发送 HTTP(S) 请求,并以适合现代数据交换的格式(如 JSON)接收文本响应。
  • 几乎每一种通用编程语言都至少对 HTTP/HTTPS 有足够的(通常是强大的)支持,这意味着 Web 服务的客户端可以用这些语言来编写。

这篇文章将通过一段完整的 Java 代码示例来探讨轻量级的 Restful 服务。

基于 Restful 的“小说” Web 服务

基于 Restful 的“小说” web 服务包含三个程序员定义的类:

  • Novel 类代表一个小说,只有三个属性:机器生成的 ID、作者和标题。属性可以根据实际情况进行扩展,但我还是想让这个例子看上去更简单一些。
  • Novels 类包含了用于各种任务的工具类:将一个 Novel 或者它们的列表的纯文本编码转换成 XML 或者 JSON;支持在小说集合上进行 CRUD 操作;以及从存储在文件中的数据初始化集合。Novels 类在 Novel 实例和 servlet 之间起中介作用。
  • NovelsServlet 类是从 HttpServlet 中继承的,HttpServlet 是一段健壮且灵活的代码,自 90 年代末的早期企业级 Java 就已经存在了。对于客户端的 CRUD 请求,servlet 可以当作 HTTP 的端点。 servlet 代码主要用于处理客户端的请求和生成相应的响应,而将复杂的细节留给 Novels 类中的工具类进行处理。

一些 Java 框架,比如 Jersey(JAX-RS)和 Restlet,就是为 Restful 服务设计的。尽管如此,HttpServlet 本身为完成这些服务提供了轻量、灵活、强大且充分测试过的 API。我会通过下面的“小说”例子来说明。

部署“小说” Web 服务

当然,部署“小说” Web 服务需要一个 Web 服务器。我的选择是 Tomcat,但是如果该服务托管在 Jetty 或者甚至是 Java 应用服务器上,那么这个服务应该至少可以工作(著名的最后一句话!)。在我的网站上有总结了如何安装 Tomcat 的 README 文件和代码。还有一个附带文档的 Apache Ant 脚本,可以用来构建“小说”服务(或者任何其他服务或网站),并且将它部署在 Tomcat 或相同的服务。

Tomcat 可以从它的官网上下载。当你在本地安装后,将 TOMCAT_HOME 设置为安装目录。有两个子目录值得关注:

  • TOMCAT_HOME/bin 目录包含了类 Unix 系统(startup.shshutdown.sh)和 Windows(startup.batshutdown.bat) 的启动和停止脚本。Tomcat 作为 Java 应用程序运行。Web 服务器的 servlet 容器叫做 Catalina。(在 Jetty 中,Web 服务器和容器的名字一样。)当 Tomcat 启动后,在浏览器中输入 http://localhost:8080/可以查看详细文档,包括示例。
  • TOMCAT_HOME/webapps 目录是已部署的 Web 网站和服务的默认目录。部署网站或 Web 服务的直接方法是复制以 .war 结尾的 JAR 文件(也就是 WAR 文件)到 TOMCAT_HOME/webapps 或它的子目录下。然后 Tomcat 会将 WAR 文件解压到它自己的目录下。比如,Tomcat 会将 novels.war 文件解压到一个叫做 novels 的子目录下,并且保留 novels.war 文件。一个网站或 Web 服务可以通过删除 WAR 文件进行移除,也可以用一个新版 WAR 文件来覆盖已有文件进行更新。顺便说一下,调试网站或服务的第一步就是检查 Tomcat 已经正确解压 WAR 文件;如果没有的话,网站或服务就无法发布,因为代码或配置中有致命错误。
  • 因为 Tomcat 默认会监听 8080 端口上的 HTTP 请求,所以本机上的 URL 请求以 http://localhost:8080/ 开始。

通过添加不带 .war 后缀的 WAR 文件名来访问由程序员部署的 WAR 文件:

http://locahost:8080/novels/

如果服务部署在 TOMCAT_HOME 下的一个子目录中(比如,myapps),这会在 URL 中反映出来:

http://locahost:8080/myapps/novels/

我会在靠近文章结尾处的测试部分提供这部分的更多细节。

如前所述,我的主页上有一个包含 Ant 脚本的 ZIP 文件,这个文件可以编译并且部署网站或者服务。(这个 ZIP 文件中也包含一个 novels.war 的副本。)对于“小说”这个例子,命令的示例(% 是命令行提示符)如下:

% ant -Dwar.name=novels deploy

这个命令首先会编译 Java 源代码,并且创建一个可部署的 novels.war 文件,然后将这个文件保存在当前目录中,再复制到 TOMCAT_HOME/webapps 目录中。如果一切顺利,GET 请求(使用浏览器或者命令行工具,比如 curl)可以用来做一个测试:

% curl http://localhost:8080/novels/

默认情况下,Tomcat 设置为 热部署 hot deploys :Web 服务器不需要关闭就可以进行部署、更新或者移除一个 web 应用。

“小说”服务的代码

让我们回到“小说”这个例子,不过是在代码层面。考虑下面的 Novel 类:

例 1:Novel 类

package novels;

import java.io.Serializable;

public class Novel implements Serializable, Comparable<Novel> {
    static final long serialVersionUID = 1L;
    private String author;
    private String title;
    private int id;

    public Novel() { }

    public void setAuthor(final String author) { this.author = author; }
    public String getAuthor() { return this.author; }
    public void setTitle(final String title) { this.title = title; }
    public String getTitle() { return this.title; }
    public void setId(final int id) { this.id = id; }
    public int getId() { return this.id; }

    public int compareTo(final Novel other) { return this.id - other.id; }
}

这个类实现了 Comparable 接口中的 compareTo 方法,因为 Novel 实例是存储在一个线程安全的无序 ConcurrentHashMap 中。在响应查看集合的请求时,“小说”服务会对从映射中提取的集合(一个 ArrayList)进行排序;compareTo 的实现通过 Novel 的 ID 将它按升序排序。

Novels 类中包含多个实用工具函数:

例 2:Novels 实用工具类

package novels;

import java.io.IOException;
import java.io.File;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.nio.file.Files;
import java.util.stream.Stream;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.Collections;
import java.beans.XMLEncoder;
import javax.servlet.ServletContext; // not in JavaSE
import org.json.JSONObject;
import org.json.XML;

public class Novels {
    private final String fileName = "/WEB-INF/data/novels.db";
    private ConcurrentMap<Integer, Novel> novels;
    private ServletContext sctx;
    private AtomicInteger mapKey;

    public Novels() {
        novels = new ConcurrentHashMap<Integer, Novel>();
        mapKey = new AtomicInteger();
    }

    public void setServletContext(ServletContext sctx) { this.sctx = sctx; }
    public ServletContext getServletContext() { return this.sctx; }

    public ConcurrentMap<Integer, Novel> getConcurrentMap() {
        if (getServletContext() == null) return null; // not initialized
        if (novels.size() < 1) populate();
        return this.novels;
    }

    public String toXml(Object obj) { // default encoding
        String xml = null;
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            XMLEncoder encoder = new XMLEncoder(out);
            encoder.writeObject(obj);
            encoder.close();
            xml = out.toString();
        }
        catch(Exception e) { }
        return xml;
    }

    public String toJson(String xml) { // option for requester
        try {
            JSONObject jobt = XML.toJSONObject(xml);
            return jobt.toString(3); // 3 is indentation level
        }
        catch(Exception e) { }
        return null;
    }

    public int addNovel(Novel novel) {
        int id = mapKey.incrementAndGet();
        novel.setId(id);
        novels.put(id, novel);
        return id;
    }

    private void populate() {
        InputStream in = sctx.getResourceAsStream(this.fileName);
        // Convert novel.db string data into novels.
        if (in != null) {
            try {
                InputStreamReader isr = new InputStreamReader(in);
                BufferedReader reader = new BufferedReader(isr);

                String record = null;
                while ((record = reader.readLine()) != null) {
                    String[] parts = record.split("!");
                    if (parts.length == 2) {
                        Novel novel = new Novel();
                        novel.setAuthor(parts[0]);
                        novel.setTitle(parts[1]);
                        addNovel(novel); // sets the Id, adds to map
                    }
                }
                in.close();
            }
            catch (IOException e) { }
        }
    }
}

最复杂的方法是 populate,这个方法从一个包含在 WAR 文件中的文本文件读取。这个文本文件包括了“小说”的初始集合。要打开此文件,populate 方法需要 ServletContext,这是一个 Java 映射类型,包含了关于嵌入在 servlet 容器中的 servlet 的所有关键信息。这个文本文件有包含了像下面这样的记录:

Jane Austen!Persuasion

这一行被解析为两部分(作者和标题),由感叹号(!)分隔。然后这个方法创建一个 Novel 实例,设置作者和标题属性,并且将“小说”加到容器中,保存在内存中。

Novels 类也有一些实用工具函数,可以将“小说”容器编码为 XML 或 JSON,取决于发出请求的人所要求的格式。默认是 XML 格式,但是也可以请求 JSON 格式。一个轻量级的 XML 转 JSON 包提供了 JSON。下面是关于编码的更多细节。

例 3:NovelsServlet 类

package novels;

import java.util.concurrent.ConcurrentMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.beans.XMLEncoder;
import org.json.JSONObject;
import org.json.XML;

public class NovelsServlet extends HttpServlet {
    static final long serialVersionUID = 1L;
    private Novels novels; // back-end bean

    // Executed when servlet is first loaded into container.
    @Override
    public void init() {
        this.novels = new Novels();
        novels.setServletContext(this.getServletContext());
    }

    // GET /novels
    // GET /novels?id=1
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        String param = request.getParameter("id");
        Integer key = (param == null) ? null : Integer.valueOf((param.trim()));

        // Check user preference for XML or JSON by inspecting
        // the HTTP headers for the Accept key.
        boolean json = false;
        String accept = request.getHeader("accept");
        if (accept != null && accept.contains("json")) json = true;

        // If no query string, assume client wants the full list.
        if (key == null) {
            ConcurrentMap<Integer, Novel> map = novels.getConcurrentMap();
            Object list = map.values().toArray();
            Arrays.sort(list);

            String payload = novels.toXml(list);        // defaults to Xml
            if (json) payload = novels.toJson(payload); // Json preferred?
            sendResponse(response, payload);
        }
        // Otherwise, return the specified Novel.
        else {
            Novel novel = novels.getConcurrentMap().get(key);
            if (novel == null) { // no such Novel
                String msg = key + " does not map to a novel.\n";
                sendResponse(response, novels.toXml(msg));
            }
            else { // requested Novel found
                if (json) sendResponse(response, novels.toJson(novels.toXml(novel)));
                else sendResponse(response, novels.toXml(novel));
            }
        }
    }

    // POST /novels
    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        String author = request.getParameter("author");
        String title = request.getParameter("title");

        // Are the data to create a new novel present?
        if (author == null || title == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

        // Create a novel.
        Novel n = new Novel();
        n.setAuthor(author);
        n.setTitle(title);

        // Save the ID of the newly created Novel.
        int id = novels.addNovel(n);

        // Generate the confirmation message.
        String msg = "Novel " + id + " created.\n";
        sendResponse(response, novels.toXml(msg));
    }

    // PUT /novels
    @Override
    public void doPut(HttpServletRequest request, HttpServletResponse response) {
        /\* A workaround is necessary for a PUT request because Tomcat does not
 generate a workable parameter map for the PUT verb. \*/
        String key = null;
        String rest = null;
        boolean author = false;

        /\* Let the hack begin. \*/
        try {
            BufferedReader br =
                new BufferedReader(new InputStreamReader(request.getInputStream()));
            String data = br.readLine();
            /\* To simplify the hack, assume that the PUT request has exactly
 two parameters: the id and either author or title. Assume, further,
 that the id comes first. From the client side, a hash character
 # separates the id and the author/title, e.g.,

 id=33#title=War and Peace
 \*/
            String[] args = data.split("#");      // id in args[0], rest in args[1]
            String[] parts1 = args[0].split("="); // id = parts1[1]
            key = parts1[1];

            String[] parts2 = args[1].split("="); // parts2[0] is key
            if (parts2[0].contains("author")) author = true;
            rest = parts2[1];
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }

        // If no key, then the request is ill formed.
        if (key == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

        // Look up the specified novel.
        Novel p = novels.getConcurrentMap().get(Integer.valueOf((key.trim())));
        if (p == null) { // not found
            String msg = key + " does not map to a novel.\n";
            sendResponse(response, novels.toXml(msg));
        }
        else { // found
            if (rest == null) {
                throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));
            }
            // Do the editing.
            else {
                if (author) p.setAuthor(rest);
                else p.setTitle(rest);

                String msg = "Novel " + key + " has been edited.\n";
                sendResponse(response, novels.toXml(msg));
            }
        }
    }

    // DELETE /novels?id=1
    @Override
    public void doDelete(HttpServletRequest request, HttpServletResponse response) {
        String param = request.getParameter("id");
        Integer key = (param == null) ? null : Integer.valueOf((param.trim()));
        // Only one Novel can be deleted at a time.
        if (key == null)
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));
        try {
            novels.getConcurrentMap().remove(key);
            String msg = "Novel " + key + " removed.\n";
            sendResponse(response, novels.toXml(msg));
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }
    }

    // Methods Not Allowed
    @Override
    public void doTrace(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    @Override
    public void doHead(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    @Override
    public void doOptions(HttpServletRequest request, HttpServletResponse response) {
        throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
    }

    // Send the response payload (Xml or Json) to the client.
    private void sendResponse(HttpServletResponse response, String payload) {
        try {
            OutputStream out = response.getOutputStream();
            out.write(payload.getBytes());
            out.flush();
        }
        catch(Exception e) {
            throw new RuntimeException(Integer.toString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
        }
    }
}

上面的 NovelsServlet 类继承了 HttpServlet 类,HttpServlet 类继承了 GenericServlet 类,后者实现了 Servlet 接口:

NovelsServlet extends HttpServlet extends GenericServlet implements Servlet

从名字可以很清楚的看出来,HttpServlet 是为实现 HTTP(S) 上的 servlet 设计的。这个类提供了以标准 HTTP 请求动词(官方说法, 方法 methods )命名的空方法:

  • doPost (Post = 创建)
  • doGet (Get = 读取)
  • doPut (Put = 更新)
  • doDelete (Delete = 删除)

其他一些 HTTP 动词也会涉及到。HttpServlet 的子类,比如 NovelsServlet,会重载相关的 do 方法,并且保留其他方法为 no-ops NovelsServlet 重载了七个 do 方法。

每个 HttpServlet 的 CRUD 方法都有两个相同的参数。下面以 doPost 为例:

public void doPost(HttpServletRequest request, HttpServletResponse response) {

request 参数是一个 HTTP 请求信息的映射,而 response 提供了一个返回给请求者的输出流。像 doPost 的方法,结构如下:

  • 读取 request 信息,采取任何适当的措施生成一个响应。如果该信息丢失或者损坏了,就会生成一个错误。
  • 使用提取的请求信息来执行适当的 CRUD 操作(在本例中,创建一个 Novel),然后使用 response 输出流为请求者编码一个适当的响应。在 doPost 例子中,响应就是已经成功生成一个新“小说”并且添加到容器中的一个确认。当响应被发送后,输出流就关闭了,同时也将连接关闭了。

关于方法重载的更多内容

HTTP 请求的格式相对比较简单。下面是一个非常熟悉的 HTTP 1.1 的格式,注释由双井号分隔:

GET /novels              ## start line
Host: localhost:8080     ## header element
Accept-type: text/plain  ## ditto
...
[body]                   ## POST and PUT only

第一行由 HTTP 动词(在本例中是 GET)和以名词(在本例中是 novels)命名目标资源的 URI 开始。报头中包含键-值对,用冒号分隔左面的键和右面的值。报头中的键 Host(大小写敏感)是必须的;主机名 localhost 是当前机器上的本地符号地址,8080 端口是 Tomcat web 服务器上等待 HTTP 请求的默认端口。(默认情况下,Tomcat 在 8443 端口上监听 HTTP 请求。)报头元素可以以任意顺序出现。在这个例子中,Accept-type 报头的值是 MIME 类型 text/plain

一些请求(特别是 POSTPUT)会有报文,而其他请求(特别是 GETDELETE)没有。如果有报文(可能为空),以两个换行符将报头和报文分隔开;HTTP 报文包含一系列键-值对。对于无报文的请求,比如说查询字符串,报头元素就可以用来发送信息。下面是一个用 ID 2 对 /novels 资源的 GET 请求:

GET /novels?id=2

通常来说,查询字符串以问号开始,并且包含一个键-值对,尽管这个键-值可能值为空。

带有 getParametergetParameterMap 等方法的 HttpServlet 很好地回避了有报文和没有报文的 HTTP 请求之前的差异。在“小说”例子中,getParameter 方法用来从 GETPOSTDELETE 方法中提取所需的信息。(处理 PUT请求需要更底层的代码,因为 Tomcat 没有提供可以解析 PUT 请求的参数映射。)下面展示了一段在 NovelsServlet中被重载的 doPost 方法:

@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) {
   String author = request.getParameter("author");
   String title = request.getParameter("title");
   ...

对于没有报文的 DELETE 请求,过程基本是一样的:

@Override
public void doDelete(HttpServletRequest request, HttpServletResponse response) {
   String param = request.getParameter("id"); // id of novel to be removed
   ...

doGet 方法需要区分 GET 请求的两种方式:一种是“获得所有”,而另一种是“获得某一个”。如果 GET 请求 URL 中包含一个键是一个 ID 的查询字符串,那么这个请求就被解析为“获得某一个”:

http://localhost:8080/novels?id=2  ## GET specified

如果没有查询字符串,GET 请求就会被解析为“获得所有”:

http://localhost:8080/novels       ## GET all

一些值得注意的细节

“小说”服务的设计反映了像 Tomcat 这样基于 Java 的 web 服务器是如何工作的。在启动时,Tomcat 构建一个线程池,从中提取请求处理程序,这种方法称为 “ 每个请求一个线程 one thread per request ” 模型。现在版本的 Tomcat 使用非阻塞 I/O 来提高个性能。

“小说”服务是作为 NovelsServlet 类的单个实例来执行的,该实例也就维护了一个“小说”集合。相应的,也就会出现竞态条件,比如出现两个请求同时被处理:

  • 一个请求向集合中添加一个新“小说”。
  • 另一个请求获得集合中的所有“小说”。

这样的结果是不确定的,取决与 的操作是以怎样的顺序进行操作的。为了避免这个问题,“小说”服务使用了线程安全的 ConcurrentMap。这个映射的关键是生成了一个线程安全的 AtomicInteger。下面是相关的代码片段:

public class Novels {
    private ConcurrentMap<Integer, Novel> novels;
    private AtomicInteger mapKey;
    ...

默认情况下,对客户端请求的响应被编码为 XML。为了简单,“小说”程序使用了以前的 XMLEncoder 类;另一个包含更丰富功能的方式是使用 JAX-B 库。代码很简单:

public String toXml(Object obj) { // default encoding
   String xml = null;
   try {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      XMLEncoder encoder = new XMLEncoder(out);
      encoder.writeObject(obj);
      encoder.close();
      xml = out.toString();
   }
   catch(Exception e) { }
   return xml;
}

Object 参数要么是一个有序的“小说” ArraList(用以响应“ 获得所有 get all ”请求),要么是一个 Novel 实例(用以响应“ 获得一个 get one ”请求),又或者是一个 String(确认消息)。

如果 HTTP 请求报头指定 JSON 作为所需要的类型,那么 XML 就被转化成 JSON。下面是 NovelsServlet 中的 doGet 方法中的检查:

String accept = request.getHeader("accept"); // "accept" is case insensitive
if (accept != null && accept.contains("json")) json = true;

Novels类中包含了 toJson 方法,可以将 XML 转换成 JSON:

public String toJson(String xml) { // option for requester
   try {
      JSONObject jobt = XML.toJSONObject(xml);
      return jobt.toString(3); // 3 is indentation level
   }
   catch(Exception e) { }
   return null;
}

NovelsServlet会对各种类型进行错误检查。比如,POST 请求应该包含新“小说”的作者和标题。如果有一个丢了,doPost 方法会抛出一个异常:

if (author == null || title == null)
   throw new RuntimeException(Integer.toString(HttpServletResponse.SC_BAD_REQUEST));

SC_BAD_REQUEST 中的 SC 代表的是 状态码 status code BAD_REQUEST 的标准 HTTP 数值是 400。如果请求中的 HTTP 动词是 TRACE,会返回一个不同的状态码:

public void doTrace(HttpServletRequest request, HttpServletResponse response) {
   throw new RuntimeException(Integer.toString(HttpServletResponse.SC_METHOD_NOT_ALLOWED));
}

测试“小说”服务

用浏览器测试 web 服务会很不顺手。在 CRUD 动词中,现代浏览器只能生成 POST(创建)和 GET(读取)请求。甚至从浏览器发送一个 POST 请求都有点不好办,因为报文需要包含键-值对;这样的测试通常通过 HTML 表单完成。命令行工具,比如说 curl,是一个更好的选择,这个部分展示的一些 curl 命令,已经包含在我网站的 ZIP 文件中了。

下面是一些测试样例,没有展示相应的输出结果:

% curl localhost:8080/novels/
% curl localhost:8080/novels?id=1
% curl --header "Accept: application/json" localhost:8080/novels/

第一条命令请求所有“小说”,默认是 XML 编码。第二条命令请求 ID 为 1 的“小说”,XML 编码。最后一条命令通过 application/json 添加了 Accept 报头元素,作为所需要的 MIME 类型。“ 获得一个 get one ”命令也可以用这个报头。这些请求用了 JSON 而不是 XML 编码作为响应。

下面两条命令在集合中创建了一个新“小说”,并且确认添加了进去:

% curl --request POST --data "author=Tolstoy&amp;title=War and Peace" localhost:8080/novels/
% curl localhost:8080/novels?id=4

curl 中的 PUT 命令与 POST 命令相似,不同的地方是 PUT 的报文没有使用标准的语法。在 NovelsServlet 中关于 doPut 方法的文档中有详细的介绍,但是简单来说,Tomcat 不会对 PUT 请求生成合适的映射。下面是一个 PUT 命令和确认命令的的例子:

% curl --request PUT --data "id=3#title=This is an UPDATE" localhost:8080/novels/
% curl localhost:8080/novels?id=3

第二个命令确认了集合已经更新。

最后,DELETE 命令会正常运行:

% curl --request DELETE localhost:8080/novels?id=2
% curl localhost:8080/novels/

这个请求是删除 ID 为 2 的“小说”。第二个命令会显示剩余的“小说”。

web.xml 配置文件

尽管官方规定它是可选的,web.xml 配置文件是一个生产级别网站或服务的重要组成部分。这个配置文件可以配置独立于代码的路由、安全性,或者网站或服务的其他功能。“小说”服务的配置通过为该服务的请求分配一个 URL 模式来配置路由:

<xml version = "1.0" encoding = "UTF-8">
<web-app>
   <servlet>
     <servlet-name>novels</servlet-name>
     <servlet-class>novels.NovelsServlet</servlet-class>
   </servlet>
   <servlet-mapping>
     <servlet-name>novels</servlet-name>
     <url-pattern>/*</url-pattern>
   </servlet-mapping>
</web-app>

servlet-name 元素为 servlet 全名(novels.NovelsServlet)提供了一个缩写(novels),然后这个名字在下面的 servlet-mapping 元素中使用。

回想一下,一个已部署服务的 URL 会在端口号后面有 WAR 文件的文件名:

http://localhost:8080/novels/

端口号后斜杠后的 URI,是所请求资源的“路径”,在这个例子中,就是“小说”服务。因此,novels 出现在了第一个单斜杠后。

web.xml 文件中,url-patter 被指定为 /*,代表 “以 /novels 为起始的任意路径”。假设 Tomcat 遇到了一个不存在的 URL,像这样:

http://localhost:8080/novels/foobar/

web.xml 配置也会指定这个请求被分配到“小说” servlet 中,因为 /* 模式也包含 /foobar。因此,这个不存在的 URL 也会得到像上面合法路径的相同结果。

生产级别的配置文件可能会包含安全相关的信息,包括 连接级别 wire-level 用户角色 users-roles 。即使在这种情况下,配置文件的大小也只会是这个例子中的两到三倍大。

总结

HttpServlet 是 Java web 技术的核心。像“小说”这样的网站或 web 服务继承了这个类,并且根据需求重载了相应的 do 动词方法。像 Jersay(JAX-RS)或 Restlet 这样的 Restful 框架通过提供定制的 servlet 完成了基本相同的功能,针对框架中的 web 应用程序的请求,这个 servlet 扮演着 HTTP(S) 终端 endpoint 的角色。

当然,基于 servlet 的应用程序可以访问 web 应用程序中所需要的任何 Java 库。如果应用程序遵循 关注点分离 separation-of-concerns 原则,那么 servlet 代码仍然相当简单:代码会检查请求,如果存在缺陷,就会发出适当的错误;否则,代码会调用所需要的功能(比如,查询数据库,以特定格式为响应编码),然后向请求这发送响应。HttpServletRequestHttpServletReponse 类型使得读取请求和编写响应变得简单。

Java 的 API 可以从非常简单变得相当复杂。如果你需要用 Java 交付一些 Restful 服务的话,我的建议是在做其他事情之前先尝试一下简单的 HttpServlet


via: https://opensource.com/article/20/7/restful-services-java

作者:Marty Kalin 选题:lujun9972 译者:Yufei-Yan 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

了解如何处理转储文件将帮你找到应用中难以重现的 bug。

崩溃转储、内存转储、核心转储、系统转储……这些全都会产生同样的产物:一个包含了当应用崩溃时,在那个特定时刻应用的内存状态的文件。

这是一篇指导文章,你可以通过克隆示例的应用仓库来跟随学习:

git clone https://github.com/hANSIc99/core_dump_example.git

信号如何关联到转储

信号是操作系统和用户应用之间的进程间通讯。Linux 使用 POSIX 标准中定义的信号。在你的系统上,你可以在 /usr/include/bits/signum-generic.h 找到标准信号的定义。如果你想知道更多关于在你的应用程序中使用信号的信息,这有一个信息丰富的 signal 手册页。简单地说,Linux 基于预期的或意外的信号来触发进一步的活动。

当你退出一个正在运行的应用程序时,应用程序通常会收到 SIGTERM 信号。因为这种类型的退出信号是预期的,所以这个操作不会创建一个内存转储。

以下信号将导致创建一个转储文件(来源:GNU C库):

  • SIGFPE:错误的算术操作
  • SIGILL:非法指令
  • SIGSEGV:对存储的无效访问
  • SIGBUS:总线错误
  • SIGABRT:程序检测到的错误,并通过调用 abort() 来报告
  • SIGIOT:这个信号在 Fedora 上已经过时,过去在 PDP-11 上用 abort() 时触发,现在映射到 SIGABRT

创建转储文件

导航到 core_dump_example 目录,运行 make,并使用 -c1 开关执行该示例二进制:

./coredump -c1

该应用将以状态 4 退出,带有如下错误:

 title=

“Abgebrochen (Speicherabzug geschrieben) ”(LCTT 译注:这是德语,应该是因为本文作者系统是德语环境)大致翻译为“分段故障(核心转储)”。

是否创建核心转储是由运行该进程的用户的资源限制决定的。你可以用 ulimit 命令修改资源限制。

检查当前创建核心转储的设置:

ulimit -c

如果它输出 unlimited,那么它使用的是(建议的)默认值。否则,用以下方法纠正限制:

ulimit -c unlimited

要禁用创建核心转储,可以设置其大小为 0:

ulimit -c 0

这个数字指定了核心转储文件的大小,单位是块。

什么是核心转储?

内核处理核心转储的方式定义在:

/proc/sys/kernel/core_pattern

我运行的是 Fedora 31,在我的系统上,该文件包含的内容是:

/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h

这表明核心转储被转发到 systemd-coredump 工具。在不同的 Linux 发行版中,core_pattern 的内容会有很大的不同。当使用 systemd-coredump 时,转储文件被压缩保存在 /var/lib/systemd/coredump 下。你不需要直接接触这些文件,你可以使用 coredumpctl。比如说:

coredumpctl list

会显示系统中保存的所有可用的转储文件。

使用 coredumpctl dump,你可以从最后保存的转储文件中检索信息:

[stephan@localhost core_dump_example]$ ./coredump 
Application started…

(…….)

Message: Process 4598 (coredump) of user 1000 dumped core.

Stack trace of thread 4598:
#0 0x00007f4bbaf22625 __GI_raise (libc.so.6)
#1 0x00007f4bbaf0b8d9 __GI_abort (libc.so.6)
#2 0x00007f4bbaf664af __libc_message (libc.so.6)
#3 0x00007f4bbaf6da9c malloc_printerr (libc.so.6)
#4 0x00007f4bbaf6f49c _int_free (libc.so.6)
#5 0x000000000040120e n/a (/home/stephan/Dokumente/core_dump_example/coredump)
#6 0x00000000004013b1 n/a (/home/stephan/Dokumente/core_dump_example/coredump)
#7 0x00007f4bbaf0d1a3 __libc_start_main (libc.so.6)
#8 0x000000000040113e n/a (/home/stephan/Dokumente/core_dump_example/coredump)
Refusing to dump core to tty (use shell redirection or specify — output).

这表明该进程被 SIGABRT 停止。这个视图中的堆栈跟踪不是很详细,因为它不包括函数名。然而,使用 coredumpctl debug,你可以简单地用调试器(默认为 GDB)打开转储文件。输入 bt 回溯 backtrace 的缩写)可以得到更详细的视图:

Core was generated by `./coredump -c1'.
Program terminated with signal SIGABRT, Aborted.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50  return ret;
(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007fc37a9aa8d9 in __GI_abort () at abort.c:79
#2  0x00007fc37aa054af in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7fc37ab14f4b "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3  0x00007fc37aa0ca9c in malloc_printerr (str=str@entry=0x7fc37ab130e0 "free(): invalid pointer") at malloc.c:5339
#4  0x00007fc37aa0e49c in _int_free (av=&lt;optimized out&gt;, p=&lt;optimized out&gt;, have_lock=0) at malloc.c:4173
#5  0x000000000040120e in freeSomething(void*) ()
#6  0x0000000000401401 in main ()

与后续帧相比,main()freeSomething() 的内存地址相当低。由于共享对象被映射到虚拟地址空间末尾的区域,可以认为 SIGABRT 是由共享库中的调用引起的。共享对象的内存地址在多次调用之间并不是恒定不变的,所以当你看到多次调用之间的地址不同时,完全可以认为是共享对象。

堆栈跟踪显示,后续的调用源于 malloc.c,这说明内存的(取消)分配可能出了问题。

在源代码中,(即使没有任何 C++ 知识)你也可以看到,它试图释放一个指针,而这个指针并没有被内存管理函数返回。这导致了未定义的行为,并导致了 SIGABRT

void freeSomething(void *ptr){
    free(ptr);
}
int nTmp = 5;
int *ptrNull = &nTmp;
freeSomething(ptrNull);

systemd 的这个 coredump 工具可以在 /etc/systemd/coredump.conf 中配置。可以在 /etc/systemd/systemd-tmpfiles-clean.timer 中配置轮换清理转储文件。

你可以在其手册页中找到更多关于 coredumpctl 的信息。

用调试符号编译

打开 Makefile 并注释掉第 9 行的最后一部分。现在应该是这样的:

CFLAGS =-Wall -Werror -std=c++11 -g

-g 开关使编译器能够创建调试信息。启动应用程序,这次使用 -c2 开关。

./coredump -c2

你会得到一个浮点异常。在 GDB 中打开该转储文件:

coredumpctl debug

这一次,你会直接被指向源代码中导致错误的那一行:

Reading symbols from /home/stephan/Dokumente/core_dump_example/coredump…
[New LWP 6218]
Core was generated by `./coredump -c2'.
Program terminated with signal SIGFPE, Arithmetic exception.
#0 0x0000000000401233 in zeroDivide () at main.cpp:29
29 nRes = 5 / nDivider;
(gdb)

键入 list 以获得更好的源代码概览:

(gdb) list
24      int zeroDivide(){
25          int nDivider = 5;
26          int nRes = 0;
27          while(nDivider &gt; 0){
28              nDivider--;
29              nRes = 5 / nDivider;
30          }
31          return nRes;
32      }

使用命令 info locals 从应用程序失败的时间点检索局部变量的值:

(gdb) info locals
nDivider = 0
nRes = 5

结合源码,可以看出,你遇到的是零除错误:

nRes = 5 / 0

结论

了解如何处理转储文件将帮助你找到并修复应用程序中难以重现的随机错误。而如果不是你的应用程序,将核心转储转发给开发人员将帮助她或他找到并修复问题。


via: https://opensource.com/article/20/8/linux-dump

作者:Stephan Avenwedde 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 Windows 上安装 GNU 编译器集合(gcc)和其他 GNU 组件来启用 GNU Autotools。

如果你是一名使用 Windows 的黑客,你不需要专有应用来编译代码。借助 Minimalist GNU for Windows(MinGW)项目,你可以下载并安装 GNU 编译器集合(GCC)以及其它几个基本的 GNU 组件,以在 Windows 计算机上启用 GNU Autotools

安装 MinGW

安装 MinGW 的最简单方法是通过 mingw-get,它是一个图形用户界面 (GUI) 应用,可帮助你选择要安装哪些组件,并让它们保持最新。要运行它,请从项目主页下载 mingw-get-setup.exe。像你安装其他 EXE 一样,在向导中单击完成安装。

 title=

在 Windows 上安装 GCC

目前为止,你只安装了一个程序,或者更准确地说,一个称为 mingw-get 的专用的包管理器。启动 mingw-get 选择要在计算机上安装的 MinGW 项目应用。

首先,从应用菜单中选择 mingw-get 启动它。

 title=

要安装 GCC,请单击 GCC 和 G++ 包来标记要安装 GNU C、C++ 编译器。要完成此过程,请从 mingw-get 窗口左上角的安装菜单中选择应用更改

安装 GCC 后,你可以使用完整路径在 PowerShell 中运行它:

PS&gt; C:\MinGW\bin\gcc.exe --version
gcc.exe (MinGW.org GCC Build-x) x.y.z
Copyright (C) 2019 Free Software Foundation, Inc.

在 Windows 上运行 Bash

虽然它自称 “minimalist”(最小化),但 MinGW 还提供一个可选的 Bourne shell 命令行解释器,称为 MSYS(它代表 最小系统 Minimal System )。它是微软的 cmd.exe 和 PowerShell 的替代方案,它默认是 Bash。除了是(自然而然的)最流行的 shell 之一外,Bash 在将开源应用移植到 Windows 平台时很有用,因为许多开源项目都假定了 POSIX 环境。

你可以在 mingw-get GUI 或 PowerShell 内安装 MSYS:

PS> mingw-get install msys

要尝试 Bash,请使用完整路径启动它:

PS> C:\MinGW\msys/1.0/bin/bash.exe
bash.exe-$ echo $0
"C:\MinGW\msys/1.0/bin/bash.exe"

在 Windows 上设置路径

你可能不希望为要使用的每个命令输入完整路径。将包含新 GNU 可执行文件的目录添加到 Windows 中的路径中。需要添加两个可执行文件的根目录:一个用于 MinGW(包括 GCC 及其相关工具链),另一个用于 MSYS(包括 Bash、GNU 和 BSD 项目中的许多常用工具)。

若要在 Windows 中修改环境,请单击应用菜单并输入 env

 title=

这将打开“首选项”窗口。点击窗口底部附近的“环境变量”按钮。

在“环境变量”窗口中,双击底部面板中的“路径”选区。

在“编辑环境变量”窗口中,单击右侧的“新增”按钮。创建一个新条目 C:\MinCW\msys\1.0\bin,然后单击 “确定”。以相同的方式创建第二条 C:\MinGW\bin,然后单击 “确定”。

 title=

在每个首选项窗口中接受这些更改。你可以重启计算机以确保所有应用都检测到新变量,或者只需重启 PowerShell 窗口。

从现在开始,你可以调用任何 MinGW 命令而不指定完整路径,因为完整路径位于 PowerShell 继承的 Windows 系统的 %PATH% 环境变量中。

Hello world

你已经完成设置,因此可以对新的 MinGW 系统进行小测试。如果你是 Vim 用户,请启动它,然后输入下面的 “hello world” 代码:

#include <stdio.h>
#include <iostream>

using namespace std;

int main() {
  cout << "Hello open source." << endl;
  return 0;
}

将文件保存为 hello.cpp,然后使用 GCC 的 C++ 组件编译文件:

PS> gcc hello.cpp --output hello

最后,运行它:

PS> .\a.exe
Hello open source.
PS>

MinGW 的内容远不止我在这里所能介绍的。毕竟,MinGW 打开了一个完整的开源世界和定制代码的潜力,因此请充分利用它。对于更广阔的开源世界,你还可以试试 Linux。当所有的限制都被消除后,你会惊讶于可能的事情。但与此同时,请试试 MinGW,并享受 GNU 的自由。


via: https://opensource.com/article/20/8/gnu-windows-mingw

作者:Seth Kenlon 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

其实我对这个实验的结果并不是特别满意,但我还是想分享一下,因为摆弄字体是件非常简单和有趣的事情。而且有人问我怎么做,我告诉她我会写一篇博文来介绍一下 :smiley:

背景:原本的手写体

先交代一些背景信息:我有一个我自己的手写字体,我已经在我的电子杂志中使用了好几年了。我用一个叫 iFontMaker 的令人愉快的应用程序制作了它。他们在网站上自诩为“你可以在 5 分钟内只用手指就能制作出你的手工字体”。根据我的经验,“5 分钟”的部分比较准确 —— 我可能花了更多的时间,比如 15 分钟。我对“只用手指”的说法持怀疑态度 —— 我用的是 Apple Pencil,它的精确度要好得多。但是,使用该应用程序制作你的笔迹的 TTF 字体是非常容易的,如果你碰巧已经有了 Apple Pencil 和 iPad,我认为这是一个有趣的方式,我只花了 7.99 美元。

下面是我的字体的样子。左边的“CONNECT”文字是我的实际笔迹,右边的段落是字体。其实有 2 种字体 —— 有一种是普通字体,一种是手写的“等宽”字体。(其实实际并不是等宽,我还没有想好如何在 iFontMaker 中制作一个实际的等宽字体)

目标:在字体上做更多的字符变化

在上面的截图中,很明显可以看出这是一种字体,而不是实际的笔迹。当你有两个相同的字母相邻时,就最容易看出来,比如“HTTP”。

所以我想,使用一些 OpenType 的功能,以某种方式为这个字体引入更多的变化,比如也许两个 “T” 可以是不同的。不过我不知道该怎么做!

来自 Tristan Hume 的主意:使用 OpenType!

然后我在 5 月份的 !!Con 2020 上(所有的演讲录音都在这里!)看到了 Tristan Hume 的这个演讲:关于使用 OpenType 通过特殊的字体将逗号插入到大的数字中。他的演讲和博文都很棒,所以这里有一堆链接 —— 下面现场演示也许是最快看到他的成果的方式。

基本思路:OpenType 允许你根据上下文替换字符

我一开始对 OpenType 到底是什么非常困惑。目前我仍然不甚了然,但我知道到你可以编写极其简单的 OpenType 规则来改变字体的外观,而且你甚至不需要真正了解字体。

下面是一个规则示例:

sub a' b by other_a;

这里 sub a' b by other_a; 的意思是:如果一个 a 字形是在一个 b 之前,那么替换 a 为字形 other_a

所以这意味着我可以让 abac 在字体中出现不同的字形。这并不像手写体那样随机,但它确实引入了一点变化。

OpenType 参考文档:真棒

我找到的最好的 OpenType 文档是这个 OpenType™ 特性文件规范 资料。里面有很多你可以做的很酷的事情的例子,比如用一个连字替换 “ffi”。

如何应用这些规则:fonttools

为字体添加新的 OpenType 规则是超容易的。有一个 Python 库叫 fonttools,这 5 行代码会把放在 rules.fea 中的 OpenType 规则列表应用到字体文件 input.ttf 中。

from fontTools.ttLib import TTFont
from fontTools.feaLib.builder import addOpenTypeFeatures

ft_font = TTFont('input.ttf')
addOpenTypeFeatures(ft_font, 'rules.fea', tables=['GSUB'])
ft_font.save('output.ttf')

fontTools 还提供了几个名为 ttxfonttools 的命令行工具。ttx 可以将 TTF 字体转换为 XML 文件,这对我很有用,因为我想重新命名我的字体中的一些字形,但我对字体一无所知。所以我只是将我的字体转换为 XML 文件,使用 sed 重命名字形,然后再次使用 ttx 将 XML 文件转换回 ttf

fonttools merge 可以让我把我的 3 个手写字体合并成 1 个,这样我就在 1 个文件中得到了我需要的所有字形。

代码

我把我的极潦草的代码放在一个叫 font-mixer 的存储库里。它大概有 33 行代码,我认为它不言自明。(都在 run.shcombine.py 中)

结果

下面是旧字体和新字体的小样。我不认为新字体的“感觉”更像手写体 —— 有更多的变化,但还是比不上实际的手写体文字(在下面)。

我觉得稍微有点不可思议,它明明还是一种字体,但它却要假装成不是字体:

而这是实际手写的同样的文字的样本:

如果我在制作另外 2 种手写字体的时候,把原来的字体混合在一起,再仔细一点,可能效果会更好。

添加 OpenType 规则这么容易,真酷!

这里最让我欣喜的是,添加 OpenType 规则来改变字体的工作方式是如此的容易,比如你可以很容易地做出一个“the”单词总是被“teh”代替的字体(让错别字一直留着!)。

不过我还是不知道如何做出更逼真的手写字体:)。我现在还在用旧的那个字体(没有额外的变化),我对它很满意。


via: https://jvns.ca/blog/2020/08/08/handwritten-font/

作者:Julia Evans 选题:lujun9972 译者:wxy 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

pipenv 是一个可帮助 Python 开发人员维护具有特定一组依赖关系的隔离虚拟环境,以实现可重新复制的开发和部署环境的工具。它类似于其他编程语言中的工具如 bundler、composer、npm、cargo、yarn 等。

最近发布了新版本的 pipenv 2020.6.2。现在可以在 Fedora 33 和 Rawhide 中使用它。对于较旧的 Fedora,维护人员决定打包到 COPR 中来先进行测试。所以在他们把它推送到稳定的Fedora版本之前,来试试吧。新版本没有带来任何新颖的功能,但是经过两年的开发,它解决了许多问题,并且在底层做了很多不同的事情。之前可以正常工作的应该可以继续工作,但是可能会略有不同。

如何获取

如果你已经在运行 Fedora 33 或 Rawhide,请运行 $ sudo dnf upgrade pipenv 或者 $ sudo dnf install pipenv,你将获得新版本。

在 Fedora 31 或 Fedora 32 上,你需要使用 copr 仓库,直到经过测试的包出现在官方仓库中为止。要启用仓库,请运行:

$ sudo dnf copr enable @python/pipenv

然后将 pipenv 升级到新版本,运行:

$ sudo dnf upgrade pipenv

或者,如果尚未安装,请通过以下方式安装:

$ sudo dnf install pipenv

如果你需要回滚到官方维护的版本,可以运行:

$ sudo dnf copr disable @python/pipenv
$ sudo dnf distro-sync pipenv

COPR 不受 Fedora 基础架构的官方支持。使用软件包需要你自担风险。

如何使用

如果你有用旧版本 pipenv 管理的项目,你应该可以毫无问题地使用新版本。如果有问题请让我们知道。

如果你还不熟悉 pipenv 或想开始一个新项目,请参考以下快速指南:

创建一个工作目录:

$ mkdir new-project && cd new-project

使用 Python 3 初始化 pipenv

$ pipenv --three

安装所需的软件包,例如:

$ pipenv install six

生成 Pipfile.lock 文件:

$ pipenv lock

现在,你可以将创建的 PipfilePipfile.lock 文件提交到版本控制系统(例如 git)中,其他人可以在克隆的仓库中使用此命令来获得相同的环境:

$ pipenv install

有关更多示例,请参见 pipenv 的文档

如何报告问题

如果你使用新版本的 pipenv 遇到任何问题,请在 Fedora 的 Bugzilla中 报告问题。Fedora 官方仓库和 copr 仓库中 pipenv 软件包的维护者是相同的人。请在报告中指出是新版本。


via: https://fedoramagazine.org/come-test-a-new-release-of-pipenv-the-python-development-tool/

作者:torsava 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 Linux 上合并和排序文本的方法有很多种,但如何去处理它取决于你试图做什么:你是只想将多个文件的内容放入一个文件中,还是以某种方式组织它,让它更易于使用。在本文中,我们将查看一些用于排序和合并文件内容的命令,并重点介绍结果有何不同。

使用 cat

如果你只想将一组文件放到单个文件中,那么 cat 命令是一个容易的选择。你所要做的就是输入 cat,然后按你希望它们在合并文件中的顺序在命令行中列出这些文件。将命令的输出重定向到要创建的文件。如果指定名称的文件已经存在,那么文件将被覆盖。例如:

$ cat firstfile secondfile thirdfile > newfile

如果要将一系列文件的内容添加到现有文件中,而不是覆盖它,只需将 > 变成 >>

$ cat firstfile secondfile thirdfile >> updated_file

如果你要合并的文件遵循一些方便的命名约定,那么任务可能更简单。如果可以使用正则表达式指定所有文件名,那就不必列出所有文件。例如,如果文件全部以 file 结束,如上所示,你可以进行如下操作:

$ cat *file > allfiles

请注意,上面的命令将按字母数字顺序添加文件内容。在 Linux 上,一个名为 filea 的文件将排在名为 fileA 的文件的前面,但会在 file7 的后面。毕竟,当我们处理字母数字序列时,我们不仅需要考虑 ABCDE,还需要考虑 0123456789aAbBcCdDeE。你可以使用 ls *file 这样的命令来查看合并文件之前文件的顺序。

注意:首先确保你的命令包含合并文件中所需的所有文件,而不是其他文件,尤其是你使用 * 等通配符时。不要忘记,用于合并的文件仍将单独存在,在确认合并后,你可能想要删除这些文件。

按时间期限合并文件

如果要基于每个文件的时间期限而不是文件名来合并文件,请使用以下命令:

$ for file in `ls -tr myfile.*`; do  cat $file >> BigFile.$$; done

使用 -tr 选项(t = 时间,r = 反向)将产生按照最早的在最前排列的文件列表。例如,如果你要保留某些活动的日志,并且希望按活动执行的顺序添加内容,则这非常有用。

上面命令中的 $$ 表示运行命令时的进程 ID。不是很必要使用此功能,但它几乎不可能会无意添加到现有的文件,而不是创建新文件。如果使用 $$,那么生成的文件可能如下所示:

$ ls -l BigFile.*
-rw-rw-r-- 1 justme justme   931725 Aug  6 12:36 BigFile.582914

合并和排序文件

Linux 提供了一些有趣的方式来对合并之前或之后的文件内容进行排序。

按字母对内容进行排序

如果要对合并的文件内容进行排序,那么可以使用以下命令对整体内容进行排序:

$ cat myfile.1 myfile.2 myfile.3 | sort > newfile

如果要按文件对内容进行分组,请使用以下命令对每个文件进行排序,然后再将它添加到新文件中:

$ for file in `ls myfile.?`; do sort $file >> newfile; done

对文件进行数字排序

要对文件内容进行数字排序,请在 sort 中使用 -n 选项。仅当文件中的行以数字开头时,此选项才有用。请记住,按照默认顺序,02 将小于 1。当你要确保行以数字排序时,请使用 -n 选项。

$ cat myfile.1 myfile.2 myfile.3 | sort -n > xyz

如果文件中的行以 2020-11-032020/11/03(年月日格式)这样的日期格式开头,-n 选项还能让你按日期对内容进行排序。其他格式的日期排序将非常棘手,并且将需要更复杂的命令。

使用 paste

paste 命令允许你逐行连接文件内容。使用此命令时,合并文件的第一行将包含要合并的每个文件的第一行。以下是示例,其中我使用了大写字母以便于查看行的来源:

$ cat file.a
A one
A two
A three

$ paste file.a file.b file.c
A one   B one   C one
A two   B two   C two
A three B three C thee
        B four  C four
                C five

将输出重定向到另一个文件来保存它:

$ paste file.a file.b file.c > merged_content

或者,你可以将每个文件的内容在同一行中合并,然后将文件粘贴在一起。这需要使用 -s(序列)选项。注意这次的输出如何显示每个文件的内容:

$ paste -s file.a file.b file.c
A one   A two   A three
B one   B two   B three B four
C one   C two   C thee  C four  C five

使用 join

合并文件的另一个命令是 joinjoin 命令让你能基于一个共同字段合并多个文件的内容。例如,你可能有一个包含一组同事的电话的文件,其中,而另一个包含了同事的电子邮件地址,并且两者均按个人姓名列出。你可以使用 join 创建一个包含电话和电子邮件地址的文件。

一个重要的限制是文件的行必须是相同的顺序,并在每个文件中包括用于连接的字段。

这是一个示例命令:

$ join phone_numbers email_addresses
Sandra 555-456-1234 [email protected]
Pedro 555-540-5405
John 555-333-1234 [email protected]
Nemo 555-123-4567 [email protected]

在本例中,即使缺少附加信息,第一个字段(名字)也必须存在于每个文件中,否则命令会因错误而失败。对内容进行排序有帮助,而且可能更容易管理,但只要顺序一致,就不需要这么做。

总结

在 Linux 上,你有很多可以合并和排序存储在单独文件中的数据的方式。这些方法可以使原本繁琐的任务变得异常简单。


via: https://www.networkworld.com/article/3570508/merging-and-sorting-files-on-linux.html

作者:Sandra Henry-Stocker 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出