分类
ArcGIS

入门弹出窗口:实现点击地址弹出信息的效果

前言

弹出窗口popups是一个简单的方法,用来在使用ArcGIS API的应用程序中增加交互面板,响应用户的操作。
每个视图都有与之关联的弹出窗口。大多数情况下,弹出窗口的内容允许用户从图层图形中获取数据。

尽管弹出窗口通常被用在图层图层和特征图层,你仍然可以使用弹出窗口来响应查询,或者响应其他不涉及图形和特征的操作。例如,可以在用户点击视图之后显示经度纬度坐标。

这个示例中我们将会学习,在视图中通过设置视图的属性(如content,title,location),来调用默认的弹出窗口,并且在不从PopupTemplategraphic、或图层的features获取信息的情况下显示它。

这个示例使用定位器locator来从视图上单击的位置反向对点进行地理编码。
它返回的地址被展示在弹出窗口的内容中,与此同时,被点击地址的精度和维度被显示在弹出窗口的标题中。

在正式操作开始之前,请确保了解视图Map
如有必要,可以先学习下面两篇文章:

1. 引入locator定位器、map地图对象、MapView地图视图模块,创建新的实例对象

使用世界地理服务the World Geocoding Service创建一个locator url
然后创建一个Map对象,基于这个Map创建一个视图实例。你的JavaScript代码应该像下面这样:

require(["esri/rest/locator", "esri/Map", "esri/views/MapView"], (locator, Map, MapView) => {
  // Create a locator url using the world geocoding service
  const locatorUrl = "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer";

  // Create the Map
  const map = new Map({
    basemap: "streets-navigation-vector"
  });

  // Create the MapView
  const view = new MapView({
    container: "viewDiv",
    map: map,
    center: [-71.6899, 43.7598],
    zoom: 12
  });
});

2. 监听视图的点击事件,在点击的位置显示弹出窗口

监听视图的点击事件,获得点击位置的经纬度。在被点击的位置显示弹出窗口,然后在弹出窗口的标题中显示被点击的坐标。
为了实现这个效果,我们将会通过open()方法,设置弹出窗口的地址标题属性。

view.popup.autoOpenEnabled = false;
view.on("click", (event) => {
  // Get the coordinates of the click on the view
  // around the decimals to 3 decimals
  const lat = Math.round(event.mapPoint.latitude * 1000) / 1000;
  const lon = Math.round(event.mapPoint.longitude * 1000) / 1000;

  view.popup.open({
    // Set the popup's title to the coordinates of the clicked location
    title: "Reverse geocode: [" + lon + ", " + lat + "]",
    location: event.mapPoint // Set the location of the popup to the clicked location
  });
});

3.定位点击位置的地址,在弹出窗口中显示匹配的地址

被点击的位置作为一个反向地理编码方法的输入,并且结果地址被显示在弹出窗口的内容中。
最后,如果在点击位置没有找到地址,弹出窗口的暖会显示消息,来告诉用户没有找到地址:

const params = {
  location: event.mapPoint
};

// Execute a reverse geocode using the clicked location
locator
  .locationToAddress(locatorUrl, params)
  .then((response) => {
    // If an address is successfully found, show it in the popup's content
    view.popup.content = response.address;
  })
  .catch(() => {
    // If the promise fails and no result is found, show a generic message
    view.popup.content = "No address was found for this location";
  });
分类
ArcGIS

入门图层:在视图中绘制图形

前言

图层是地图中最基础的组件,它是以图形或图像的形式,收集到的真实世界中的空间数据。
图层可以存储矢量数据的离散特征,或存储光栅数据的连续单元/像素。
(译者注:这句话是在说,图层既可以是位图的形式,存放一个一个的像素;也可以是矢量图的形式,存放的一系列生成函数,在需要的时候实时渲染成图形)

一张地图可能包含不同种类的图层,如果想要简单的浏览一下API中可用的图层特点,可以查看这张表格

所有具体的图层对象,都从Layer中继承了属性、方法、事件。本文中将会讨论一些常用的属性。
如果要学习不同图层类型的属性特点,可以看图层类型的示例,比如瓦片图层

在正式操作开始之前,请确保了解视图Map
如有必要,可以先学习下面两篇文章:

1. 创建Map对象、地图视图,以及一个HTML选择框

先来创建一个基本的Map对象,并且基于Map对象创建一个地图视图MapView
JavaScript代码如下:

require(["esri/Map", "esri/views/MapView"], (Map, MapView) => {
  // Create the Map
  const map = new Map({
    basemap: "oceans"
  });

  // Create the MapView
  const view = new MapView({
    container: "viewDiv",
    map: map
  });
});

然后在HTML中添加一个选择框元素,这样做的目的我们后面会提到。

<body>
  <div id="viewDiv"></div>
  <span id="layerToggle" class="esri-widget"> <input type="checkbox" id="streetsLayer" checked /> Transportation </span>
</body>

2. 用瓦片图层TileLayer创建两个图层

接下来创建两个瓦片图层(TileLayer)的实例,注意瓦片图层的代码需要写在创建map和view之前
为了实现这个需要,我们必须引入esri/layers/TileLayer模块并且定制这个图层的url属性。
url属性必须指向一个缓存地图服务,这个服务可以托管在ArcGIS Server上或Portal for ArcGIS上。

所有用于连接ArcGIS服务的图层,都具有url字段,它必须被设置成在视图中显示。
在这个示例中,我们使用Esri World Transportation service服务和一个包含纽约市房屋数据的服务,来创建街道和高速公路图层:

require([
  "esri/Map",
  "esri/views/MapView",
  "esri/layers/TileLayer" // Require the TileLayer module
], (Map, MapView, TileLayer) => {
  const transportationLayer = new TileLayer({
    url: "https://server.arcgisonline.com/arcgis/rest/services/Reference/World_Transportation/MapServer"
  });

  const housingLayer = new TileLayer({
    url: "https://tiles.arcgis.com/tiles/nGt4QxSblgDfeJn9/arcgis/rest/services/New_York_Housing_Density/MapServer"
  });

  /*****************************************************************
   * The code to create a map and view instance in the previous step
   * should be placed here.
   *****************************************************************/
});

3. 为图层设置额外的属性

我们可以设置图层额外的属性,例如id, minScale, maxScale, opacity, and visible。这些属性可以在构造函数中设置,也可以直接在实例中设置。
我们增加id属性在每个图层中,并且设置交通图层的不透明度opacity

const transportationLayer = new TileLayer({
  url: "https://server.arcgisonline.com/arcgis/rest/services/Reference/World_Transportation/MapServer",
  id: "streets",
  opacity: 0.7
});

const housingLayer = new TileLayer({
  url: "https://tiles.arcgis.com/tiles/nGt4QxSblgDfeJn9/arcgis/rest/services/New_York_Housing_Density/MapServer",
  id: "ny-housing"
});

id属性是一个图层的唯一标识,使得我们容易的在其他位置来引用这个图层。如果开发者没有直接设置id,当图层创建时也会自动生成一个id
minScalemaxScale属性在图层缩放时控制可见性(译者注,说的就是缩放级别)。使用这些属性可以在缩放时改善程序的性能和简化地图的绘制。
此外,visible属性默认是true

4. 为地图增加图层

我们有多种不同的方法来添加图层。想要学习这些方法可以访问Map.layers
在本文的示例中,我们分别尝试一下不同的添加方法。

构造函数法

先来看看使用构造函数的方法,在map创建时添加一个房屋图层(housing layer):

// Both layers were created prior to this code snippet
const map = new Map({
  basemap: "oceans",
  layers: [housingLayer] // layers can be added as an array to the map's constructor
});

add()方法

也可以使用map.layers.add()方法来添加一个交通图层:

map.layers.add(transportationLayer);

到目前为止,所有添加的图层都可以在视图中显示了。

5. 设置图层的可见性

使用addEventListener方法,可以监听我们前面创建的这个checkbox选择框的变化事件(event)。当这个选择框被选中或者取消选中时,我们就可以改变交通图层的可见性(通俗的说就是显示、不显示、缩放、透明度等等一系列属性),例如visible
任何图层的任何属性,都可以直接设置在图层实例中。如下面代码:

require(["esri/Map", "esri/views/MapView", "esri/layers/TileLayer"], (
  Map,
  MapView,
  TileLayer
) => {
  /*****************************************************************
   * All code previously written in the steps above should be placed
   * before the following code
   *******************************************************************/

  // Create a variable referencing the checkbox node
  const streetsLayerToggle = document.getElementById("streetsLayer");

  // Listen to the change event for the checkbox
  streetsLayerToggle.addEventListener("change", () => {
    // When the checkbox is checked (true), set the layer's visibility to true
    transportationLayer.visible = streetsLayerToggle.checked;
  });
});

尽管在上面的示例中,图层是不可见的,但它仍然在map中存在。因此,在用户看不见图层的情况下,我们作为开发者仍然可以访问到这些图层的属性,并且使用它们来数据分析。

6. 理解图层视图(LayerViews)

图层对象管理着来自服务的地理数据和表格数据,但它不处理在视图中的渲染层,渲染的工作是交给图层视图(LayerView)进行的。
图层的LayerView是在图层渲染之前创建的。使用FeatureLayers时,对应的FeatureLayerView可以为开发人员提供访问视图中渲染的与该层的功能相关的图形的权限。

在这个步骤中,我们将会学习视图的layerview-create事件,并且输出住房和交通的LayerViews,以便控制他们的属性。
需要注意的是,我们将会使用第三节中创建的id属性,来准确的找到一个图层。除了地图的操作图层外,这个事件还可以通过基础地图图层和高程图层来触发:

require(["esri/Map", "esri/views/MapView", "esri/layers/TileLayer"], (
  Map,
  MapView,
  TileLayer
) => {
  /*****************************************************************
   * All code previously written in the steps above should be placed
   * before the following code
   *******************************************************************/

  // This event fires each time a layer's LayerView is created for the
  // specified view instance
  view.on("layerview-create", (event) => {
    if (event.layer.id === "ny-housing") {
      // Explore the properties of the housing layer's layer view here
      console.log("LayerView for New York housing density created!", event.layerView);
    }
    if (event.layer.id === "streets") {
      // Explore the properties of the transportation layer's layer view here
      console.log("LayerView for streets created!", event.layerView);
    }
  });
});

7. 使用Layer.when()方法

在图层加载时(loaded)、或者当图层的所有属性对于开发者都可用的时候,这个图层会解析为一个promise
在这个示例中,我们想要让房屋图层有完整的扩展(fullExtent),因为我们事先不知道,视图初始化时使用什么样的扩展(中心点、缩放)比较合适。

在图层被加载之前,我们无法获取到这个图层。因此我们必须在它被解析之后去修改它。
这种修改过程使用的是when()方法:

// When the layer's promise resolves, animate the view to the layer's fullExtent
housingLayer.when(() => {
  view.goTo(housingLayer.fullExtent);
});

8.总结

本文中还有许多图层的属性尚未讨论。如果想了解图层更多的信息,可以看API文档和里面的示例。本文最终的示例代码可以在沙盒中查看。

分类
ArcGIS

入门MapView:创建2D地图

前言

在这个教程中,我们将会学习如何创建2D地图视图(MapView)。

注意:这篇文章使用AMD模块,你也可以下载一个兼容ES模块的示例,该示例使用Vite.js来创建本地构建
如果想了解关于不同API模块的区别,可以看这篇文章。

一、引用ArcGIS Map SDK for JavaScript

名词解释:
JS——JavaScript
SDK——Software Development Kit,指的是某种计算机语言的一系列开发工具

ArcGIS Map SDK for JavaScript就是ArcGIS为JS编写的一个地图API。

第一步就是初始化一个HTML文件,类似下面代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />
    <title>Intro to MapView - Create a 2D map</title>
  </head>
</html>

我们需要在<Head>标签中,通过<script><link>标签引入ArcGIS Map JS SDK:

<link rel="stylesheet" href="https://js.arcgis.com/4.26/esri/themes/light/main.css" />
<script src="https://js.arcgis.com/4.26/"></script>

<script>标签通过CDN加载了ArcGIS Map JS SDK。如果出现了版本更新,可以通过修改代码中引用的版本号,来获取最新版本。

<link>标签实现了对于main.css的引用,这个css包含了Esri控件和组件样式特性。(译者注,就是官方必备的CSS样式,原文说的比较啰嗦)

2. 加载模块

使用另一个<script>标签去加载特定的API模块。
esri/Map用来加载特定代码以便创建一个Map对象
esri/views/MapView用来创建一个2D地图
使用方法如下:

<script>
  require([ "esri/Map", "esri/views/MapView" ], (Map, MapView) => {
    // 这里是回调函数,可以操作创建好的对象
  });
</script>

示例中的JS代码放在了HTML脚本中,但对于大型项目来说这种做法并适合,应该放在单独的JS文件中

全局的require()方法用来加载API的AMD模块。对于不同模块的区别,可以访问这个链接来查看教程。

3. 创建Map对象

一张地图使用的是Map对象,这个对象来自esri/Map模块的Map类。在创建Map对象的过程中,我们可以在构造函数中定制它的属性,例如可以修改basemap底图。

require(["esri/Map", "esri/views/MapView"], (Map, MapView) => {
  const map = new Map({
    basemap: "topo-vector"
  });
});

其他的参数包括: satellite, hybrid, gray-vector, dark-gray-vector, oceans, streets-vector, osm, national-geographic, streets-night-vector。在沙盒中更改basemap属性,就可以切换底图。

4. 创建2D视图(View)

作为HTML文件中的容器,视图引用结点(View)允许用户在HTML中嵌入一张地图。通过对象的构造函数,我们可以创建新的MapView并且设置它的属性:

require(["esri/Map", "esri/views/MapView"], (Map, MapView) => {
  const map = new Map({
    basemap: "topo-vector"
  });

  const view = new MapView({
    container: "viewDiv", // Reference to the DOM node that will contain the view
    map: map // References the map object created in step 3
  });
});

在这个小节中,我们为DOM结点设置一个容器属性(container),以便让它能够承载地图,因此我们给div标签添加一个id属性。地图视图的map属性实际上引用的就是上一步中我们创建的map对象。
可以点击MapView documentation来查看此视图的其他属性,例如center(中心)zoom(缩放),这些属性用来定义视图的初始范围。

视图共有两种:地图视图MapView用来显示2D地图,场景视图SceneView用来显示3D视图,点击这篇文章可以学习使用3D视图。

5. 定义页面内容

到目前为止,我们已经写好了负责创建map对象和view视图的JavaScript代码。
(译者注,如果搞不清楚这几个对象的关系,可以理解成:最下层是map对象,map对象的上层是view视图,view视图的上层是layer图层,图层有很多层。
所以无论使用2D还是3D,都要先创建map,基于map创建view,最后在view上绘制layer即可)

下一步我们就来添加HTML以便展示我们的地图。HTML的写法非常简单:先写一个<body>标签,然后在<body>标签中想要显示这个视图的位置放一个<div>标签

<body>
  <div id="viewDiv"></div>
</body>

其中,<div>标签的id,必须与前面MapView中container属性的字符串相匹配。

6.为页面设置样式

样式的代码写在<style>标签中,<style>标签需要被<head>标签包裹,当然,你也可以根据自己的习惯,把样式写在单独的CSS文件中,与普通的前端开发别无二致。

<style>
  html,
  body,
  #viewDiv {
    padding: 0;
    margin: 0;
    height: 100%;
    width: 100%;
  }
</style>

到目前为止,我们使用ArcGIS Maps SDK for JavaScript 4.26!完成了一个2D地图应用,最终的代码效果类似下面:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no" />
    <title>Intro to MapView - Create a 2D map</title>
    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }
    </style>
    <link rel="stylesheet" href="https://js.arcgis.com/4.26/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.26/"></script>
    <script>
      require(["esri/Map", "esri/views/MapView"], (Map, MapView) => {
        const map = new Map({
          basemap: "topo-vector"
        });
        const view = new MapView({
          container: "viewDiv", // Reference to the view div created in step 5
          map: map, // Reference to the map object created before the view
          zoom: 4, // Sets zoom level based on level of detail (LOD)
          center: [15, 65] // Sets center point of view using longitude,latitude
        });
      });
    </script>
  </head>
  <body>
    <div id="viewDiv"></div>
  </body>
</html>
分类
DevOps spring-boot

SpringBoot应用作为服务运行

译者注

原文

https://www.baeldung.com/spring-boot-app-as-a-service

1、 前言

本文中,我们探讨把SpringBoot应用作为服务运行的一些方案。
首先,我们将会去解释web程序的打包选项和系统服务。
然后,当需要同时运行在linux和Windows系统时,我们有哪些办法来实现这个目的。
最后,我们将会总结一些引用文章,以便帮助读者获取更多信息。

2、项目设置和构建说明

2.1 打包

Web应用以传统方式被打包成web应用存档(Application aRchives,WAR),然后部署到一个web服务器。
SpringBoot应用既可以被打包成WAR也可以被打包成Jar文件,具体如何打包取决于你的应用服务是否需要安装配置的过程。

2.2 Maven配置

我们先从定义Maven的配置文件pox.xml开始:

<packaging>jar</packaging>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.0.RELEASE</version>
</parent>

<dependencies>
    ....
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <executable>true</executable>
            </configuration>
        </plugin>
    </plugins>
</build>

打包一定要设置为jar,在本文编写时,我们使用的是最新的SpringBoot稳定版,但实际上只要版本高于1.3就可以了。
你可以在这篇文章中找到可行的版本。

2.3 构建你的应用程序

在应用程序根目录执行这条命令来完成构建:

$ mvn clean package

然后,就可以在target目录中找到可执行的JAR文件。
如果要执行这个文件,需要在命令行中执行这条命令:

$ java -jar your-app.jar

此外,你仍然需要通过-jar选项来调用Java解释器。
有许多原因可以解释,为什么通过服务运行应用程序是一个好的办法。

3. 在linux中运行

为了作为一个后台进程运行这个程序,我们需要使用UNIX命令nohup,但出于某些原因,这并不是最好的办法。在这篇文章中给了很好的解释。
我们需要为我们的进程生成一个守护进程。在Linux下,我们需要选择:使用传统的System V初始化脚本还是使用Systemd配置文件,来配置守护进程。前者是被人们普遍认可的,但它逐渐被后者取代。

你可以在这篇文章中找到二者的详细区别。

为了加强安全性。我们先创建一个特殊用户,来运行这个服务,并且改变这个JAR文件的权限:

$ sudo useradd baeldung
$ sudo passwd baeldung
$ sudo chown baeldung:baeldung your-app.jar
$ sudo chmod 500 your-app.jar

3.1 System V init

SpringBoot可执行JAR文件使得服务启动进程十分简单:

$ sudo ln -s /path/to/your-app.jar /etc/init.d/your-app

上面的代码对你的可执行JAR文件创建了一个符号连接。必须使用到这个JAR文件的完整路径才能正确执行,否则符号连接无法正常工作。这个连接使得你可以让这个应用程序作为服务来启动:

$ sudo service your-app start

这个脚本支持标准服务的启动(start)、终止(stop)、重启(restart)、状态(status)命令,此外:

  • 它使用我们刚刚创建的“baeldung”用户来运行这个服务
  • 它在/var/run/your-app/your-app.pid文件中追踪这个程序的进程
  • 它写入控制命令到/var/log/your-app.log进程,以便我们可以查看程序的运行状态

3.2 systemd

systemd服务设置起来非常的简单。首先我们创建一个名为your-app.service的文件,并且填入下面的示例代码,然后把它放到/etc/systemd/system目录中:

[Unit]
Description=A Spring Boot application
After=syslog.target

[Service]
User=baeldung
ExecStart=/path/to/your-app.jar SuccessExitStatus=143 

[Install] 
WantedBy=multi-user.target

我们需要根据应用程序的实际情况来更新字段DescriptionUserExecStart。你需要能够执行前面提到的运行一个服务的基本命令。

3.3 Upstart

Upstart是一个基于时间的服务管理器,它是System V init的潜在替代者,它可以更好的控制不同守护进程的行为。

这篇安装教程十分有用,它对于几乎所有的Linux发行版都是可用的。
当使用Ubuntu时,你可能已经安装和配置好了,请检查/etc/init文件中是否以upstart开头的任务。

我们创建一个名为your-app.conf的任务,来启动我们的SpringBoot程序:

# Place in /home/{user}/.config/upstart

description "Some Spring Boot application"

respawn # attempt service restart if stops abruptly

exec java -jar /path/to/your-app.jar

现在运行start your-app,你的服务就可以启动了。

Upstart提供了许多任务配置选项,你可以在这个地址中找到大多数信息。

4. 在Windows中

本小节中,我们有几种不同的办法来使用Windows服务运行Java JAR包。

4.1 Windows服务装饰器(Windows Service Wrapper

由于Java服务装饰器使用的GPL协议和Jenkins使用的MIT协议在兼容上存在困难,人们开启了Windows服务装饰器项目,也称为“winsw”。

Winsw提供了安装、卸载、启动、停止服务的方法。
它可以在Windows平台运行任何类型的可执行服务,而不是像Java Service Wrapper只能运行Java的程序。

首先从这里下载二进制文件。然后编辑用来定义我们的Windows服务的配置文件:

<service>
    <id>MyApp</id>
    <name>MyApp</name>
    <description>This runs Spring Boot as a Service.</description>
    <env name="MYAPP_HOME" value="%BASE%"/>
    <executable>java</executable>
    <arguments>-Xmx256m -jar "%BASE%\MyApp.jar"</arguments>
    <logmode>rotate</logmode>
</service>

最后,把winsw.exe重命名为MyApp.exe,以便和配置文件MyApp.xml匹配。然后就可以安装服务:

$ MyApp.exe install

相似的,也可以卸载(stop)、启动(start)、停止(stop),等等。

4.2 Java服务装饰器

如果你不介意Java服务装饰器项目的GPL许可协议,有一个可替代的方式来配置你的JAR文件作为Windows服务。
Java服务装饰器也需要你去定制一个配置文件,来声明如何把你的程序作为服务在Windows上运行。

这篇文章详细阐述了在Windows下如何配置一个JAR可执行文件作为服务运行,因此无需再重复之前的内容。

5. 额外的引用

我们也可以使用Apache Commons Daemon
项目的 Procrun来让SpringBoot应用程序作为Windows服务启动。
Procrun是一个应用程序集合,它允许Windows用户把Java应用程序装饰成为一个Windows服务。这样的服务可以在机器启动时自动启动,并且即使没有用户登录也会持续运行。

这里可以找到关于SpringBoot程序在Unix下运行的更多信息。
这篇文章也介绍了如何编辑Redhat的Systemd系统单元文件

最后,这篇快速指南描述了如何为你的JAR文件创建一个Bash脚本,以便让它自己运行。

6.结论

服务是的我们可以更高效地管理我们的应用程序,正如我们看到的,把SpringBoot设置为一个服务的过程比以往更简单了。

需要注意的是,在用户权限方面,我们应该遵循重要而简单的安全措施,来运行你的服务。

分类
Java Security

Java中的MD5散列

译者注

原文

https://www.baeldung.com/java-md5

Demo

https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-security-2

一、前言

MD5广泛应用在密码散列中,它提供128位的散列。
在本文中,我们将会学习不同的方法,使用多种Java库来实现MD5散列

二、使用MessageDigest

java.security.MessageDigest类中提供了散列功能。
我们需要把你想使用的算法作为参数,来实例化一个对象:

MessageDigest.getInstance(String Algorithm)

然后使用update()方法来更新消息的内容:

public void update(byte [] input)

如果你想读取一个长文件,上面这个方法,可以被多次调用,最终我们需要使用digest()方法来生成一个散列代码:

public byte[] digest()

下面的案例展示了如何给一个密码生成散列,并且验证它:

@Test
public void givenPassword_whenHashing_thenVerifying() 
  throws NoSuchAlgorithmException {
    String hash = "35454B055CC325EA1AF2126E27707052";
    String password = "ILoveJava";

    MessageDigest md = MessageDigest.getInstance("MD5");
    md.update(password.getBytes());
    byte[] digest = md.digest();
    String myHash = DatatypeConverter
      .printHexBinary(digest).toUpperCase();
    assertThat(myHash.equals(hash)).isTrue();
}

相似的,我们也可以检查文件的校验和:

@Test
public void givenFile_generatingChecksum_thenVerifying() 
  throws NoSuchAlgorithmException, IOException {
    String filename = "src/test/resources/test_md5.txt";
    String checksum = "5EB63BBBE01EEED093CB22BB8F5ACDC3";

    MessageDigest md = MessageDigest.getInstance("MD5");
    md.update(Files.readAllBytes(Paths.get(filename)));
    byte[] digest = md.digest();
    String myChecksum = DatatypeConverter
      .printHexBinary(digest).toUpperCase();
    assertThat(myChecksum.equals(checksum)).isTrue();
}

我们需要注意的是,这个MessageDigest线程不安全的。因此,我们需要在每个线程中使用新的实例。

3. 使用Apache Commons

阿帕奇的org.apache.commons.codec.digest.DigestUtils类也提供了相似的功能,而且使用起来很简单。
下面给出一个生成密码散列和校验的例子:

@Test
public void givenPassword_whenHashingUsingCommons_thenVerifying()  {
    String hash = "35454B055CC325EA1AF2126E27707052";
    String password = "ILoveJava";

    String md5Hex = DigestUtils
      .md5Hex(password).toUpperCase();
    assertThat(md5Hex.equals(hash)).isTrue();
}

4. 使用Guava

下面的例子是另一个生成MD5校验和的方法,使用com.google.common.io.Files.hash类:

@Test
public void givenFile_whenChecksumUsingGuava_thenVerifying() 
  throws IOException {
    String filename = "src/test/resources/test_md5.txt";
    String checksum = "5EB63BBBE01EEED093CB22BB8F5ACDC3";

    HashCode hash = com.google.common.io.Files
      .hash(new File(filename), Hashing.md5());
    String myChecksum = hash.toString()
      .toUpperCase();

    assertThat(myChecksum.equals(checksum)).isTrue();
}

需要注意的是,Hashing.md5是被反对的,然而,正如官方文档所说,原因是建议用户不要使用MD5去生成安全信息。
这意味着我们仍然可以使用它,如果我们需要整合使用MD5的传统项目。除此之外,我们最好去考虑更安全的方案,例如使用SHA-256
(译者注:MD5已经被破解了,因此它不再安全了)

5、 总结

本文中我们学习了使用不同的方式,调用Java API或第三方API,来实现MD5的散列。
实际使用时,我们需要根据项目的需要和项目的依赖关系来选择具体的方案。

和往常一样,你可以在Github上找到本文的示例代码。

分类
Java

Java无头模式

译者注

原文:

https://www.baeldung.com/java-headless-mode

Demo

https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-lang-2

1. 前言

有时,我们需要在没有真实的显示器、键盘、鼠标的情况下,来实现基于图形的Java应用程序,也就是说,程序运行在服务器或容器上。

本文我们将会学习Java的无头模式,以便实现上面提到的需求。我们将会知道,在无头模式的情况下,我们可以做什么、不能做什么。

2. 设置无头模式

在Java中,我们有很多方式来设置无头模式:

  • 将系统属性java.awt.headless设置为true
  • 使用命令行参数java -Djava.awt.headless=true
  • 在服务的启动脚本中,添加参数-Djava.awt.headless=trueJAVA_OPTS环境变量中

如果环境设置为无头模式,JVM就会识别到它。然而这样就会有一些细微的区别。我们来具体看一下。

3. 无头模式的UI组件示例

无头环境中的UI组件一个典型使用情况,就是作为图像转换器程序。
尽管它在运行过程中需要图形数据,但他并不需要显示。这样的app将会运行在服务器中,转换后的数据将会保存并通过网络传输给另一台机器来显示。

我们看一下如何操作。

首先,我们在JUnit类中启用无头模式,

@Before
public void setUpHeadlessMode() {
    System.setProperty("java.awt.headless", "true");
}

为了确保无头模式已经正常开启,我们可以写一个测试程序,通过调用java.awt.GraphicsEnvironment来断言无头模式是true:

@Test
public void whenSetUpSuccessful_thenHeadlessIsTrue() {
    assertThat(GraphicsEnvironment.isHeadless()).isTrue();
}

通过上面的测试方法,我们就可以准确的了解当前无头模式是否已经启用。
现在我们来做一个简单的图像转换器:

@Test
public void whenHeadlessMode_thenImagesWork() {
    boolean result = false;
    try (InputStream inStream = HeadlessModeUnitTest.class.getResourceAsStream(IN_FILE); 
      FileOutputStream outStream = new FileOutputStream(OUT_FILE)) {
        BufferedImage inputImage = ImageIO.read(inStream);
        result = ImageIO.write(removeAlphaChannel(inputImage), FORMAT, outStream);
    }

    assertThat(result).isTrue();
}

在下一个示例中,我们可以看到,所有字体的信息,包括字体规格都可用了:

@Test
public void whenHeadless_thenFontsWork() {
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    String fonts[] = ge.getAvailableFontFamilyNames();
    assertThat(fonts).isNotEmpty();
    Font font = new Font(fonts[0], Font.BOLD, 14);
    FontMetrics fm = (new Canvas()).getFontMetrics(font);

assertThat(fm.getHeight()).isGreaterThan(0);
assertThat(fm.getAscent()).isGreaterThan(0);
assertThat(fm.getDescent()).isGreaterThan(0);
}

4. 无头异常HeadlessException

如果有组件依赖外围设备,它们就无法在无头模式中工作。当使用非交互环境时,就会抛出无头异常:

Exception in thread "main" java.awt.HeadlessException
    at java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:204)
    at java.awt.Window.<init>(Window.java:536)
    at java.awt.Frame.<init>(Frame.java:420)

例如下面的代码,在一个无头模式的测试方法中,使用Frame对象就会导致无头异常:

@Test
public void whenHeadlessmode_thenFrameThrowsHeadlessException() {
    assertThatExceptionOfType(HeadlessException.class).isThrownBy(() -> {
        Frame frame = new Frame();
        frame.setVisible(true);
        frame.setSize(120, 120);
    });
}

根据经验,需要注意的是,这些顶级组件(比如Frame或者Button)需要交互环境,否则就会抛出异常。如果无头模式没有开启,它们可能会取而代之的抛出非交互错误irrecoverable Error

5. 在无头模式中绕过重量级组件

在本小节中,我们来提出一个问题:

如果我们把一个带有GUI组件的代码分别在“有头的生产环境机器上”和“无头的代码分析服务器上”运行,会发生什么?

在上面的例子中,我们已经知道了重量级组件无法在服务器上运行,并且会抛出异常。
所有我们可以使用一个条件来达到目的:

public void FlexibleApp() {
    if (GraphicsEnvironment.isHeadless()) {
        System.out.println("Hello World");
    } else {
        JOptionPane.showMessageDialog(null, "Hello World");
    }
}

用这样的模式,我们可以创造一个灵活的程序,根据它所在的环境来自动调整行为。

6、 总结

通过不同的代码示例,我们了解了Java的无头模式和它的部分原理。在这篇文章中提供了兼容列表,列表中给出了无头模式中可以进行哪些操作。

和往常一样,你可以在Github上找到本文中的示例代码。

分类
Spring MVC

Spring请求参数注解@RequestParam

译者注

原文

https://www.baeldung.com/spring-request-param

Demo

https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-mvc-basics-5

一、前言

在本文中,我们将要学习Spring的@RequestParam注解,和它的作用。
我们可以使用@RequestParam,从请求的参数或文件中,去解析查询参数。

二、简单的映射

假设我们定义了一个请求地址/api/foos,它接收了一个名为id的参数:

@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam String id) {
    return "ID: " + id;
}

在这个示例中,我们使用@RequestParam来解析id的查询参数。

一个简单的GET请求将会调用这个getFoos()方法:

http://localhost:8080/spring-mvc-basics/api/foos?id=abc
----
ID: abc

接下来,我们看一下这个注解的参数,包括:

  • 名称name
  • value
  • 必须required
  • 默认值defaultValue

3. 指定请求参数名称

在前面的例子中,变量名称和参数名称是相同的。但有时我们需要让这二者不同,或者我们没有使用SpringBoot,此时我们可能需要在编译时的配置文件中做一些额外的操作,否则参数名称将不会进行二进制编码。

幸运的是,我们可以使用name属性来配置请求参数@RequestParam的名称:

@PostMapping("/api/foos")
@ResponseBody
public String addFoo(@RequestParam(name = "id") String fooId, @RequestParam String name) { 
    return "ID: " + fooId + " Name: " + name;
}

我们还可以写成@RequestParam(value = “id”)或者干脆写成@RequestParam(“id”)

4. 可选的请求参数

使用@RequestParam注解的方法参数默认是必须的,这就一位置如果参数没有在请求中给出,就会发生错误:

GET /api/foos HTTP/1.1
-----
400 Bad Request
Required String parameter 'id' is not present

我们可以配置required = false来实现可选功能optional

@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam(required = false) String id) { 
    return "ID: " + id;
}

此时,无论请求中有没有给出这个参数,都可以正确的解析到我们刚才写的方法上:

http://localhost:8080/spring-mvc-basics/api/foos?id=abc
----
ID: abc

如果请求中没有给出参数,获取到的是null,而不是出错:

http://localhost:8080/spring-mvc-basics/api/foos
----
ID: null

4.1 使用Java8的Optional

我们还可以用Optional去装饰这个参数:

@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam Optional<String> id){
    return "ID: " + id.orElseGet(() -> "not provided");
}

此时,我们就不需要再去设置required属性了。如果请求中没有给出参数的值,就会返回默认值。

http://localhost:8080/spring-mvc-basics/api/foos
----
ID: not provided

5. 请求参数的默认值

我们通过defaultValue属性,为请求参数设置默认值:

@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam(defaultValue = "test") String id) {
    return "ID: " + id;
}

这样写的效果就类似required=false,用户不必再去提供这个参数:

http://localhost:8080/spring-mvc-basics/api/foos
----
ID: test

也可以提供参数:

http://localhost:8080/spring-mvc-basics/api/foos?id=abc
----
ID: abc

需要注意的是,当我们设置默认值属性时,必须属性required就已经设置为false了。

6. 映射所有的参数

我们还可以用映射,实现在没有定义参数名称的情况下,使用多个参数:

@PostMapping("/api/foos")
@ResponseBody
public String updateFoos(@RequestParam Map<String,String> allParams) {
    return "Parameters are " + allParams.entrySet();
}

上面的写法会返回请求的参数,如果发起请求就会:

curl -X POST -F 'name=abc' -F 'id=123' http://localhost:8080/spring-mvc-basics/api/foos
-----
Parameters are {[name=abc], [id=123]}

7. 映射多值参数

单一变量的请求参数可以包含多个值:

@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam List<String> id) {
    return "IDs are " + id;
}

并且SpringMVC会自动映射到一个逗号分隔的id参数:

http://localhost:8080/spring-mvc-basics/api/foos?id=1,2,3
----
IDs are [1,2,3]

或者一个分离的id列表:

http://localhost:8080/spring-mvc-basics/api/foos?id=1&id=2
----
IDs are [1,2]

8. 结论

在本文中,我们学习了如何使用@RequestParam注解。
你可以在Github上找到本文中示例的完整代码。

分类
Spring MVC

Spring 路径变量注解 @Pathvariable

译者注

原文

https://www.baeldung.com/spring-pathvariable

Demo

https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-mvc-java-2

一、前言

本文中我们将会学习Spring的路径变量注解@PathVariable
路径注解可以用来处理URI映射中的模板变量,并且设置这些变量作为方法参数。

我们来看看@PathVariable和它的各种属性。

二、简单的映射

@PathVariable注解的简单用法,可以通过主键来区分实体:

@GetMapping("/api/employees/{id}")
@ResponseBody
public String getEmployeesById(@PathVariable String id) {
    return "ID: " + id;
}

在这个例子中,我们使用@PathVariable注解来解析URI,前面的部分是固定的,后面是变量{id}

当我们是用GET方式向/api/employees/{id}发起请求时,就会给getEmployeesById方法提供参数id的值:

http://localhost:8080/api/employees/111
---- 
ID: 111

现在我们进一步了解这个注解,并且看一看它的属性。

3. 指定路径变量名称

在前面的例子中,我们跳过了“定义模板路径变量名称”的步骤,因为方法中的参数名称和路径变量的名称是相同的,Spring自动完成了匹配。

然而,如果路径变量名称和参数名称不同,我们可以在路径变量注解@PathVariable中指定它:

@GetMapping("/api/employeeswithvariable/{id}")
@ResponseBody
public String getEmployeesByIdWithVariableName(@PathVariable("id") String employeeId) {
    return "ID: " + employeeId;
}

发起请求时识别的变量如下:

http://localhost:8080/api/employeeswithvariable/1 
----
ID: 1

我们还可以像这样清楚的定义路径变量@PathVariable(value=”id”),而不是PathVariable(“id”)

4. 在单次请求中定义多个路径变量

根据实际使用情况,我们可以在一个控制器方法的URI请求中,使用一个以上的路径变量,当然,这个方法的参数也不止一个:

@GetMapping("/api/employees/{id}/{name}")
@ResponseBody
public String getEmployeesByIdAndName(@PathVariable String id, @PathVariable String name) {
    return "ID: " + id + ", name: " + name;
}

当发起请求时返回的结果如下:

http://localhost:8080/api/employees/1/bar
----
ID: 1, name: bar

我们也可以用一个java.util.Map<String, String>:类型的方法参数,处理一个以上的@PathVariable路径变量,如下面代码,id和name参数被打包成一个整体来处理:

@GetMapping("/api/employeeswithmapvariable/{id}/{name}")
@ResponseBody
public String getEmployeesByIdAndNameWithMapVariable(@PathVariable Map<String, String> pathVarsMap) {
    String id = pathVarsMap.get("id");
    String name = pathVarsMap.get("name");
    if (id != null && name != null) {
        return "ID: " + id + ", name: " + name;
    } else {
        return "Missing Parameters";
    }
}

请求的结果:

http://localhost:8080/api/employees/1/bar
----
ID: 1, name: bar

然而当路径变量@PathVariable中出现句点符号(.)时,就会出现一些小问题。
对于这种少数情况的讨论可以查看链接

5. 可选的路径变量

在Spring中,使用@PathVariable注解的方法参数默认是必要(required)的,即一旦使用注解就必须把值传过来:

@GetMapping(value = { "/api/employeeswithrequired", "/api/employeeswithrequired/{id}" })
@ResponseBody
public String getEmployeesByIdWithRequired(@PathVariable String id) {
    return "ID: " + id;
}

如上所示,这个控制器同时处理两个路径,/api/employeeswithrequired/api/employeeswithrequired/1 request。然而因为这个方法使用了@PathVariables注解,所以它不能处理发送到这个没有参数的/api/employeeswithrequired路径的请求:

http://localhost:8080/api/employeeswithrequired
----
{"timestamp":"2020-07-08T02:20:07.349+00:00","status":404,"error":"Not Found","message":"","path":"/api/employeeswithrequired"}

http://localhost:8080/api/employeeswithrequired/1
----
ID: 111

对于这种问题,有两种处理办法。

5.1 设置@PathVariable注解为非必要(required = false)

我们可以设置@PathVariable注解的必要(required)属性为false,来让它变成可选参数,同时,加入对于参数为空时的处理办法:

@GetMapping(value = { "/api/employeeswithrequiredfalse", "/api/employeeswithrequiredfalse/{id}" })
@ResponseBody
public String getEmployeesByIdWithRequiredFalse(@PathVariable(required = false) String id) {
    if (id != null) {
        return "ID: " + id;
    } else {
        return "ID missing";
    }
}

当对于这个API发起不带参数的请求时,结果如下:

http://localhost:8080/api/employeeswithrequiredfalse
----
ID missing

5.2 使用java.util.Optional

介绍了Spring4.1之后,在JAVA8以后的版本中,我们也可以使用java.util.Optional来处理非必要的路径参数:

@GetMapping(value = { "/api/employeeswithoptional", "/api/employeeswithoptional/{id}" })
@ResponseBody
public String getEmployeesByIdWithOptional(@PathVariable Optional<String> id) {
    if (id.isPresent()) {
        return "ID: " + id.get();
    } else {
        return "ID missing";
    }
}

现在,如果不在请求中指定路径变量id,我们将会得到默认的返回结果:

http://localhost:8080/api/employeeswithoptional
----
ID missing

5.3 使用Map<String, String>类型的方法参数

在前面的示例中,我们可以使用一个java.util.Map类型的方法参数去处理URI中的所有路径变量。现在,我们也可以这样去处理非必要路径变量的情况:

@GetMapping(value = { "/api/employeeswithmap/{id}", "/api/employeeswithmap" })
@ResponseBody
public String getEmployeesByIdWithMap(@PathVariable Map<String, String> pathVarsMap) {
    String id = pathVarsMap.get("id");
    if (id != null) {
        return "ID: " + id;
    } else {
        return "ID missing";
    }
}

6. @PathVariable的默认值

开箱即用,@PathVariable注解没有定义默认值的方法。然而,我们可以用上面提到的一些办法,来让默认值满足我们的需要,只需要检查路径变量是否为null。
例如,使用java.util.Optional<String, String>,我们可以验证路径变量是不是空值,如果它是空值,就可以返回一个默认值:

@GetMapping(value = { "/api/defaultemployeeswithoptional", "/api/defaultemployeeswithoptional/{id}" })
@ResponseBody
public String getDefaultEmployeesByIdWithOptional(@PathVariable Optional<String> id) {
    if (id.isPresent()) {
        return "ID: " + id.get();
    } else {
        return "ID: Default Employee";
    }
}

7. 结论

在本文中,我们讨论了如何使用Spring的路径变量注解@PathVariable
我们有很多高效的方法去应多不同的使用场景,例如“可选参数”和“返回默认值”等。

你可以在Github上获得本文中的示例代码

分类
spring

了解Spring类型转换

译者注

原文

https://www.baeldung.com/spring-type-conversions

Demo

https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-basic-customization-2

一、介绍

在这篇文章中,我们来学习Spring类型转换。
Spring为内建的类型提供了各种开箱即用的转换器,这意味着我们可以轻松的转换基本类型,例如String、Integer、Boolean或一些其他的类型。

除此之外,Spring也提供了一个固定的类型转换器SPI,以便我们用来自定义我们的转换器。

二、内建转换器

我们从Spring提供的开箱即用的转换器开始,先看看Spring内置的整数转换器:

@Autowired
ConversionService conversionService;

@Test
public void whenConvertStringToIntegerUsingDefaultConverter_thenSuccess() {
    assertThat(
      conversionService.convert("25", Integer.class)).isEqualTo(25);
}

我们需要做的就是:自动装入(autowire)Spring提供的转换器服务ConversionService,并且调用这个转换方法convert()。第一个参数是“我们想要转换的值”,第二个参数是“我们想要转换的目标类型”

此外,还可以从Spring转换为Integer,还有多种不同的组合可以使用。

3. 创建自定义转换器

我们看一个转换的例子,在这个例子中,我们把代表Employee对象的字符串转换为Employee实例对象。

先给出Employee类:

public class Employee {

    private long id;
    private double salary;

    // standard constructors, getters, setters
}

这个字符串的形式是“逗号分隔值”,分别表示idsalary(工资)属性,例如:"1,50000.00"

为了创建我们的自定义转换器,我们需要实现转换器接口Converter<S, T>,并且实现转换方法convert()

public class StringToEmployeeConverter
  implements Converter<String, Employee> {

    @Override
    public Employee convert(String from) {
        String[] data = from.split(",");
        return new Employee(
          Long.parseLong(data[0]), 
          Double.parseDouble(data[1]));
    }
}

现在还没有完成,我们还需要把刚刚创建的StringToEmployeeConverter类添加到格式注册器FormatterRegistry中,来告诉Spring我们用到了这个类。具体的做法就是实现WebMvcConfigurer类,并且重写Formatters()方法:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToEmployeeConverter());
    }
}

这样就完成了。我们的新转换器就在ConversionService中可用,并且我们可以使用同样的方式把它用在其他内置转换器上:

@Test
public void whenConvertStringToEmployee_thenSuccess() {
    Employee employee = conversionService
      .convert("1,50000.00", Employee.class);
    Employee actualEmployee = new Employee(1, 50000.00);
    assertThat(conversionService.convert("1,50000.00",
      Employee.class))
      .isEqualToComparingFieldByField(actualEmployee);
}

3.1 隐式转换

除了使用转换服务ConversionService来实现显式转换,Spring能在控制器方法中,使用所有已注册的转换器来实现隐式转换:

@RestController
public class StringToEmployeeConverterController {

    @GetMapping("/string-to-employee")
    public ResponseEntity<Object> getStringToEmployee(
      @RequestParam("employee") Employee employee) {
        return ResponseEntity.ok(employee);
    }
}

这样使用转换器更自然,我们对它进行一下测试:

@Test
public void getStringToEmployeeTest() throws Exception {
    mockMvc.perform(get("/string-to-employee?employee=1,2000"))
      .andDo(print())
      .andExpect(jsonPath("$.id", is(1)))
      .andExpect(jsonPath("$.salary", is(2000.0)))
}

你就可以看到,这个测试将会输出请求和相应的详细信息,就像下面这个JSON格式的Employee对象:

{"id":1,"salary":2000.0}

4. 创建一个转换器工厂

我们还可以创建一个用来按需创建转换器的转换器工厂ConverterFactory,这在创建枚举类型转换器的时候尤其有用。

我们来看一个简单的枚举:

public enum Modes {
    ALPHA, BETA;
}

接下来,创建一个“字符串到枚举类型转换器工厂”StringToEnumConverterFactory,这个工厂可以创建字符串到枚举的转换器:

@Component
public class StringToEnumConverterFactory 
  implements ConverterFactory<String, Enum> {

    private static class StringToEnumConverter<T extends Enum> 
      implements Converter<String, T> {

        private Class<T> enumType;

        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }

    @Override
    public <T extends Enum> Converter<String, T> getConverter(
      Class<T> targetType) {
        return new StringToEnumConverter(targetType);
    }
}

正如你看到的,这个工厂类内部使用一个转换器接口的实现。
需要注意的是,尽管我们将会使用我们的Modes Enum去演示用法,但我们在StringToEnumConverterFactory工厂的任何地方都没有提及这个枚举。我们这个工厂类会按需创建对于任何枚举类型的转换器。

下一步是注册这个工厂类,操作就像注册我们的Converter转换器一样,例子如下:

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new StringToEmployeeConverter());
    registry.addConverterFactory(new StringToEnumConverterFactory());
}

现在这个转换服务ConversionService可以把字符串转换成枚举了:

@Test
public void whenConvertStringToEnum_thenSuccess() {
    assertThat(conversionService.convert("ALPHA", Modes.class))
      .isEqualTo(Modes.ALPHA);
}

5. 创建一个通用转换器

通用转换器GenericConverter让我们在牺牲安全性的前提下,获得更大的灵活性,来创建一个更通用的转换器。
我们来设计一个可以转换Integer、Double、StringBigDecimal的转换器。
不需要去写三个转换器,而是使用通用转换器GenericConverter来达到这个目的。

第一步是告诉Spring我们希望这个转换器支持什么类型。操作时创建一个ConvertiblePair的集合(每个元素是一对类型,表示可以转换):

public class GenericBigDecimalConverter 
  implements GenericConverter {

@Override
public Set<ConvertiblePair> getConvertibleTypes () {

    ConvertiblePair[] pairs = new ConvertiblePair[] {
          new ConvertiblePair(Number.class, BigDecimal.class),
          new ConvertiblePair(String.class, BigDecimal.class)};
        return ImmutableSet.copyOf(pairs);
    }
}

下一步是在同样的方法里重写转换方法convert()

@Override
public Object convert (Object source, TypeDescriptor sourceType, 
  TypeDescriptor targetType) {

    if (sourceType.getType() == BigDecimal.class) {
        return source;
    }

    if(sourceType.getType() == String.class) {
        String number = (String) source;
        return new BigDecimal(number);
    } else {
        Number number = (Number) source;
        BigDecimal converted = new BigDecimal(number.doubleValue());
        return converted.setScale(2, BigDecimal.ROUND_HALF_EVEN);
    }
}

转换方法很简单,但TypeDescriptor在获取有关源和目标的详细信息时相当的灵活。
你可能已经猜到了,下一步是注册这个转换器:

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new StringToEmployeeConverter());
    registry.addConverterFactory(new StringToEnumConverterFactory());
    registry.addConverter(new GenericBigDecimalConverter());
}

调用这个转换器的方法和前面的例子相似:

@Test
public void whenConvertingToBigDecimalUsingGenericConverter_thenSuccess() {
    assertThat(conversionService
      .convert(Integer.valueOf(11), BigDecimal.class))
      .isEqualTo(BigDecimal.valueOf(11.00)
      .setScale(2, BigDecimal.ROUND_HALF_EVEN));
    assertThat(conversionService
      .convert(Double.valueOf(25.23), BigDecimal.class))
      .isEqualByComparingTo(BigDecimal.valueOf(Double.valueOf(25.23)));
    assertThat(conversionService.convert("2.32", BigDecimal.class))
      .isEqualTo(BigDecimal.valueOf(2.32));
}

6. 结论

本文中,我们学习了如何使用和拓展Spring的类型转换系统,并且给出了一些示例。
和往常一样,你可以在Github上找到本文的示例代码。

分类
Data Jackson Json

初步了解Jackson对象映射器

译者注

原文

https://www.baeldung.com/jackson-object-mapper-tutorial

Demo

https://github.com/eugenp/tutorials/tree/master/jackson-simple

1. 前言

本文致力于帮助读者理解Jackson对象映射器(ObjectMapper),以及学习如何把对象序列化为JSON、如何把JSON反序列化成Java对象。

如果希望了解更多Jackson库的知识,请点击Jackson入门教程

2. 依赖

对于maven托管的项目,我们首先在pom.xml中增加下面的依赖信息:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.13.3</version>
</dependency>

这个依赖递归的将以下两个库添加到类路径中:

  1. jackson-annotations
  2. jackson-core

我们只需要使用Maven中心仓库的jackson-databind中的最新版本。

3. 使用对象映射器进行读写

我们从最简单的读写操作开始。
对象映射器的简单读取API是一个很好的切入点,我们可以用它来分析JSON内容,或者把JSON反序列化成Java对象。
另一方面,我们可以使用读取值API来把Java对象序列化成JSON格式的输出。
我们将会使用带有两个字段的“汽车”作为示例代码,来演示序列化和反序列化功能:

public class Car {

    private String color;
    private String type;

    // standard getters setters
}

3.1 Java 对象到 JSON

先来看第一个示例,使用对象映射器的写方法来实现Java对象到JSON:

ObjectMapper objectMapper = new ObjectMapper();
Car car = new Car("yellow", "renault");
objectMapper.writeValue(new File("target/car.json"), car);

上面代码输出的信息将会是:

{"color":"yellow","type":"renault"}

对象映射器的writeValueAsString()方法和writeValueAsBytes()方法会从Java对象生成JSON,并且以字符串或二进制数组的方式返回生成的JSON:

String carAsString = objectMapper.writeValueAsString(car);

3.2 JSON到Java对象

在这个示例中,使用对象映射器类来把JSON字符串转化为Java对象:

String json = "{ \"color\" : \"Black\", \"type\" : \"BMW\" }";
Car car = objectMapper.readValue(json, Car.class);

这个读方法也支持其他输入形式,如文件:

Car car = objectMapper.readValue(new File("src/test/resources/json_car.json"), Car.class);

或者URL:

Car car = objectMapper.readValue(new URL("file:src/test/resources/json_car.json"), Car.class);

3.3 JSON 到 Jackson JsonNode

除了转化为对象,我们也可以把JSON数据转化为JSON节点对象,这样就可以在不创建具体类的情况下,直接从某个JSON对象中获得某个字段的数据:

String json = "{ \"color\" : \"Black\", \"type\" : \"FIAT\" }";
JsonNode jsonNode = objectMapper.readTree(json);
String color = jsonNode.get("color").asText();
// Output: color -> Black

3.4 根据JSON数组字符串创建Java列表

我们可以使用类型引用(TypeReference),把JSON从数组格式转化为Java对象列表:

String jsonCarArray = "[{ \"color\" : \"Black\", \"type\" : \"BMW\" }, { \"color\" : \"Red\", \"type\" : \"FIAT\" }]";

List<Car> listCar = objectMapper.readValue(jsonCarArray, new TypeReference<List<Car>>(){});

3.5 从JSON字符串创建Java映射(Map)

相似的,我们可以把JSON解析成一个Java映射:

String json = "{ \"color\" : \"Black\", \"type\" : \"BMW\" }";

Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String,Object>>(){});

4. 高级特性

Jackson库的强大功能之一,就是高度自由定制的序列化和反序列化方法。
在这个版块,我们将会学习一些高级特性,它允许你输入(或输出)的JSON和生成(或消耗)的对象不同。

4.1 配置序列化和反序列化特性

当我们把JSON对象转化为Java类时,如果JSON字符串有一些新字段,在默认设置下会导致异常:

String jsonString = "{ \"color\" : \"Black\", \"type\" : \"Fiat\", \"year\" : \"1970\" }";

上面示例中的JSON字符串,如果使用默认的解析设置,在转化为Java对象Car时就会出现“未识别的属性异常”(UnrecognizedPropertyException)。
通过配置方法,我们可以扩展默认的进程,实现忽略空字段。

objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Car car = objectMapper.readValue(jsonString, Car.class);

JsonNode jsonNodeRoot = objectMapper.readTree(jsonString);
JsonNode jsonNodeYear = jsonNodeRoot.get("year");
String year = jsonNodeYear.asText();

另一个基于FAIL_ON_NULL_FOR_PRIMITIVES的选项定义了“是否允许主键空值”:

objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);

相似地,FAIL_ON_NUMBERS_FOR_ENUM选项控制“枚举值是否允许序列化、反序列化成数字”

objectMapper.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, false);

你可以在官方网站中找到序列化和反序列化的详尽表单

4.2 创建用户自定义的序列化器和反序列化器

对象映射器类的另一个重要特性是:允许注册一个用户自定义的序列化器和反序列化器。
自定义的序列化器和反序列化器在某些情况下非常有用,比如输入或输出的JSON和需要比序列化或反序列化的JAVA类结构不同。
下面是一个自定义JSON序列化器的例子:

public class CustomCarSerializer extends StdSerializer<Car> {

    public CustomCarSerializer() {
        this(null);
    }

    public CustomCarSerializer(Class<Car> t) {
        super(t);
    }

    @Override
    public void serialize(
      Car car, JsonGenerator jsonGenerator, SerializerProvider serializer) {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeStringField("car_brand", car.getType());
        jsonGenerator.writeEndObject();
    }
}

这个自定义的序列化器可以像这样被调用:

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = 
  new SimpleModule("CustomCarSerializer", new Version(1, 0, 0, null, null, null));
module.addSerializer(Car.class, new CustomCarSerializer());
mapper.registerModule(module);
Car car = new Car("yellow", "renault");
String carJson = mapper.writeValueAsString(car);

此时,客户端将会收到类似这样的信息:

var carJson = {"car_brand":"renault"}

接下来,这是一个自定义JSON反序列化的例子:

public class CustomCarDeserializer extends StdDeserializer<Car> {

    public CustomCarDeserializer() {
        this(null);
    }

    public CustomCarDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Car deserialize(JsonParser parser, DeserializationContext deserializer) {
        Car car = new Car();
        ObjectCodec codec = parser.getCodec();
        JsonNode node = codec.readTree(parser);

        // try catch block
        JsonNode colorNode = node.get("color");
        String color = colorNode.asText();
        car.setColor(color);
        return car;
    }
}

这个自定义的反序列化可以像这样被调用:

String json = "{ \"color\" : \"Black\", \"type\" : \"BMW\" }";
ObjectMapper mapper = new ObjectMapper();
SimpleModule module =
  new SimpleModule("CustomCarDeserializer", new Version(1, 0, 0, null, null, null));
module.addDeserializer(Car.class, new CustomCarDeserializer());
mapper.registerModule(module);
Car car = mapper.readValue(json, Car.class);

4.3 处理日期格式

java.util.Date这个类提供的默认的序列化器,在处理日期时会提供数字形式,例如时间戳(时间戳是一个数字,UTC时间是从1970年1月1日开始经过的毫秒数)。但是这种格式对于用户的可读性比较长,并且需要进一步转化格式才能变成用户可以阅读的格式。
我们把刚才用到的这个Car类,包装到一个带有datePurchased属性的Request类中(装饰器模式):

public class Request 
{
    private Car car;
    private Date datePurchased;

    // standard getters setters
}

为了控制字符串格式,比如设置为“yyyy-MM-dd HH:mm” (年-月-日 时:分),可以使用下面的代码:

ObjectMapper objectMapper = new ObjectMapper();
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm a z");
objectMapper.setDateFormat(df);
String carAsString = objectMapper.writeValueAsString(request);
// output: {"car":{"color":"yellow","type":"renault"},"datePurchased":"2016-07-03 11:43 AM CEST"}

如果要了解更多使用Jackson序列化时间的方法,请点击链接

4.4 处理集合

DeserializationFeature类提供了另一个很小但是很有用的特性,那就是允许从JSON数组生成我们想要的类型的集合。
例如,我们可以生成这样一个数组类型的结果:

String jsonCarArray = 
  "[{ \"color\" : \"Black\", \"type\" : \"BMW\" }, { \"color\" : \"Red\", \"type\" : \"FIAT\" }]";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY, true);
Car[] cars = objectMapper.readValue(jsonCarArray, Car[].class);
// print cars

或者列表形式:

String jsonCarArray = 
  "[{ \"color\" : \"Black\", \"type\" : \"BMW\" }, { \"color\" : \"Red\", \"type\" : \"FIAT\" }]";
ObjectMapper objectMapper = new ObjectMapper();
List<Car> listCar = objectMapper.readValue(jsonCarArray, new TypeReference<List<Car>>(){});
// print cars

这里有用Jackson处理集合的更多信息。

5. 结论

Jackson是一个成熟稳定的JSON序列化、反序列化的Java库。对象映射器API提供了直接的方式,去灵活的解析和生成JSON对象的方式。
本文讨论了一些知名度非常高的主要特性。
源代码在Github上提供。