Murain的笔记

Murain的笔记

记录遇到的一些问题和解决方法

我以前在项目中导出word文档需要使用Freemarker编写xml,非常麻烦。直到我发现了Poi-tl,在Poi-tl中使用Word文档就能创建文档模板。

创建模板

直接新建template.docx

{{title}}

写入内容

XWPFTemplate template = XWPFTemplate.compile("template.docx").render(
  new HashMap<String, Object>(){{
    put("title", "Hi, poi-tl Word模板引擎");
}});  
template.writeAndClose(new FileOutputStream("output.docx")); 

具体使用在官方文档中有非常详细的教程


v11文档
PostgreSQL支持三种分表方式:范围分区(RANGE)、列表分区(LIST)和哈希分区(HASH)。

  1. 范围分区(RANGE):按照某列(或某几列)的值划分成互不重叠的区间,如按时间按月划分订单表。
  2. 列表分区(LIST):按照枚举出的值进行分区,例如按国家划分客户表。
  3. 哈希分区(HASH):根据某列(或某几列)的哈希值对模取余,余数相同的划分到同一分区,如按产品编号哈希分区的产品表。

PostgreSQL 10.x版本之前,分表需要使用继承和触发器来实现,但从PostgreSQL 10.x版本开始,引入了声明式的分表方式,更加简化操作。

版本特性:

  • PostgreSQL 11 引入了声明式分表的功能,支持HASH分区方法,并自动传播约束、索引、触发器等到子表。
  • PostgreSQL 12 进一步提升了声明式分表性能,支持CHECK约束、INSERT ON CONFLICT、SELECT FOR UPDATE等操作自动路由到子表。
  • PostgreSQL 15 增加了对IDENTITY列、生成列、排除约束等的支持,以及改进了查询优化器对分区谓词的处理。

    练习 以地区和时间进行分区

    表 fire_points 结构如下

    列名数据类型长度小数位数允许空值主键备注
    idint4320FALSETRUE火点ID
    fire_timetimestamp60TRUEFALSE火点发生时间
    province_codechar60TRUEFALSE省份代码
    city_codechar60TRUEFALSE城市代码
    county_codechar60TRUEFALSE县区代码
    town_codevarchar500TRUEFALSE乡镇代码
    latitudenumeric96TRUEFALSE纬度
    longitudenumeric86TRUEFALSE经度
    shapegeometry00TRUEFALSE空间信息

先根据省份代码列表分区,然后根据时间按月范围分区

DO $$
DECLARE
    province_codes VARCHAR[] := ARRAY[
        '110000', '120000', '130000', '140000', '150000', '210000', '220000', '230000',
        '310000', '320000', '330000', '340000', '350000', '360000', '370000', '410000',
        '420000', '430000', '440000', '450000', '460000', '500000', '510000', '520000',
        '530000', '540000', '610000', '620000', '630000', '640000', '650000', '710000',
        '810000', '820000'
    ];
    province_code CHAR(6);
    sql_query TEXT;
BEGIN
    -- 1. 创建主分区表 fire_points_partitioned
    CREATE TABLE IF NOT EXISTS fire_points_partitioned (
        id SERIAL,
        fire_time TIMESTAMP,
        province_code CHAR(6),
        city_code CHAR(6),
        county_code CHAR(6),
        town_code VARCHAR(50),
        latitude NUMERIC(8, 6),
        longitude NUMERIC(9, 6),
        shape GEOMETRY
    ) PARTITION BY LIST (province_code);
 
    -- 2. 根据省份代码列表创建对应的分区表
    FOREACH province_code IN ARRAY province_codes
    LOOP
        -- 3. 创建省份分区表
        sql_query := format(
            'CREATE TABLE IF NOT EXISTS fire_points_partitioned_%s PARTITION OF fire_points_partitioned FOR VALUES IN (%L)',
            province_code, province_code
        );
        EXECUTE sql_query;
    END LOOP;
     
    -- 4. 添加索引
    EXECUTE 'CREATE INDEX IF NOT EXISTS idx_fire_points_partitioned_province_code ON fire_points_partitioned (province_code)';
    EXECUTE 'CREATE INDEX IF NOT EXISTS idx_fire_points_partitioned_fire_time ON fire_points_partitioned (fire_time)';
     
END;
$$;

这样,fire_points_partitioned表将根据省份代码进行列表分区,并在province_code和fire_time字段上添加索引。


查看内核版本

$ uname-r
3.10.0-1160.53.1.el7.x86_64

安装elrepo的yum源

# 在线地址
yum install https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
# rpm文件
yum install elrepo-release-7.el7.elrepo.noarch.rpm

查询可用的内核

$ yum --disablerepo="*" --enablerepo="elrepo-kernel" list available
已加载插件: fastestmirror
Loading mirror speeds from cached hostfile
* elrepo-kernel: mirrors.tuna.tsinghua.edu.cn
可安装的软件包
kernel-lt.x86_64                    5.4.251-1.el7.elrepo    elrepo-kernel
kernel-t-devel.x86_64               5.4.251-1.el7.elrepo    elrepo-kernel
kernel-It-doc.noarch                5.4.251-1.el7.elrepo    elrepo-kernel
kernel-lt-headers.x86_64            5.4.251-1.el7.elrepo    elrepo-kernel
kernel-lt-tools.x86_64              5.4.251-1.el7.elrepo    elrepo-kernel
kernel-lt-tools-libs.x86_64         5.4.251-1.el7.elrepo    elrepo-kernel
kernel-lt-tools-libs-devel.x86_64   5.4.251-1.el7.elrepo    elrepo-kernel
kernel-ml-doc.noarch                6.4.7-1.el7.elrepo      elrepo-kernel
kernel-ml-headers.x86_64            6.4.7-1.el7.elrepo      elrepo-kernel
kernel-ml-tools.x86_64              6.4.7-1.el7.elrepo      elrepo-kernel
kernel-ml-tools-libs.x86_64         6.4.7-1.el7.elrepo      elrepo-kernel
kernel-ml-tools-libs-devel.x86_64   6.4.7-1.el7.elrepo      elrepo-kernel
perf.x86_64                         5.4.251-1.el7.elrepo    elrepo-kernel
python-perf.x86_64                  5.4.251-1.el7.elrepo    elrepo-kernel

安装最新的主线稳定内核

yum -y --enablerepo=elrepo-kernel install kernel-ml.x86_64 kernel-ml-devel.x86_64

修改内核启动顺序

$ awk -F\' '$1=="menuentry " {print $2}' /etc/grub2.cfg
Centos Linux (6.4.7-1.el7.elrepo.x86 64) 7 (Core)                   # 0
Centos Linux (3.10.0-1160.92.1.el7.x86 64) 7 (Core)                 # 1
CentOs Linux (3.10.0-1160.53.1.el7.x86 64) 7 (Core)                 # 2
CentOs Linux (3.10.0-1160.el7.x86 64) 7 (Core)                      # 3
CentOs Linux (0-rescue-acca19161ce94d449c58923b12797030) 7 (Core)   # 4
$ grub2-set-default 0 # 设置默认选择第0个 重新启动时生效
# 重启后使用`uname-r`检查内核版本是否正确

Shapefile文件结构

Shapefile(SHP文件)通常由多个文件组成,每个文件都具有特定的后缀名并承载不同类型的数据。
以下是Shapefile各个文件后缀名的解释:

  1. .shp:主文件,它包含了地理要素的几何形状信息。每个几何对象(如点、线、多边形)都存储在此文件中。
  2. .shx:索引文件,它包含了地理要素的索引信息。索引文件加快了要素的读取和查询速度,可以通过索引快速定位到指定要素的位置。
  3. .dbf:属性表文件,它包含了地理要素的属性信息。属性表文件以dBase格式存储,每个记录代表一个要素,每个字段存储一个属性值。
  4. .prj:投影文件,它定义了地理数据的坐标系和投影信息。投影文件描述了地理数据如何映射到地图的平面坐标系统,以便正确显示和分析。
  5. .sbn.sbx:空间索引文件和空间索引索引文件的补充文件。这些文件存储了Shapefile中的要素的空间索引信息,用于快速查找空间关系和邻近要素。
  6. .cpg:代码页文件,它指定了属性表文件中文本字段的字符编码格式。它用于确保文本字段的正确显示和解释。

这些文件的后缀名和对应的文件内容结合在一起,构成了Shapefile的完整数据集。在使用Shapefile时,通常需要将这些文件一起保持在同一目录中,以便正确读取和使用地理数据。

使用Java解析

在Java中使用geotools解析shp

POM 依赖

<project>
    <repositories>
        <repository>
            <id>osgeo</id>
            <name>OSGeo Release Repository</name>
            <url>https://repo.osgeo.org/repository/release/</url>
        </repository>
    </repositories>
    <dependencies>
        <dependency>
            <groupId>org.geotools</groupId>
            <artifactId>gt-referencing</artifactId>
            <version>24.7</version>
        </dependency>
        <dependency>
            <groupId>org.geotools</groupId>
            <artifactId>gt-epsg-hsql</artifactId>
            <version>24.7</version>
        </dependency>
        <dependency>
            <groupId>org.geotools</groupId>
            <artifactId>gt-epsg-extension</artifactId>
            <version>24.7</version>
        </dependency>
        <dependency>
            <groupId>org.geotools</groupId>
            <artifactId>gt-jts-wrapper</artifactId>
            <version>24.7</version>
        </dependency>
        <dependency>
            <groupId>org.geotools</groupId>
            <artifactId>gt-shapefile</artifactId>
            <version>24.7</version>
        </dependency>
    </dependencies>
</project>

示例代码

// 请注意此代码示例仅作参考
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.referencing.CRS;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.geometry.Geometry;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

public class Shapefile {

    public static void main(String[] args) throws IOException, FactoryException {
        File shapefile = new File("xxx.shp");
        ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();
        Map<String, Serializable> params = new HashMap<>();
        params.put("url", shapefile.toURI().toURL());
        params.put("create spatial index", Boolean.TRUE);
        params.put("charset", "UTF-8");
        ShapefileDataStore dataStore = (ShapefileDataStore) dataStoreFactory.createDataStore(params);
        // 获取数据集名称
        for (String typeName : dataStore.getTypeNames()) {
            // 获取数据集的元数据信息
            SimpleFeatureType schema = dataStore.getSchema(typeName);
            // 获取坐标系
            CoordinateReferenceSystem crs = schema.getCoordinateReferenceSystem();
            // 获取坐标系的EPSG代码
            Integer srid = CRS.lookupEpsgCode(crs, true);
            SimpleFeatureSource featureSource = dataStore.getFeatureSource();
            SimpleFeatureCollection features = featureSource.getFeatures();
            try (SimpleFeatureIterator iterator = features.features()) {
                while (iterator.hasNext()) {
                    SimpleFeature feature = iterator.next();
                    Geometry geometry = (Geometry) feature.getDefaultGeometry();
                }
            }
        }
    }
}

定义

CRS 是坐标参照系统(Coordinate Reference System)的缩写。在地理信息系统(GIS)中,坐标参照系统是一个用于描述地球或其部分的坐标系统和地理数据集。它包括一个坐标系统和一个地理坐标的转换方法。

CRS 可以分为两类:地理坐标参照系统(Geographic CRS)和投影坐标参照系统(Projected CRS)。

  • 地理坐标参照系统(Geographic CRS):也称为地理坐标系统,它使用经度和纬度来定义地球表面上的位置。这种系统通常用于全球范围内的地图。
  • 投影坐标参照系统(Projected CRS):这是一种地理坐标系统的平面表示,它将地球表面的三维形状转换为二维平面。这种转换通常会导致一些扭曲,但在小范围内(如城市或国家)这种扭曲是可以接受的。

在 GIS 中,选择正确的 CRS 是非常重要的,因为它会影响到地图的准确性和可用性。

SRID 是 Spatial Reference System Identifier 的缩写。在地理信息系统(GIS)中,SRID 是用来唯一标识坐标参照系统(CRS)的数字。每个坐标参照系统都有一个与之关联的 SRID。

描述CRS

通常会使用两种主要的文本格式:WKT(Well-Known Text)和 PROJ。

WKT(Well-Known Text):这是一种文本标记语言,用于表示矢量几何对象、空间参照系统以及空间参照系统之间的转换。WKT 格式的 CRS 描述非常详细,包括了投影方法、地理区域、单位、轴信息等。

PROJ:这是一个开源库,用于转换地理坐标。PROJ 字符串是一种简洁的方式来描述 CRS,它包括了投影方法和参数。虽然 PROJ 字符串比 WKT 更简洁,但它可能不包含 WKT 所有的信息。

这两种格式都可以用来在 GIS 软件中定义和转换 CRS。选择使用哪种格式取决于你的具体需求和你正在使用的 GIS 软件。


在项目中遇到了需要在实现HTTP反向代理的同时作为一个网关进行鉴权。我选择了 Smiley's HTTP Proxy Servlet 来实现。

原始ProxyServlet类

package org.mitre.dsmiley.httpproxy;

import javax.servlet.http.HttpServlet;

/**
 * An HTTP reverse proxy/gateway servlet. It is designed to be extended for customization
 * if desired. Most of the work is handled by
 * <a href="http://hc.apache.org/httpcomponents-client-ga/">Apache HttpClient</a>.
 * <p>
 *   There are alternatives to a servlet based proxy such as Apache mod_proxy if that is available to you. However
 *   this servlet is easily customizable by Java, secure-able by your web application's security (e.g. spring-security),
 *   portable across servlet engines, and is embeddable into another web application.
 * </p>
 * <p>
 *   Inspiration: http://httpd.apache.org/docs/2.0/mod/mod_proxy.html
 * </p>
 *
 * @author David Smiley dsmiley@apache.org
 */
@SuppressWarnings({"deprecation", "serial", "WeakerAccess"})
public class ProxyServlet extends HttpServlet {
    
  @Override
  protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
      throws ServletException, IOException {
    //...
  }
    // ...
}

对这个类进行一些重写

自定义异常处理

/**
 * 重写service方法,处理内部的异常
 */
override fun service(servletRequest: HttpServletRequest?, servletResponse: HttpServletResponse?) {
    logger.info("========= 重写请求 =========")
    try {
        super.service(servletRequest, servletResponse)
    } catch (e: ZZException) {
        logger.error("ZZException", e)
        handleZZException(e, servletRequest!!, servletResponse!!)
    }
}

/**
 * 处理Servlet的service异常部分,
 */
private fun handleZZException(ex: ZZException, request: HttpServletRequest, response: HttpServletResponse) {
    logger.info("RequestURI:{}", request.requestURI)
    // 设置HTTP状态码为403 Forbidden
    response.status = HttpStatus.FORBIDDEN.value()

    // 设置响应类型为JSON
    response.contentType = "application/json;charset=UTF-8"

    // 创建一个包含错误信息的map
    val errorDetails = mapOf(
        "code" to ex.code,
        "msg" to ex.message
    )
    // 将错误信息map转换为JSON字符串
    val json = jacksonObjectMapper().writeValueAsString(errorDetails)

    // 写入JSON字符串到响应体
    response.writer.write(json)

    // 记录日志或其他处理...
}

重写请求的query参数

/**
 * 重写请求的query参数
 */
override fun rewriteQueryStringFromRequest(servletRequest: HttpServletRequest, queryString: String?): String {
    if (queryString.isNullOrBlank()) {
        return ""
    }
    logger.debug("请求参数: $queryString")

    val queryStrBuffer = StringBuffer(queryString.length)
    queryString.split("&".toRegex()).filter { it.isNotBlank() }.forEach {
        processQueryParam(it, queryStrBuffer)
    }
    // 去掉最后一个&
    if (queryStrBuffer.endsWith("&")) {
        val removeRange = queryStrBuffer.removeRange(queryStrBuffer.length - 1, queryStrBuffer.length)
        return removeRange.toString()
    }
    return queryStrBuffer.toString()
}

/**
 * 处理请求参数
 */
protected open fun processQueryParam(param: String, queryStrBuffer: StringBuffer) {
    // tk
    if (param.lowercase().contains("tk=")) {
        val tkStr = param.substring(3)
        if (StrUtil.isBlank(tkStr)) {
            throw ZZException("403", "令牌不能为空")
        }
        tkTL.set(tkStr)
        return
    }
    queryStrBuffer.append("$param&")
}

重写请求的url

protected fun superRewriteUrlFromRequest(servletRequest: HttpServletRequest): String =
    super.rewriteUrlFromRequest(servletRequest)

/**
 * 重写请求的url
 */
override fun rewriteUrlFromRequest(servletRequest: HttpServletRequest): String {

    logger.debug("========= 重写请求url =========")
    // 制作请求 URI
    val uri = StringBuilder(500)

    // 目标uri
    uri.append(getTargetUri(servletRequest))

    // 处理给servlet的路径
    val pathInfo = rewritePathInfoFromRequest(servletRequest)
    uri.append(URLUtil.encode(pathInfo))

    // 处理 query string & fragment
    var queryString = URLUtil.decode(servletRequest.queryString)
    var fragment: String? = null
    // 从 queryString 中分离出 fragment,如果找到则更新 queryString
    val fragIdx = queryString?.indexOf('#')
    if (fragIdx != null) {
        if (fragIdx >= 0) {
            fragment = queryString.substring(fragIdx + 1)
            queryString = queryString.substring(0, fragIdx)
        }
    }

    queryString = rewriteQueryStringFromRequest(servletRequest, queryString)

    // 获取请求的ip
    val ip = getIpAddr(servletRequest)

    // 处理令牌
    val (appInfo, layerInfo) = checkAuth(tkTL.get(), ip, layerIdTL.get())
    // 记录日志
    processLogInfo(appInfo, layerInfo)

    uri.append('?')
    uri.append(queryString)
    if (doSendUrlFragment && fragment != null) {
        uri.append('#')
        uri.append(encodeUriQuery(fragment, false))
    }
    logger.info("重写后的url: $uri")
    return uri.toString()
}

鉴权处理令牌

protected open fun checkAuth(
    tk: String,
    ip: String,
    layerId: String
): Pair<Map<String, Any>, Map<String, Any?>> {
    logger.debug("tk:$tk\nlayerId:$layerId")
    val tokenMap = redisTool.getMap<String, Map<String, Any?>>("$platformName:platform:app:token")
    val appInfo = tokenMap.getMap(tk)
    if (appInfo.isEmpty()) {
        throw ZZException("403", "令牌无效")
    }
    //校验ip是否在白名单内
    checkIp(appInfo.getString("appIpWhitelist"), ip)

    val layers: List<Map<String, Any?>> = appInfo.getList("layers")
    val layerInfo = layers.find { StrUtil.equals(it.getString("lRealLayerId"), layerId) }
        ?: throw ZZException("403", "令牌与图层不匹配")
    logger.info("图层信息: $layerInfo")
    return Pair(appInfo, layerInfo)
}

注入Servlet

/**
 * OGC标准服务代理
 * WMTS
 */
@Bean
fun ogcServletRegistrationBean(proxyServlet: OGCServerProxyServlet): ServletRegistrationBean<OGCServerProxyServlet> {
    val servletRegistrationBean = ServletRegistrationBean(proxyServlet, "/geoserver/*")
    servletRegistrationBean.addInitParameter(ProxyServlet.P_TARGET_URI, "http://127.0.0.1:8080/geowebcache/service")
    servletRegistrationBean.addInitParameter(ProxyServlet.P_LOG, "true")
    return servletRegistrationBean
}