-
一篇介绍了Elasticsearch的入门《5000字详说Elasticsearch入门(一)》,本篇介绍Springboot如何集成使用Elasticsearch。分为3步:配置properties文件、引入pom依赖、配置RestHighLevelClient类。 1、选择ES的Client API 我们知道Elasticsearch是一款Restful API风格的分布式搜索引擎。ES Client有两种连接方式:TransportClient 和 RestClient。TransportClient通过TCP方式访问ES,RestClient方式通过Http方式访问ES。ES在7.0中已经弃用TransportClient,在8.0中完全删除它,所以建议使用RestClient的方式。 RestClient方式有多种实现,比如:ES自带的RestHighLevelClient 、Springboot实现的ElasticsearchRestTemplate 。笔者建议使用RestHighLevelClient,原因有3个: 使用RestHighLevelClient比较灵活,可以直接使用ES的DSL语法,实现复杂查询,同时没有与其他部件绑定,所以版本可以自由选择。 由于ElasticsearchRestTemplate是spring-boot-starter-data-elasticsearch封装的工具类,虽然使用上稍微方便一些,但是失去了灵活性,出现问题时也不易排查。而且ElasticsearchRestTemplate本身与spring-boot-starter-data-elasticsearch紧密依赖。如果想升级ElasticsearchRestTemplate,那就必须连带升级项目的Springboot版本,这个风险就比较高了,一般项目的Springboot版本不会轻易升级。 还有些第三方的插件,将SQL转成ES的DSL,笔者也不建议使用。它们即不灵活,更新速度也慢。这里也再次吐槽下,ES的DSL设计确实有点反人类,不过也不用死记,可以通过归类记忆核心内容,其余使用细节直接查阅官方文档就好。 整理来说,用ES提供的RestHighLevelClient是最稳定最方便的。由于使用RestHighLevelClient,多少会涉及到ES的DSL语法,详见官网:Match phrase query | Elasticsearch Guide [7.17] | Elastic。 下文中的示例中使用RestHighLevelClient时,也会附带上对应的DSL语法。 2、配置properties文件 # ES集群机器 elasticsearch.hosts=10.20.1.29,10.20.0.91,10.20.0.93 # ES提供服务的端口号 elasticsearch.port=9200 3、引入pom依赖 <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.10.2</version> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-client</artifactId> <version>7.10.2</version> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.10.2</version> </dependency> 4、配置RestHighLevelClient @Data @Configuration @ConfigurationProperties(prefix = "elasticsearch") public class ElasticSearchConfig { private String hosts; private Integer port; @Bean public RestHighLevelClient restHighLevelClient() { HttpHost[] httpHosts = Arrays.stream(hosts.split(",")) .filter(e -> !StringUtils.isEmpty(e)) .map(e -> new HttpHost(e, port, "http")) .toArray(HttpHost[]::new); return new RestHighLevelClient( RestClient.builder( httpHosts ) ); } } 至此Springboot与Elasticsearch的集成已经结束,接下来就是使用了。 5、使用RestClient API 下文演示常规场景下的RestClient API的使用方式和对应的DSL语法,涉及到的相关完整代码见如下地址: 代码地址:GitHub - yclxiao/spring-elasticsearch: Springboot集成Elasticsearch 5.1、创建索引,指定Mapping 通过代码里调用RestClient的API也可以创建索引,但是笔者建议统一通过如下方式建立,字段和类型非常清晰可控,方便统一管理。比如创建一个goods索引: PUT /goods { "mappings": { "properties": { "brandName": { "type": "keyword" }, "categoryName": { "type": "keyword" }, "createTime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" }, "id": { "type": "keyword" }, "price": { "type": "double" }, "saleNum": { "type": "integer" }, "status": { "type": "integer" }, "stock": { "type": "integer" }, "title": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" } } } } 5.2、创建实例类 @Data @AllArgsConstructor @NoArgsConstructor public class Goods { /** * 商品编号 */ private Long id; /** * 商品标题 */ private String title; /** * 商品价格 */ private BigDecimal price; /** * 商品库存 */ private Integer stock; /** * 商品销售数量 */ private Integer saleNum; /** * 商品分类 */ private String categoryName; /** * 商品品牌 */ private String brandName; /** * 上下架状态 */ private Integer status; /** * 商品创建时间 */ @JSONField(format = "yyyy-MM-dd HH:mm:ss") private Date createTime; } 5.3、使用API操作数据 下面介绍主流场景下API的使用,每个场景会配上对应的DSL语法。更多语法详见官方文档:Match phrase query | Elasticsearch Guide [7.17] | Elastic。 5.3.1、增加文档 // 创建索引请求对象 IndexRequest indexRequest = new IndexRequest("goods").id(goods.getId() + "").source(data, XContentType.JSON); // 执行增加文档 IndexResponse response = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT); DSL: PUT goods/_doc/3 { "id": 3, "brandName": "华为", "categoryName": "手机", "createTime": "2023-10-23 19:12:56", "price": 5999, "saleNum": 1001, "status": 1, "stock": 900, "title": "华为自研芯片Meta60测试机" } 5.3.2、更新文档 // 创建索引请求对象 UpdateRequest updateRequest = new UpdateRequest("goods", "4"); // 设置更新文档内容 updateRequest.doc(data, XContentType.JSON); // 执行更新文档 UpdateResponse response = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT); DSL: POST goods/_update/3 { "doc": { "title":"华为自研芯片Meta60测试机111" } } 5.3.3、删除文档 // 创建删除请求对象 DeleteRequest deleteRequest = new DeleteRequest("goods", "3"); // 执行删除文档 DeleteResponse response = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT); DSL: DELETE goods/_doc/3 5.3.4、Match匹配查询 // 构建查询条件 (查询全部) MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // 创建查询源构造器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); // 设置分页 searchSourceBuilder.from(0); // 从第几条开始,不包含它 searchSourceBuilder.size(3); // 要取多少条数据 // 设置排序 searchSourceBuilder.sort("price", SortOrder.ASC); // 设置源字段过虑,第一个参数结果集包括哪些字段,第二个参数表示结果集不包括哪些字段;查询的文档只包含哪些指定的字段 searchSourceBuilder.fetchSource(new String[]{"id", "title", "categoryName"}, new String[]{}); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 根据状态和数据条数验证是否返回了数据 if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) { SearchHits hits = searchResponse.getHits(); for (SearchHit hit : hits) { // 将 JSON 转换成对象 Goods goods = JSON.parseObject(hit.getSourceAsString(), Goods.class); // 输出查询信息 log.info(goods.toString()); } } DSL: POST goods/_search { "query": { "match_all": { } } } POST goods/_search { "query": { "match": { "title": "移动多余" } } } 5.3.5、MatchPhrase查询 短语匹配会把查询文本看做短语,有顺序要求。 // 构建查询条件 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // title是 舒适轻巧 时,可以查到,改成 轻巧舒适 时,则查不到,因为短语匹配,会把查询文本看做短语,有顺序要求 searchSourceBuilder.query(QueryBuilders.matchPhraseQuery("title", "轻巧舒适")); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 根据状态和数据条数验证是否返回了数据 if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) { SearchHits hits = searchResponse.getHits(); for (SearchHit hit : hits) { // 将 JSON 转换成对象 Goods goods = JSON.parseObject(hit.getSourceAsString(), Goods.class); // 输出查询信息 log.info(goods.toString()); } } DSL: POST goods/_search { "query": { "match_phrase": { "title": "轻巧舒适" } } } 5.3.6、Term查询 Term查询时不对查询文本做分词。 // 构建查询条件(注意:termQuery 支持多种格式查询,如 boolean、int、double、string 等,这里使用的是 string 的查询) SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 表示检索title字段值为 李宁的跑鞋 的文档,如果是match则可以查到,term查询不到,因为term查询时不对查询文本做分词 searchSourceBuilder.query(QueryBuilders.termQuery("title", "李宁的跑鞋")); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 根据状态和数据条数验证是否返回了数据 if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) { SearchHits hits = searchResponse.getHits(); for (SearchHit hit : hits) { // 将 JSON 转换成对象 Goods goods = JSON.parseObject(hit.getSourceAsString(), Goods.class); // 输出查询信息 log.info("=======" + goods.toString()); } } DSL: POST goods/_search { "query": { "term": { "title": { "value": "李宁的跑鞋" } } } } 5.3.7、设置排序 // 设置排序 searchSourceBuilder.sort("price", SortOrder.ASC); DSL: POST goods/_search { "query": { "match_all": { } }, "from": 6, "size": 2, "sort": [ { "price": { "order": "asc" } } ] } 5.3.8、通配符查询 // 构建查询条件 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); // 查询所有以 “鞋” 结尾的商品信息 searchSourceBuilder.query(QueryBuilders.wildcardQuery("title", "*鞋")); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 根据状态和数据条数验证是否返回了数据 if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) { SearchHits hits = searchResponse.getHits(); for (SearchHit hit : hits) { // 将 JSON 转换成对象 Goods goods = JSON.parseObject(hit.getSourceAsString(), Goods.class); // 输出查询信息 log.info(goods.toString()); } } DSL: POST goods/_search { "query": { "wildcard": { "title": { "value": "*鞋" } } } } 5.3.9、范围查询 // 构建查询条件 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.rangeQuery("price").gte(1000)); DSL: POST goods/_search { "query": { "bool": { "must": [ { "match": { "title": "华为" } } ], "filter": [ { "range": { "price": { "gte": 5000, "lte": 10000 } } } ] } } } 5.3.10、Scroll滚动查询 滚动查询可以用来解决深度分页查询问题,每次都要记住上一次的scrollId。 // 假设用户想获取第5页数据,其中每页1条 int pageNo = 3; int pageSize = 1; // 定义请求对象 SearchRequest searchRequest = new SearchRequest("goods"); // 构建查询条件 SearchSourceBuilder builder = new SearchSourceBuilder(); searchRequest.source(builder.query(QueryBuilders.matchAllQuery()).sort("price", SortOrder.DESC).size(pageSize)); String scrollId = null; // 3、发送请求到ES SearchResponse scrollResponse = null; // 设置游标id存活时间 Scroll scroll = new Scroll(TimeValue.timeValueMinutes(2)); // 记录所有游标id List<String> scrollIds = new ArrayList<>(); for (int i = 0; i < pageNo; i++) { try { // 首次检索 if (i == 0) { //记录游标id searchRequest.scroll(scroll); // 首次查询需要指定索引名称和查询条件 SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 下一次搜索要用到该游标id scrollId = response.getScrollId(); } // 非首次检索 else { // 不需要在使用其他条件,也不需要指定索引名称,只需要使用执行游标id存活时间和上次游标id即可,毕竟信息都在上次游标id里面呢 SearchScrollRequest searchScrollRequest = new SearchScrollRequest(scrollId); searchScrollRequest.scroll(scroll); scrollResponse = restHighLevelClient.scroll(searchScrollRequest, RequestOptions.DEFAULT); // 下一次搜索要用到该游标id scrollId = scrollResponse.getScrollId(); } // 记录所有游标id scrollIds.add(scrollId); } catch (Exception e) { e.printStackTrace(); } } // 查询完毕,清除游标id ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); clearScrollRequest.scrollIds(scrollIds); try { restHighLevelClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT); } catch (IOException e) { System.out.println("清除滚动查询游标id失败"); e.printStackTrace(); } // 4、处理响应结果 System.out.println("滚动查询返回数据:"); assert scrollResponse != null; SearchHits hits = scrollResponse.getHits(); for (SearchHit hit : hits) { // 将 JSON 转换成对象 Goods goods = JSON.parseObject(hit.getSourceAsString(), Goods.class); // 输出查询信息 log.info(goods.toString()); } DSL: # 第一次使用 scroll API POST goods/_search?scroll=2m { "query": { "match_all": {} }, "size": 2 } # 进行翻页 POST /_search/scroll { "scroll" : "2m", "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFkxBWkYwOGw2U1dPSF94aHZTelFkaWcAAAAAAAADHhZoU05ERFl3WFIycXM3M3JKMmRQVkJB" } 5.3.11、组合查询 组合查询一般用到bool。 // 创建 Bool 查询构建器 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // 构建查询条件 boolQueryBuilder .must(QueryBuilders.matchQuery("title", "李宁")) .filter() .add(QueryBuilders.rangeQuery("createTime").format("yyyyMMdd") .gte("20231023").lte("20231025")); // 构建查询源构建器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(boolQueryBuilder); searchSourceBuilder.size(100); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 根据状态和数据条数验证是否返回了数据 if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) { SearchHits hits = searchResponse.getHits(); for (SearchHit hit : hits) { // 将 JSON 转换成对象 Goods goods = JSON.parseObject(hit.getSourceAsString(), Goods.class); // 输出查询信息 log.info(goods.toString()); } } DSL: POST goods/_search { "query": { "bool": { "must": [ { "match": { "title": "李宁" } } ], "filter": [ { "range": { "price": { "gte": 5000, "lte": 10000 } } } ] } } } 5.3.12、高亮查询 //查询条件(词条查询:对应ES query里的match) MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("title", "华为手机"); //设置高亮三要素 // field: 你的高亮字段 // preTags :前缀 // postTags:后缀 HighlightBuilder highlightBuilder = new HighlightBuilder() .field("title") .preTags("<font color='blue'>") .postTags("</font>"); // 构建查询源构建器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchQueryBuilder); searchSourceBuilder.highlighter(highlightBuilder); searchSourceBuilder.size(100); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); // 根据状态和数据条数验证是否返回了数据 if (RestStatus.OK.equals(searchResponse.status()) && searchResponse.getHits().getTotalHits().value > 0) { SearchHits hits = searchResponse.getHits(); for (SearchHit hit : hits) { // 将 JSON 转换成对象 Goods goods = JSON.parseObject(hit.getSourceAsString(), Goods.class); // 获取高亮的数据 HighlightField highlightField = hit.getHighlightFields().get("title"); System.out.println("高亮名称:" + highlightField.getFragments()[0].string()); // 替换掉原来的数据 Text[] fragments = highlightField.getFragments(); if (fragments != null && fragments.length > 0) { StringBuilder title = new StringBuilder(); for (Text fragment : fragments) { //System.out.println(fragment); title.append(fragment); } goods.setTitle(title.toString()); } // 输出查询信息 log.info(goods.toString()); } } DSL: POST goods/_search { "query": { "match": { "title": "跑鞋" } }, "highlight": { "fields": { "body": { "pre_tags": [ "<font color='red'>" ], "post_tags": [ "</font>" ] }, "title": {} } } } 5.3.13、聚合查询 // 构建查询条件 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // 创建查询源构造器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); // 获取最贵的商品 AggregationBuilder maxPrice = AggregationBuilders.max("maxPrice").field("price"); searchSourceBuilder.aggregation(maxPrice); // 获取最便宜的商品 AggregationBuilder minPrice = AggregationBuilders.min("minPrice").field("price"); searchSourceBuilder.aggregation(minPrice); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); Aggregations aggregations = searchResponse.getAggregations(); ParsedMax max = aggregations.get("maxPrice"); log.info("最贵的价格:" + max.getValue()); ParsedMin min = aggregations.get("minPrice"); log.info("最便宜的价格:" + min.getValue()); DSL: POST goods/_search { "aggs": { "max_price": { "max": { "field": "price" } } } } 5.3.14、分组查询 // 构建查询条件 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // 创建查询源构造器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); // 根据商品分类进行分组查询 TermsAggregationBuilder aggBrandName = AggregationBuilders .terms("brandNameName") .field("brandName"); searchSourceBuilder.aggregation(aggBrandName); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); Aggregations aggregations = searchResponse.getAggregations(); ParsedStringTerms aggBrandName1 = aggregations.get("brandNameName"); for (Terms.Bucket bucket : aggBrandName1.getBuckets()) { // 分组名 ==== 数量 System.out.println(bucket.getKeyAsString() + "====" + bucket.getDocCount()); } DSL: POST goods/_search { "aggs": { "brandNameName": { "terms": { "field": "brandName" } } } } 5.3.15、子查询 // 构建查询条件 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // 创建查询源构造器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); // 根据商品分类进行分组查询,并且获取分类商品中的平均价格 TermsAggregationBuilder subAggregation = AggregationBuilders .terms("brandNameName") .field("brandName") .subAggregation(AggregationBuilders.avg("avgPrice").field("price")); searchSourceBuilder.aggregation(subAggregation); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); Aggregations aggregations = searchResponse.getAggregations(); ParsedStringTerms aggBrandName1 = aggregations.get("brandNameName"); for (Terms.Bucket bucket : aggBrandName1.getBuckets()) { // 获取聚合后的品牌的平均价格,注意返回值不是Aggregation对象,而是指定的ParsedAvg对象 ParsedAvg avgPrice = bucket.getAggregations().get("avgPrice"); // 分组名 ==== 平均价格 System.out.println(bucket.getKeyAsString() + "====" + avgPrice.getValueAsString()); } DSL: POST goods/_search { "aggs": { "brandNameName": { "terms": { "field": "brandName" }, "aggs": { "avgPrice": { "avg": { "field": "price" } } } } } } 5.3.16、子查询下的子查询 子查询下的子查询有点绕。 // 构建查询条件 MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // 创建查询源构造器 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(matchAllQueryBuilder); // 注意这里聚合写的位置不要写错,很容易搞混,错一个括号就不对了 TermsAggregationBuilder subAggregation = AggregationBuilders .terms("categoryNameAgg").field("categoryName") .subAggregation(AggregationBuilders.avg("categoryNameAvgPrice").field("price")) .subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName") .subAggregation(AggregationBuilders.avg("brandNameAvgPrice").field("price"))); searchSourceBuilder.aggregation(subAggregation); // 创建查询请求对象,将查询对象配置到其中 SearchRequest searchRequest = new SearchRequest("goods"); searchRequest.source(searchSourceBuilder); // 执行查询,然后处理响应结果 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); //获取总记录数 System.out.println("totalHits = " + searchResponse.getHits().getTotalHits().value); // 获取聚合信息 Aggregations aggregations = searchResponse.getAggregations(); ParsedStringTerms categoryNameAgg = aggregations.get("categoryNameAgg"); //获取值返回 for (Terms.Bucket bucket : categoryNameAgg.getBuckets()) { // 获取聚合后的分类名称 String categoryName = bucket.getKeyAsString(); // 获取聚合命中的文档数量 long docCount = bucket.getDocCount(); // 获取聚合后的分类的平均价格,注意返回值不是Aggregation对象,而是指定的ParsedAvg对象 ParsedAvg avgPrice = bucket.getAggregations().get("categoryNameAvgPrice"); System.out.println(categoryName + "======平均价:" + avgPrice.getValue() + "======数量:" + docCount); ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brandNameAgg"); for (Terms.Bucket brandeNameAggBucket : brandNameAgg.getBuckets()) { // 获取聚合后的品牌名称 String brandName = brandeNameAggBucket.getKeyAsString(); // 获取聚合后的品牌的平均价格,注意返回值不是Aggregation对象,而是指定的ParsedAvg对象 ParsedAvg brandNameAvgPrice = brandeNameAggBucket.getAggregations().get("brandNameAvgPrice"); System.out.println(" " + brandName + "======" + brandNameAvgPrice.getValue()); } } DSL: POST goods/_search { "aggs": { "categoryNameName": { "terms": { "field": "categoryName" }, "aggs": { "avgPrice": { "avg": { "field": "price" } }, "brandNameAgg": { "terms": { "field": "brandName" }, "aggs": { "brandNameAvg": { "avg": { "field": "price" } } } } } } } } RestClient API使用的参考文献:重学Elasticsearch第6章 : SpringBoot整合RestHighLevelClient_elasticsearch_white camel-华为云开发者联盟 6、总结 本文主要介绍了Springboot与Elasticsearch集成和使用,重点内容如下: 选择RestHighLevelClient作为ES的RestClient。 核心步骤:增加properties配置、引入pom、配置conf类。 调用API、使用DSL语法。 ———————————————— 原文链接:https://blog.csdn.net/baily_ycl/article/details/138240754
-
Spring Boot 提供了 RestTemplate 来辅助发起一个 REST 请求,默认通过 JDK 自带的 HttpURLConnection 来作为底层 HTTP 消息的发送方式,使用 JackSon 来序列化服务器返回的 JSON 数据。 RestTemplate 是核心类, 提供了所有访问 REST 服务的接口,尽管实际上可以使用 HTTP Client 类或者 java.net.URL来完成,但 RestTemplate 提供了阻STful 风格的 API。 Spring Boot 提供了 RestTemplateBuilder 来创建一个 RestTemplate。 RestTemplate定义11个基本操作方法,大致如下: delete(): 在特定的URL上对资源执行HTTP DELETE操作 exchange(): 在URL上执行特定的HTTP方法,返回包含对象的ResponseEntity,这个对象是从响应体中 映射得到的 3.execute(): 在URL上执行特定的HTTP方法,返回一个从响应体映射得到的对象(所有的get、post、delete、put、options、head、exchange方法最终调用的都是excute方法),例如: @Override public <T> T getForObject(String url, Class<T> responseType, Object... urlVariables) throws RestClientException { RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor<T> responseExtractor = <span style="white-space:pre"> </span>new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); } 4.getForEntity(): 发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象 5.getForObject() :发送一个HTTP GET请求,返回的请求体将映射为一个对象 6.postForEntity() :POST 数据到一个URL,返回包含一个对象的ResponseEntity,这个对象是从响应体中映射得 到的 7.postForObject(): POST 数据到一个URL,返回根据响应体匹配形成的对象 8.headForHeaders(): 发送HTTP HEAD请求,返回包含特定资源URL的HTTP头 9.optionsForAllow(): 发送HTTP OPTIONS请求,返回对特定URL的Allow头信息 10.postForLocation() :POST 数据到一个URL,返回新创建资源的URL 11.put(): PUT 资源到特定的URL 实际上,由于Post 操作的非幂等性,它几乎可以代替其他的CRUD操作. 一、GET请求 在RestTemplate中,发送一个GET请求,我们可以通过如下两种方式: 第一种:getForEntity getForEntity方法的返回值是一个ResponseEntity<T>,ResponseEntity<T>是Spring对HTTP请求响应的封装,包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。其重载方法如下: <T> ResponseEntity<T> getForObject(URI url, Class<T> responseType) throws RestClientException; <T> ResponseEntity<T> getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException; <T> ResponseEntity<T> getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException; 比如下面一个例子: @Autowired RestTemplateBuilder restTemplateBuilder; @RequestMapping("/gethello") public String getHello() { RestTemplate client = restTemplateBuilder.build (); ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/hello", String.class); String body = responseEntity.getBody(); HttpStatus statusCode = responseEntity.getStatusCode(); int statusCodeValue = responseEntity.getStatusCodeValue(); HttpHeaders headers = responseEntity.getHeaders(); StringBuffer result = new StringBuffer(); result.append("responseEntity.getBody():").append(body).append("<hr>") .append("responseEntity.getStatusCode():").append(statusCode).append("<hr>") .append("responseEntity.getStatusCodeValue():").append(statusCodeValue).append("<hr>") .append("responseEntity.getHeaders():").append(headers).append("<hr>"); return result.toString(); } 有时候我在调用服务提供者提供的接口时,可能需要传递参数,有两种不同的方式,如下: @RequestMapping("/sayhello") public String sayHello() { ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/sayhello?name={1}", String.class, "张三"); return responseEntity.getBody(); } @RequestMapping("/sayhello2") public String sayHello2() { Map<String, String> map = new HashMap<>(); map.put("name", "李四"); ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/sayhello?name={name}", String.class, map); return responseEntity.getBody(); } 可以用一个数字做占位符,最后是一个可变长度的参数,来一一替换前面的占位符 也可以前面使用name={name}这种形式,最后一个参数是一个map,map的key即为前边占位符的名字,map的value为参数值 第一个调用地址也可以是一个URI而不是字符串,这个时候我们构建一个URI即可,参数神马的都包含在URI中了,如下: @RequestMapping("/sayhello3") public String sayHello3() { UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://HELLO-SERVICE/sayhello?name={name}").build().expand("王五").encode(); URI uri = uriComponents.toUri(); ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class); return responseEntity.getBody(); } 通过Spring中提供的UriComponents来构建Uri即可。 当然,服务提供者不仅可以返回String,也可以返回一个自定义类型的对象,比如我的服务提供者中有如下方法: @RequestMapping(value = "/getbook1", method = RequestMethod.GET) public Book book1() { return new Book("三国演义", 90, "罗贯中", "花城出版社"); } 对于该方法我可以在服务消费者中通过如下方式来调用: @RequestMapping("/book1") public Book book1() { ResponseEntity<Book> responseEntity = restTemplate.getForEntity("http://HELLO- SERVICE/getbook1", Book.class); return responseEntity.getBody(); } 第二种:getForObject getForObject函数实际上是对getForEntity函数的进一步封装,如果你只关注返回的消息体的内容,对其他信息都不关注,此时可以使用getForObject,重载方法如下: <T> T getForObject(URI url, Class<T> responseType) throws RestClientException; <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException; <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException; 二、POST请求 在RestTemplate中,POST请求可以通过如下三个方法来发起: 第一种:postForEntity、postForObject POST请求有postForObject()和postForEntity()两种方法,和GET请求的getForObject()和getForEntity()方法类似。getForLocation()是POST请求所特有的。 <T> T postForObject(URI url, Object request, Class<T> responseType) throws RestClientException; <T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables) throws RestClientException; <T> T postForObject(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException; 上面三个方法中,第一个参数都是资源要POST到的URL,第二个参数是要发送的对象,而第三个参数是预期返回的Java类型。在URL作为String类型的两个版本中,第四个参数指定了URL变量(要么是可变参数列表,要么是一个Map)。 该方法和get请求中的getForEntity方法类似,如下例子: @RequestMapping("/book3") public Book book3() { Book book = new Book(); book.setName("红楼梦"); ResponseEntity<Book> responseEntity = restTemplate.postForEntity("http://HELLO-SERVICE/getbook2", book, Book.class); return responseEntity.getBody(); } 第二种:postForLacation postForLacation()会在POST请求的请求体中发送一个资源到服务器端,返回的不再是资源对象,而是创建资源的位置。 postForLocation(String url, Object request, Object... uriVariables) throws RestClientException; postForLocation(String url, Object request, Map<String, ?> uriVariables) throws RestClientException; postForLocation(URI url, Object request) throws RestClientException; public String postSpitter(Spitter spitter) { RestTemplate rest = new RestTemplate(); return rest.postForLocation("http://localhost:8080/Spitter/spitters", spitter).toString(); } postForLocation也是提交新资源,提交成功之后,返回新资源的URI,postForLocation的参数和前面两种的参数基本一致,只不过该方法的返回值为Uri,这个只需要服务提供者返回一个Uri即可,该Uri表示新资源的位置。 三、PUT请求 在RestTemplate中,对PUT请求可以通过put方法进行调用实现,比如: RestTemplate restTemplate=new RestTemplate(); Longid=100011; User user=new User("didi",40); restTemplate.put("http://USER-SERVICE/user/{l}",user,id); put函数也实现了三种不同的重载方法: put(String url,Object request,Object... urlVariables) put(String url,Object request,Map urlVariables) put(URI url,Object request) put函数为void类型,所以没有返回内容,也就没有其他函数定义的responseType参数,除此之外的其他传入参数定义与用法与postForObject基本一致。 四、DELETE请求 在RestTemplate中,对DELETE请求可以通过delete方法进行调用实现,比如: RestTemplate restTemplate=new RestTemplate(); Longid=10001L; restTemplate.delete("http://USER-SERVICE/user/{1)",id); delete函数也实现了三种不同的重载方法: delete(String url,Object... urlVariables) delete(String url,Map urlVariables) delete(URI url) 由于我们在进行REST请求时,通常都将DELETE请求的唯一标识拼接在url中,所以DELETE请求也不需要request的body信息,就如put()方法一样,返回值类型为void。 说明:第三种重载方法,url指定DELETE请求的位置,urlVariables绑定url中的参数即可。 五、通用方法exchange() exchange方法可以在发送个服务器端的请求中设置头信息,其重载方法如下: <T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType) throws RestClientException; <T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws RestClientException; <T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException; MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>(); headers.add("Accept", "application/json"); HttpEntity<Object> requestEntity = new HttpEntity<Object>(headers); ResponseEntity<Spitter> response=rest.exchange("http://localhost:8080/Spitter/spitters/ {spitter}", HttpMethod.GET, requestEntity, Spitter.class, spitterId); 补充: 如果期望返回的类型是一个列表,如 List,不能简单调用 xxxForObject,因为存在泛型的类型擦除, RestTemplate 在反序列化的时候并不知道实际反序列化的类型,因此可以使用 ParameterizedTypeReference 来包含泛型类型,代码如下: RestTemplate client= restTemplateBuilder.build(); //根据条件查询一纽订单 String uri = base+"/orders?offset={offset }"; Integer offset = 1; //元参数 HttpEntity body = null; ParameterizedTypeReference<List<Order> typeRef = new ParameterizedTypeReference<List<Order>(){}; ResponseEntity<List<Order> rs=client.exchange(uri, HttpMethod.GET, body, typeRef, offset); List<Order> order = rs.getBody() ; 注意到 typeRef 定义是用{}结束的,这里创建了一个 ParameterizedTypeReference 子类,依 据在类定义中的泛型信息保留的原则, typeRef保留了期望返回的泛型 List。 exchange 是一个基础的 REST 调用接口,除了需要指明 HTTP Method,调用方法同其他方法类似。 除了使用ParameterizedTypeReference子类对象,也可以先将返回结果映射成 json字符串,然后通过 ObjectMapper 来转为指定类型(笔记<SpringBoot中的JSON>中有介绍 ) ———————————————— 原文链接:https://blog.csdn.net/fsy9595887/article/details/86420048
-
1、GC触发的条件 Java中,GC的触发主要有两种方式: 显式触发:通过程序调用System.gc()或Runtime.getRuntime().gc()方法,向JVM发出建议进行垃圾回收的请求。但请注意,这仅仅是建议,JVM可以忽略这个请求。 隐式触发:由JVM根据内部算法和内存使用情况自动决定。当堆内存中的对象空间不足以满足新对象分配时,JVM会自动触发GC以尝试回收内存。 2、GCRoots的对象类型 在Java中,GC Roots是对象图遍历的起始点,它们是在垃圾回收过程中,被JVM视为存活的对象。GC Roots主要包括以下几种: 虚拟机栈中引用的对象:包括局部变量和参数等。 方法区中类静态属性引用的对象:类的静态变量引用的对象。 方法区中常量引用的对象:如字符串常量池中的对象。 本地方法栈中JNI(Java Native Interface)引用的对象:由JNI调用本地方法时,本地方法栈中引用的对象。 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/qq_41840843/article/details/140333907
-
static关键字 1修饰属性 Java的静态属性和类相关, 和具体的实例无关. 换句话说, 同一个类的不同实例共用同一个静态属性. 举例 class TestDemo{ public int a; public static int count; } public class Main{ public static void main(String[] args) { TestDemo t1 = new TestDemo(); t1.a++; TestDemo.count++; System.out.println(t1.a); System.out.println(TestDemo.count); System.out.println("============"); TestDemo t2 = new TestDemo(); t2.a++; TestDemo.count++; System.out.println(t2.a); System.out.println(TestDemo.count); } } 上面的代码输出结果是: 1 1 ============ 为什么呢?-----count成员属性被static修饰后,属于类不属于具体的对象,而a属于具体的对象。 2修饰方法 如果在任何方法上应用 static 关键字,此方法称为静态方法。 静态方法属于类,而不属于类的对象。 可以直接调用静态方法,而无需创建类的实例。 静态方法可以访问静态数据成员,并可以更改静态数据成员的值。 静态方法不可以访问非静态数据成员。 举例 class TestDemo{ public int a; public static int count; public static void change() { count = 100; //a = 10; error 不可以访问非静态数据成员 } } public class Main{ public static void main(String[] args) { TestDemo.change();//无需创建实例对象 就可以调用 System.out.println(TestDemo.count); } } final 被final修饰的成员属性叫做常量,属于对象,被final修饰后,后续不可被修改。 被final和static同时修饰的成员属性叫做静态常量,属于类本身,只有一份,后续不可被修改。 构造方法 基本语法 构造方法是一种特殊方法, 使用关键字new实例化新对象时会被自动调用, 用于完成初始化操作. new 执行过程 为对象分配内存空间 调用对象的构造方法 语法规则 1.方法名称必须与类名称相同 2.构造方法没有返回值类型声明 3.每一个类中一定至少存在一个构造方法(没有明确定义,则系统自动生成一个无参构造)。 注意事项 如果类中没有提供任何的构造函数,那么编译器会默认生成一个不带有参数的构造函数 若类中定义了构造方法,则默认的无参构造将不再生成. 构造方法支持重载. 规则和普通方法的重载一致 . this关键字 this表示当前对象引用(注意不是当前对象). 可以借助 this 来访问对象的字段和方法。 可以使用this调用构造函数,但是要放在第一行!!! 举例 class Person { private String name;//实例成员变量 private int age; private String sex; //默认构造函数 构造对象 public Person() { //this调用构造函数 this("bit", 12, "man");//必须放在第一行进行显示 } //这两个构造函数之间的关系为重载。 public Person(String name,int age,String sex) { this.name = name; this.age = age; this.sex = sex; } public void show() { System.out.println("name: "+name+" age: "+age+" sex: "+sex); } } public class Main{ public static void main(String[] args) { Person person = new Person();//调用不带参数的构造函数 person.show(); } } // 输出 结果 name: bit age: 12 sex: man 代码块 字段的初始化方式有: 1. 就地初始化 2. 使用构造方法初始化 3.使用代码块初始化 下面将讲解使用第3种方式的初始化。 定义 使用 {} 定义的一段代码. 根据代码块定义的位置以及关键字,又可分为以下四种: 普通代码块 构造块 静态块 同步代码块 普通代码块 定义在方法中的代码块。 举例 public class Main{ public static void main(String[] args) { { //直接使用{}定义,普通方法块 int x = 10 ; System.out.println("x1 = " +x); } int x = 100 ; System.out.println("x2 = " +x); } } // 执行结果 x1 = 10 x2 = 100 构造代码块 定义在类中的代码块(不加修饰符),也称实例代码块。 构造代码块一般用于初始化实例成员变量。 举例 class Person{ private String name;//实例成员变量 private int age; private String sex; public Person() { System.out.println("I am Person init()!"); } //实例代码块 { this.name = "bit"; this.age = 12; this.sex = "man"; System.out.println("I am instance init()!"); } public void show(){ System.out.println("name: "+name+" age: "+age+" sex: "+sex); } } public class Main { public static void main(String[] args) { Person p1 = new Person(); p1.show(); } } 运行结果: I am instance init()! I am Person init()! name: bit age: 12 sex: man 静态代码块 使用static定义的代码块。一般用于初始化静态成员属性。 静态代码块不管生成多少个对象,其只会执行一次,且是最先执行的。 静态代码块执行完毕后, 实例代码块(构造块)执行,再然后是构造函数执行。 举例 class Person{ private String name;//实例成员变量 private int age; private String sex; private static int count = 0;//静态成员变量 由类共享数据 方法区 public Person(){ System.out.println("I am Person init()!"); } //实例代码块 { this.name = "bit"; this.age = 12; this.sex = "man"; System.out.println("I am instance init()!"); } //静态代码块 static { count = 10;//只能访问静态数据成员 System.out.println("I am static init()!"); } public void show(){ System.out.println("name: "+name+" age: "+age+" sex: "+sex); } } public class Main { public static void main(String[] args) { Person p1 = new Person(); Person p2 = new Person();//静态代码块是否还会被执行? } } 匿名对象 匿名只是表示没有名字的对象. 没有引用的对象称为匿名对象. 匿名对象只能在创建对象时使用. 如果一个对象只是用一次, 后面不需要用了, 可以考虑使用匿名对象. 举例 class Person { private String name; private int age; public Person(String name,int age) { this.age = age; this.name = name; } public void show() { System.out.println("name:"+name+" " + "age:"+age); } } public class Main { public static void main(String[] args) { new Person("caocao",19).show();//通过匿名对象调用方法 } } toString class Person { private String name; private int age; public Person(String name,int age) { this.age = age; this.name = name; } public void show() { System.out.println("name:"+name+" " + "age:"+age); } } public class Test { public static void main(String[] args) { Person person = new Person("caocao",19); person.show(); System.out.println(person); } } 为什么第二行打印结果是这样的呢? 为了更好的打印,需要重写Object的toString方法,即: class Person { private String name; private int age; public Person(String name,int age) { this.age = age; this.name = name; } public void show() { System.out.println("name:"+name+" " + "age:"+age); } //重写Object的toString方法 @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class Test { public static void main(String[] args) { Person person = new Person("caocao",19); person.show(); System.out.println(person); } } 总结 一个类可以产生无数的对象,类就是模板,对象就是具体的实例。 类中定义的属性,大概分为几类:类属性,对象属性。其中被static所修饰的数据属性称为类属性, static修饰的方法称为类方法,特点是不依赖于对象,我们只需要通过类名就可以调用其属性或者方法。 静态代码块优先实例代码块执行,实例代码块优先构造函数执行。 this关键字代表的是当前对象的引用。并不是当前对象。 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/wmh_1234567/article/details/140722526
-
大家好,本次7月合计带来的内容包括 .Sql Server,mysql,java,python,nginx多个内容供您查找学习,感谢1.Sql Server查询卡顿的排查方法【转】 https://bbs.huaweicloud.com/forum/thread-02113157814182541065-1-1.html 2.mysql WITH RECURSIVE语法的具体使用【转】 https://bbs.huaweicloud.com/forum/thread-02127157734079592051-1-1.html 3.MySQL文件权限存在的安全问题和解决方案【转】 https://bbs.huaweicloud.com/forum/thread-02127157814439433059-1-1.html 4.MySQL的表约束的具体使用【转】 https://bbs.huaweicloud.com/forum/thread-02119157814488009063-1-1.html 5.MySQL建表语句基础及示例详解【转】 https://bbs.huaweicloud.com/forum/thread-02113157815352821066-1-1.html 6.MySQL分表和分区分表的区别小结【转】 https://bbs.huaweicloud.com/forum/thread-02119157815516348065-1-1.html 7.Redission实现分布式锁lock()和tryLock()方法的区别小结【转】 https://bbs.huaweicloud.com/forum/thread-02119157816143144066-1-1.html 8.无法启动Redis打开redis-server闪退的问题解决办法【转】 https://bbs.huaweicloud.com/forum/thread-0220157816373247058-1-1.html 9.Nginx出现404 Not Found nginx/1.23.4的完美解决方案【转】 https://bbs.huaweicloud.com/forum/thread-0220157816562623059-1-1.html 10.Nginx上传文件出现“ 413 (499 502 404) Request Entity Too Large错误解决【转】 https://bbs.huaweicloud.com/forum/thread-02127157817937530061-1-1.html 11.Nginx代理MySQL实现通过域名连接数据库的详细教程【转】 https://bbs.huaweicloud.com/forum/thread-02127157818132517062-1-1.html 12.Nginx实现灰度发布的常见方法小结【转】 https://bbs.huaweicloud.com/forum/thread-02127157818398915063-1-1.html 13.Vscode中launch.json与tasks.json文件的详细介绍【转】 https://bbs.huaweicloud.com/forum/thread-02127157818683109058-1-1.html 14.Python中网络请求的12种方式【转】 https://bbs.huaweicloud.com/forum/thread-02113157818977531068-1-1.html 15. pip install过程中出现error: subprocess-exited-with-error错误的解决办法【转】 https://bbs.huaweicloud.com/forum/thread-0286157819160557055-1-1.html 16.在Python代码中执行Linux命令的详细用法教程【转】 https://bbs.huaweicloud.com/forum/thread-02127157819336403064-1-1.html 17.ava 集合去重的三种方法【转】 https://bbs.huaweicloud.com/forum/thread-0220157820152566061-1-1.html
-
在Java中,如果你有一个集合(如List、Set等),其中包含自定义对象,并且你希望根据某个特定属性去重,只保留每个具有唯一属性值的对象的一个实例,你可以使用以下几种方法:方法1:使用HashSet如果对象的类实现了equals()和hashCode()方法,并且这两个方法是基于你想去重的那个属性来实现的,那么可以直接将列表转换为HashSet以达到去重的目的。123List<MyObject> list = ...; // 假设这是你的原始列表Set<MyObject> uniqueSet = new HashSet<>(list);List<MyObject> uniqueList = new ArrayList<>(uniqueSet);方法2:Stream API (Java 8及以上版本)通过Java 8引入的Stream API可以更方便地处理这种情况,尤其是当你想基于对象的某个属性进行去重时:1234567891011List<MyObject> list = ...;List<MyObject> uniqueList = list.stream() .collect(Collectors.collectingAndThen( Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(MyObject::getProperty))), ArrayList::new ));// 或者简化版本,如果MyObject直接或间接实现了Comparable接口:List<MyObject> uniqueList = list.stream() .distinct() .collect(Collectors.toList());上述代码中,我们首先将列表转换为流,然后使用Collectors.toCollection()收集到一个TreeSet中。TreeSet会自动根据传入的比较器对元素排序并去除重复项。这里假设getProperty()是获取对象属性的方法。最后将TreeSet转换回ArrayList。方法3:手动遍历并使用Map如果你想根据对象的某个属性保持第一个出现的对象,可以使用toMap方法:12345678List<MyObject> list = ...;Map<String, MyObject> map = list.stream() .collect(Collectors.toMap( MyObject::getProperty, // key extractor function Function.identity(), // value mapping function (o1, o2) -> o1 // merge function - 如果有冲突则保留第一个对象 ));List<MyObject> uniqueList = new ArrayList<>(map.values());这个方法会根据对象的属性值作为键存入Map中,由于Map不允许键重复,所以结果自然就是唯一的。通过merge函数指定当遇到相同键时保留第一个对象。请注意,这些示例假定MyObject是一个代表具体业务实体的类,而getProperty()是返回该类中用于判断是否重复的属性值的方法。根据实际情况调整类名和方法名。
-
在Java中,使用 poi-tl 插件可以非常方便地生成和操作Word文档,并且能够动态填充数据。要根据导出数据的类别数量动态显示两列或三列,可以按照以下步骤进行:1. 导入依赖首先,在您的项目中添加 poi-tl 和 poi 的依赖。以Maven为例,您需要在 pom.xml 文件中添加以下内容: <dependency><groupId>com.deepoove</groupId><artifactId>poi-tl</artifactId><version>1.9.0</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.1.2</version></dependency>2. 创建模板创建一个 Word 模板(例如 template.docx),其中包含占位符。在这里,我们假设有一个表格区域用来放置动态列的数据。示例模板(template.docx){{table}}3. Java 代码实现编写Java代码,根据数据的类别动态生成两列或三列的表格,并将数据填充到Word文档中。动态填充数据的Java示例 import com.deepoove.poi.XWPFTemplate;import com.deepoove.poi.data.RowRenderData;import com.deepoove.poi.data.TableRenderData;import java.io.FileOutputStream;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;public class DynamicWordExport {public static void main(String[] args) throws Exception {// 假设这是要填充的数据List<Map<String, String>> data = new ArrayList<>();data.add(new HashMap<String, String>() {{put("Name", "John");put("Age", "30");put("Gender", "Male");}});data.add(new HashMap<String, String>() {{put("Name", "Jane");put("Age", "28");put("Gender", "Female");}});// 根据数据动态生成 TableRenderDataTableRenderData tableData = generateTableData(data);// 填充模板Map<String, Object> templateData = new HashMap<>();templateData.put("table", tableData);XWPFTemplate template = XWPFTemplate.compile("template.docx").render(templateData);// 输出到文件FileOutputStream out = new FileOutputStream("output.docx");template.write(out);out.close();template.close();}private static TableRenderData generateTableData(List<Map<String, String>> data) {List<RowRenderData> rows = new ArrayList<>();// 表头行List<String> headers = new ArrayList<>(data.get(0).keySet());RowRenderData headerRow = RowRenderData.build(headers.toArray(new String[0]));rows.add(headerRow);// 数据行for (Map<String, String> entry : data) {List<String> rowValues = new ArrayList<>();for (String key : headers) {rowValues.add(entry.get(key));}RowRenderData row = RowRenderData.build(rowValues.toArray(new String[0]));rows.add(row);}return new TableRenderData(rows);}}4. 运行程序执行上述 Java 程序,将会生成一个包含动态数据的 Word 文档 output.docx。5. 根据列数调整表格布局如果要根据数据类别的多少动态显示两列或三列,可以在 generateTableData 方法中根据 headers 的数量进行条件判断: private static TableRenderData generateTableData(List<Map<String, String>> data) {List<RowRenderData> rows = new ArrayList<>();// 表头行List<String> headers = new ArrayList<>(data.get(0).keySet());RowRenderData headerRow = RowRenderData.build(headers.toArray(new String[0]));rows.add(headerRow);// 数据行for (Map<String, String> entry : data) {List<String> rowValues = new ArrayList<>();for (String key : headers) {rowValues.add(entry.get(key));}RowRenderData row = RowRenderData.build(rowValues.toArray(new String[0]));// 根据列数调整表格布局if (headers.size() == 2) {row.setCellWidth(Arrays.asList("50%", "50%"));} else if (headers.size() == 3) {row.setCellWidth(Arrays.asList("33%", "33%", "33%"));}rows.add(row);}return new TableRenderData(rows);}通过这种方法,可以根据数据的类别数量动态调整 Word 文档中的表格列数和布局。
-
介绍七夕魔方照片墙是一款创意应用,旨在让用户通过一个3D魔方展示他们的照片。每个面都可以显示不同的图片,用户可以旋转和翻转魔方来浏览所有的照片。这种方式不仅仅是简单地展示照片,而是将视觉效果与互动体验结合起来,使得照片展示更加生动有趣。应用使用场景个人相册展示: 用户可以将自己的旅游照片、家庭照片等按日期或主题放在魔方上展示。企业产品宣传: 企业可以将产品图片制作成魔方,用于展会或者网站上进行互动展示。艺术作品展示: 艺术家可以将自己的作品集成在魔方上,观众可以通过旋转魔方查看每一个细节。教育培训: 可以用于教学内容的展示,比如展示几何图形的各个面。原理解释七夕魔方照片墙基于三维图形渲染技术,将多个二维图像拼接在一个立方体的六个面上。通过用户的交互(如鼠标拖拽、触摸屏滑动),实现立方体的旋转变化,从而展现不同的面上的照片。算法原理流程图及解释+----------------+| Load Images |+--------+-------+ | v+--------+----------+| Initialize Cube || - Create 3D model || - Map images |+--------+----------+ | v+--------+-----------+| Render Cube || - Set up camera || - Apply textures |+--------+-----------+ | v+--------+-----------+| User Interaction || - Detect input || - Update rotation |+--------+-----------+ | v+--------+-----------+| Update Display || - Re-render cube |+--------------------+加载图片:从本地或远程资源加载用户选择的图片。初始化魔方:创建一个3D模型,并将图片映射到魔方的六个面上。渲染魔方:设置摄像机视角,将已贴好图像的3D魔方渲染到屏幕上。用户交互:检测用户输入(如鼠标拖拽、手指滑动等),并根据输入更新魔方的旋转状态。更新显示:重新渲染魔方,以反映最新的旋转状态。应用场景代码示例实现以下是一个利用 Java 和 JavaFX 实现七夕魔方照片墙的简单示例:import javafx.application.Application;import javafx.scene.*;import javafx.scene.image.Image;import javafx.scene.paint.PhongMaterial;import javafx.scene.shape.Box;import javafx.stage.Stage;import javafx.scene.transform.Rotate;public class PhotoCube extends Application { private static final double CUBE_SIZE = 200.0; private static final String[] IMAGES = { "image1.jpg", "image2.jpg", "image3.jpg", "image4.jpg", "image5.jpg", "image6.jpg" }; @Override public void start(Stage primaryStage) throws Exception { Group root = new Group(); Scene scene = new Scene(root, 800, 600, true); PerspectiveCamera camera = new PerspectiveCamera(true); camera.setTranslateZ(-500); Box cube = createPhotoCube(); root.getChildren().add(cube); scene.setOnMouseDragged(event -> { cube.getTransforms().add(new Rotate(event.getSceneX(), Rotate.Y_AXIS)); cube.getTransforms().add(new Rotate(event.getSceneY(), Rotate.X_AXIS)); }); primaryStage.setTitle("七夕魔方照片墙"); primaryStage.setScene(scene); primaryStage.show(); } private Box createPhotoCube() { Box box = new Box(CUBE_SIZE, CUBE_SIZE, CUBE_SIZE); PhongMaterial material = new PhongMaterial(); material.setDiffuseMap(new Image(IMAGES[0])); box.setMaterial(material); return box; } public static void main(String[] args) { launch(args); }}部署测试场景开发环境: 安装 JDK 和 JavaFX SDK。确保你的开发环境可以编译和运行 JavaFX 项目。构建项目: 使用 IDE(如 IntelliJ IDEA 或 Eclipse)创建并构建该项目。运行测试: 在本地运行应用程序,测试魔方的旋转和图片展示功能。材料链接JavaFX 官方文档JavaFX 示例项目Java 图形编程入门教程总结七夕魔方照片墙通过3D图形渲染和用户交互,为照片展示提供了一种创新和有趣的方式。它不仅适用于个人照片展示,还可以用于企业宣传、教育培训等多个领域。通过详细的算法解释和代码实现,我们了解了其工作原理和实现方法。未来展望未来,可以考虑加入更多的交互功能,例如:动画效果:增加自动旋转和缩放的动画效果,使展示更具动态性。多媒体支持:除了图片外,还可以支持视频和音频展示,增加内容的丰富性。云端存储和分享:集成云存储服务,让用户可以方便地上传和分享自己的魔方照片墙。这些改进将进一步提升用户体验和应用的实用性。
-
在开发过程中,我们可能会遇到需要生成word,或者通过模板word替换相应内容的需求。但在文档中插入图片时,如果段落格式设置不对,就会导致图片只显示一点点或者不显示。接下来就介绍一下java编辑word和插入图片需怎么处理。1.引入依赖首先我们在项目中引入Apache POI,用于读取和操作word,这里我使用的版本是4.1.2,版本可以根据项目需求自己选择。12345<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version></dependency> 2.编辑word这里是通过模板加入占位符,然后替换占位符的内容首先我们打开word模板文件12345678910String path = "***.docx";File file = new File(path);try { XWPFDocument template = new XWPFDocument(new FileInputStream(file)); // 替换内容 XWPFDocument outWord = PoiWordUtil.replaceWithPlaceholder(template, list); return outWord;} catch (IOException e) { log.error("读取模板文件失败", e);}替换相应内容1// 这里我定义了Placeholder来封装替换数据public static XWPFDocument replaceTextAndImage(XWPFDocument document, List<Placeholder> list) { for (XWPFParagraph xwpfParagraph : document.getParagraphs()) { String paragraphText = xwpfParagraph.getText(<em id="__mceDel"> if (StringUtils.isEmpty(paragraphText)) contin</em><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"> for (Placeholder placeholder : list) {</em></em></em></em></em></em><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"> String key = placeholder.getKey();</em></em></em></em></em></em></em><em><em><em><em><em><em><em> if (paragraphText.contains(key)) {<br> for (XWPFRun cellRun : xwpfParagraph.getRuns()) {<br> String text = cellRun.getText(0);<br> if (text != null && text.contains(key)) {<br> //获取占位符类型<br> String type = placeholder.getType();<br> //获取对应key的value<br> String value = placeholder.getValue();<br> if("0".equals(type)){<br> //把文本的内容,key替换为value<br> text = text.replace(key, value);<br> //把替换好的文本内容,保存到当前这个文本对象<br> cellRun.setText(text, 0);<br> }else {<br> text = text.replace(key, "");<br> cellRun.setText(text, 0);<br> if (StringUtils.isEmpty(value)) </em></em></em></em></em></em></em><em id="__mceDel"><em id="__mceDel">continue;</em></em><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"><em id="__mceDel"> try {<br> // 获取段落行距模式<br> int rule = xwpfParagraph.getSpacingLineRule().getValue();<br> // 如果段落行距为固定值,会导致图片显示不全,所以需要改成其他模式<br> if (LineSpacingRule.EXACT.getValue() == rule) {<br> // 设置段落行距为单倍行距<br> xwpfParagraph.setSpacingBetween(1);<br> }<br> // 获取文件流<br> InputStream imageStream = ImageUtils.getFile(value);</em></em></em></em></em></em></em><em id="__mceDel"> if (imageStream == null) continue;<br> // 通过BufferedImage获取图片信息<br> BufferedImage bufferedImage = ImageIO.read(imageStream);<br> int height = bufferedImage.getHeight();<br> int width = bufferedImage.getWidth();<br> // 这里需要重新获取流,之前的流已经被BufferedImage使用掉了<br> cellRun.addPicture(ImageUtils.getFile(value), XWPFDocument.PICTURE_TYPE_JPEG, "", Units.toEMU(width), Units.toEMU(height));</em> } catch (Exception e) {<br> e.printStackTrace();<br> }<br> }<br> }<br> }<br> }<br> }<br> }<br> return document;<br>}在插入图片时,如果段落的行距设置成了固定值,那么在显示图片时只能显示行距大小的部分,所以当插入图片的段落行距为固定值时,我们需要修改为其他模式,这样图片就能正常大小显示。然后我们使用cellRun.addPicture()来插入图片,这里我们可以通过BufferedImage来获取图片的尺寸大小。这样就解决了插入图片显示异常的问题了。转载自https://www.cnblogs.com/qq545505061/p/18302543
-
原子性的意义原子性特别是在并发编程领域,是一个极其重要的概念,原子性指的是一个操作或一组操作要么全部执行成功,要么全部不执行,不会出现部分执行的情况。这意味着原子性操作是不可分割的,它们在执行过程中不会被其他操作中断或干扰。原子性的意义在于它保证了数据的一致性和程序的正确性。在多线程或多进程的环境中,当多个操作同时访问和修改共享数据时,如果没有原子性保证,可能会导致数据不一致或不确定的结果。例如,如果一个线程在读取某个数据时,另一个线程同时修改了这个数据,那么第一个线程读取到的数据可能是不正确的。通过确保操作的原子性,可以避免这种情况,从而维护数据的完整性和程序的正确执行。了解了上面的原子性的重要概念后,接下来一起聊一聊 volatile 关键字。volatile 关键字在 Java 中用于确保变量的更新对所有线程都是可见的,但它并不保证复合操作的原子性。这意味着当多个线程同时访问一个 volatile 变量时,可能会遇到读取不一致的问题,尽管它们不会看到部分更新的值。Volatile 的限制不保证原子性:volatile 变量的单个读写操作是原子的,但复合操作(如自增或同步块)不是原子的。不保证顺序性:volatile 变量的读写操作不会与其他操作(如非 volatile 变量的读写)发生重排序。一个例子用一个示例来解释会更清楚点,假如我们有一段代码是这样的:class Counter { private volatile int count = 0; void increment() { count++; } int getCount() { return count; } } 尽管 count 是 volatile 变量,但 increment 方法中的复合操作 count++(读取-增加-写入)不是原子的。因此,在多线程环境中,多个线程可能会同时读取相同的初始值,然后增加它,导致最终值低于预期。volatile 不保证原子性的代码验证以下是一个简单的 Java 程序,演示了 volatile 变量在多线程环境中不保证复合操作原子性的问题:public class VolatileTest { private static volatile int counter = 0; public static void main(String[] args) throws InterruptedException { int numberOfThreads = 10000; Thread[] threads = new Thread[numberOfThreads]; for (int i = 0; i < numberOfThreads; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 100; j++) { counter++; } }); threads[i].start(); } for (int i = 0; i < numberOfThreads; i++) { threads[i].join(); } System.out.println("Expected count: " + (numberOfThreads * 100)); System.out.println("Actual count: " + counter); } }在这个例子中:counter 是一个 volatile 变量。每个线程都会对 counter 执行 100 次自增操作。理论上,如果 counter++ 是原子的,最终的 counter 值应该是 10000 * 100。然而,由于 counter++ 包含三个操作:读取 counter 的值、增加 1、写回 counter 的值,这些操作不是原子的。因此,在多线程环境中,最终的 counter 值通常会小于预期值,这证明了 volatile 变量不保证复合操作的原子性。解决方案1. 使用 synchronized 方法或块:将访问 volatile 变量的方法或代码块声明为 synchronized,确保原子性和可见性。class Counter { private volatile int count = 0; synchronized void increment() { count++; } synchronized int getCount() { return count; } }2. 使用 AtomicInteger 类:java.util.concurrent.atomic 包中的 AtomicInteger 提供了原子操作,可以替代 volatile 变量。import java.util.concurrent.atomic.AtomicInteger; class Counter { private AtomicInteger count = new AtomicInteger(0); void increment() { count.incrementAndGet(); } int getCount() { return count.get(); } }3. 使用锁(如 ReentrantLock):使用显式锁(如 ReentrantLock)来同步访问 volatile 变量的代码块。import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Counter { private volatile int count = 0; private final Lock lock = new ReentrantLock(); void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } } }使用volatile变量的正确使用场景如果操作是简单的读写,并且你只需要保证可见性,可以使用 volatile。但对于复合操作,可以使用上述其他方法来实现,通过这些方法,可以确保在多线程环境中对共享资源的正确同步和可见性。转载自https://www.cnblogs.com/wgjava/p/18311697
-
介绍在Spring Boot开发的动态世界中,确保数据完整性和跟踪变化是至关重要的。实现这一目标的一个强大工具是@Audited注解。本文深入探讨了该注解的复杂性、其目的、实现步骤以及如何利用其功能进行有效的实体审计。理解@AuditedSpring Boot中的@Audited注解用于审计实体,提供对数据随时间变化的详细记录。这在需要跟踪修改、用户操作或合规要求的情况下非常有价值。实现步骤1. 依赖项:要包含@Audited,需要在项目中添加spring-data-envers依赖。确保你的pom.xml或build.gradle反映这一添加。<!-- Maven Dependency --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> </dependency>spring-boot-starter-data-jpa依赖包含了使用Spring Data JPA进行数据访问所需的组件。然而,如果你特别想在Spring Boot中使用@Audited注解启用实体审计,还需要包含hibernate-envers依赖。该依赖添加了对Hibernate Envers的支持,这是负责实体版本控制和审计的工具。2. 实体配置:将@Audited注解应用于你想要审计的实体类。import org.hibernate.envers.Audited; @Entity @Audited public class YourEntity { // 你的实体字段和方法 }3. application.yml配置:确保你的application.yml或application.properties包含Hibernate Envers所需的配置。spring: data: jpa: repositories: enabled: true auditing: enabled: true4. 审计表字段:Hibernate Envers生成的审计表通常包括REV(修订号)、REVTYPE(修订类型)、AUDIT_TIMESTAMP(审计时间戳)等字段。这些字段共同存储对审计实体的历史更改。Spring Boot会自动创建审计表(例如,‘YourEntity_AUD’)以存储元数据。探索审计表中的字段:– REV:修订号(递增)– REVTYPE:修订类型(插入、更新、删除)– AUDITEDFIELD:审计字段值– MODIFIEDBY:进行更改的用户– MODIFIEDDATE:修改日期和时间5. 检索审计数据:使用Spring Data JPA仓库查询审计历史。import org.springframework.data.repository.history.RevisionRepository; import org.springframework.data.history.Revision; import java.util.List; public interface YourEntityAuditRepository extends RevisionRepository<YourEntity, Long, Integer> { List<Revision<Integer, YourEntity>> findRevisionsById(Long entityId); } 在这个例子中:– YourEntityAuditRepository扩展了RevisionRepository,这是一个处理修订的Spring Data JPA接口。– findRevisionsById方法允许你检索具有指定ID的实体的所有修订。然后,你可以在服务或控制器中使用此仓库查询审计历史:import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class AuditService { private final YourEntityAuditRepository entityAuditRepository; @Autowired public AuditService(YourEntityAuditRepository entityAuditRepository) { this.entityAuditRepository = entityAuditRepository; } public List<Revision<Integer, YourEntity>> getEntityRevisions(Long entityId) { return entityAuditRepository.findRevisionsById(entityId); } }另一个例子使用Hibernate Envers查询具有给定ID的特定实体的审计历史List<YourEntity_AUD> revisions = auditReader.findRevisions(YourEntity.class, entityld);– auditReader:一个AuditReader实例,由Hibernate Envers提供。它允许你与实体的审计历史进行交互。– findRevisions:Hibernate Envers提供的方法,用于检索具有指定ID的给定实体的所有修订。– YourEntity.class:你想要检索审计历史的实体类。– entityId:你想要获取修订的实体的特定ID。– List<YourEntity_AUD>:结果是一个审计实体(YourEntity_AUD)的列表,列表中的每个条目代表实体的一个修订。在Hibernate Envers中,当你为一个实体启用审计时,它会生成一个带有“_AUD”后缀的相应审计实体(默认情况下)。这个审计实体会跟踪原始实体随时间变化的所有更改。因此,这行代码本质上是在查询具有给定ID的实体的所有修订的审计历史,并将结果存储在一个审计实体列表中。然后,可以使用此列表分析或显示实体在不同修订中的更改。转载自https://www.cnblogs.com/didispace/p/18322448
-
前言在Spring框架中,@PostConstruct注解用于在Bean初始化完成后立即执行某些方法。这个注解的作用是保证在依赖注入完成后,执行一些初始化工作。诞生背景@PostConstruct注解的诞生是为了提供一种标准化的、简单的方法来进行对象初始化工作。1. 简化初始化逻辑在传统的Java开发中,进行对象初始化通常需要在构造函数中完成。然而,构造函数的局限性使得一些初始化操作变得复杂,例如:构造函数不能依赖于容器注入的资源,因为这些资源在对象实例化时可能还没有准备好。复杂的初始化逻辑会导致构造函数变得冗长且难以维护。@Component public class MyService { private final SomeDependency dependency; public MyService(SomeDependency dependency) { this.dependency = dependency; // 这里不能进行复杂的初始化逻辑,因为依赖可能还没有完全准备好 } }@PostConstruct提供了一种更清晰的方式来处理初始化逻辑。依赖注入完成后,容器会自动调用标注了 @PostConstruct的方法,使得开发者可以在这时安全地访问所有依赖资源。简化逻辑代码示例import javax.annotation.PostConstruct; import org.springframework.stereotype.Component; @Component public class MyService { private final SomeDependency dependency; public MyService(SomeDependency dependency) { this.dependency = dependency; // 这里不能进行复杂的初始化逻辑,因为依赖可能还没有完全准备好 } @PostConstruct public void init() { // 在这里进行初始化逻辑 // 这时依赖已经完全准备好,可以安全地使用 dependency.setup(); System.out.println("MyService 初始化完成"); } // 其他业务方法 public void performTask() { System.out.println("执行任务"); } }2. 标准化的生命周期管理在Java EE和Spring这样的依赖注入框架中,Bean的生命周期管理是一个重要的特性。@PostConstruct和@PreDestroy注解是Java EE(JSR-250)标准的一部分,为生命周期管理提供了标准化的解决方案。这使得开发者可以在不同的框架中使用相同的注解,而不用依赖于特定框架的API。3. 提高代码的可读性和可维护性@PostConstruct注解可以将初始化逻辑从构造函数中分离出来,使代码更具可读性和可维护性。开发者可以更清晰地看到哪些方法是用于初始化的,哪些方法是用于业务逻辑的。这种分离还可以使单元测试更加容易,因为初始化逻辑可以被单独测试。4. 与依赖注入框架的结合在依赖注入框架(如Spring)中,Bean的依赖关系是由容器管理的。在Bean实例化和依赖注入完成之后,框架会自动调用 @PostConstruct方法。这种机制确保了所有依赖都已准备就绪,开发者可以安全地在 @PostConstruct方法中进行进一步的初始化操作。生命周期的详细介绍Bean实例化:Spring容器根据配置文件或注解扫描创建Bean实例。依赖注入:Spring容器进行依赖注入,将所需的依赖对象注入到Bean中。调用@PostConstruct方法:依赖注入完成后,Spring容器会调用标注了@PostConstruct的方法。这个方法通常用于执行初始化操作,比如配置资源、进行检查或执行一些准备工作。Bean可用:在调用了@PostConstruct方法后,Bean被标记为可用,并准备好接受请求或执行其预定的功能。Bean销毁(可选,使用@PreDestroy):当Spring容器关闭时,可以通过@PreDestroy注解来指定一个方法,在Bean被销毁之前执行清理工作。生命周期的代码示例import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import org.springframework.stereotype.Component; @Component public class MyBean { // 构造方法 public MyBean() { System.out.println("MyBean实例化"); } // @PostConstruct注解的方法 @PostConstruct public void init() { System.out.println("MyBean初始化 - @PostConstruct方法调用"); // 这里可以执行一些初始化操作 } // @PreDestroy注解的方法 @PreDestroy public void cleanup() { System.out.println("MyBean销毁之前 - @PreDestroy方法调用"); // 这里可以执行一些清理操作 } // 其他业务方法 public void performTask() { System.out.println("MyBean执行任务"); } }关键点总结依赖注入后执行:@PostConstruct方法在Bean的依赖注入完成之后执行,确保所有依赖都已经准备就绪。只执行一次:@PostConstruct方法在Bean的整个生命周期中只会执行一次。用于初始化逻辑:适合用来执行一些初始化逻辑,比如检查配置、初始化资源、启动辅助线程等。与Spring结合:@PostConstruct是JavaEE规范中的一部分,但在Spring中也得到了广泛应用,主要用于简化Bean的初始化工作。注意事项只适用于单个方法:一个类中只能有一个 @PostConstruct方法。方法签名:@PostConstruct方法不能有任何参数,也不能有返回值。异常处理:@PostConstruct方法如果抛出异常,会阻止Bean的创建,Spring容器会在启动时抛出异常。转载自https://www.cnblogs.com/hyg0513/p/18314560
-
扑克算牌公式及Java和C++软件制作要实现一个扑克的算牌公式,并开发相应的Java和C++程序,首先需要定义一些基本的步骤和概念。具体来说,我们需要:定义扑克数据结构:表示扑克牌和其属性。历史数据导入:从外部文件或数据库中导入历史数据。概率计算:基于历史数据计算剩余牌的出现次数和概率。界面及交互:提供用户接口来发牌并显示结果。1. 数据结构定义我们用简单的数据结构来表示扑克牌。在Java和C++中,可以分别使用类和结构体来实现。Java代码public class Card { public enum Suit {HEARTS, DIAMONDS, CLUBS, SPADES} private Suit suit; private int value; public Card(Suit suit, int value) { this.suit = suit; this.value = value; } public Suit getSuit() { return suit; } public int getValue() { return value; } @Override public String toString() { return suit.toString() + value; } }C++代码#include <string> #include <iostream> struct Card { enum Suit { HEARTS, DIAMONDS, CLUBS, SPADES } suit; int value; std::string toString() const { static const char* suitNames[] = {"Hearts", "Diamonds", "Clubs", "Spades"}; return suitNames[suit] + std::to_string(value); } };2. 历史数据导入假设历史数据存储在一个CSV文件中,我们可以编写函数来读取这些数据。Java代码import java.io.*; import java.util.*; public class HistoryData { public static List<List<Card>> loadHistory(String filename) throws IOException { List<List<Card>> history = new ArrayList<>(); BufferedReader br = new BufferedReader(new FileReader(filename)); String line; while ((line = br.readLine()) != null) { String[] parts = line.split(","); List<Card> hand = new ArrayList<>(); for (String part : parts) { String[] cardParts = part.split(" "); Card.Suit suit = Card.Suit.valueOf(cardParts[0].toUpperCase()); int value = Integer.parseInt(cardParts[1]); hand.add(new Card(suit, value)); } history.add(hand); } br.close(); return history; } }C++代码#include <vector> #include <fstream> #include <sstream> #include <iostream> std::vector<std::vector<Card>> loadHistory(const std::string& filename) { std::vector<std::vector<Card>> history; std::ifstream file(filename); std::string line; while (std::getline(file, line)) { std::istringstream ss(line); std::vector<Card> hand; std::string part; while (std::getline(ss, part, ',')) { std::istringstream cardStream(part); std::string suitStr; int value; cardStream >> suitStr >> value; Card::Suit suit; if (suitStr == "Hearts") suit = Card::HEARTS; else if (suitStr == "Diamonds") suit = Card::DIAMONDS; else if (suitStr == "Clubs") suit = Card::CLUBS; else if (suitStr == "Spades") suit = Card::SPADES; hand.push_back({suit, value}); } history.push_back(hand); } return history; }3. 概率计算基于导入的历史数据进行概率计算。Java代码import java.util.HashMap; import java.util.List; import java.util.Map; public class ProbabilityCalculator { public static Map<Card, Double> calculateProbabilities(List<List<Card>> history, List<Card> givenCards) { Map<Card, Integer> counts = new HashMap<>(); for (List<Card> hand : history) { boolean match = true; for (Card card : givenCards) { if (!hand.contains(card)) { match = false; break; } } if (match) { for (Card card : hand) { counts.put(card, counts.getOrDefault(card, 0) + 1); } } } int totalHands = history.size(); Map<Card, Double> probabilities = new HashMap<>(); for (Map.Entry<Card, Integer> entry : counts.entrySet()) { probabilities.put(entry.getKey(), entry.getValue() / (double) totalHands); } return probabilities; } }C++代码#include <unordered_map> #include <vector> std::unordered_map<Card, double, CardHash> calculateProbabilities( const std::vector<std::vector<Card>>& history, const std::vector<Card>& givenCards) { std::unordered_map<Card, int, CardHash> counts; int totalHands = 0; for (const auto& hand : history) { bool match = true; for (const auto& card : givenCards) { if (std::find(hand.begin(), hand.end(), card) == hand.end()) { match = false; break; } } if (match) { totalHands++; for (const auto& card : hand) { counts[card]++; } } } std::unordered_map<Card, double, CardHash> probabilities; for (const auto& entry : counts) { probabilities[entry.first] = entry.second / static_cast<double>(totalHands); } return probabilities; }4. 主程序及交互将所有部分整合到主程序中,并提供交互界面。Java代码import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Scanner; public class Main { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); System.out.println("Enter the path to the historical data file:"); String filepath = scanner.nextLine(); List<List<Card>> history = HistoryData.loadHistory(filepath); System.out.println("Enter the three cards (e.g., 'HEARTS 2', 'CLUBS 5', 'HEARTS 10'):"); List<Card> givenCards = new ArrayList<>(); for (int i = 0; i < 3; i++) { String input = scanner.nextLine(); String[] parts = input.split(" "); Card.Suit suit = Card.Suit.valueOf(parts[0].toUpperCase()); int value = Integer.parseInt(parts[1]); givenCards.add(new Card(suit, value)); } Map<Card, Double> probabilities = ProbabilityCalculator.calculateProbabilities(history, givenCards); System.out.println("Probabilities of remaining cards:"); for (Map.Entry<Card, Double> entry : probabilities.entrySet()) { System.out.printf("%s: %.2f%%\n", entry.getKey(), entry.getValue() * 100); } } }C++代码#include <iostream> #include <vector> #include <unordered_map> int main() { std::cout << "Enter the path to the historical data file:" << std::endl; std::string filepath; std::cin >> filepath; auto history = loadHistory(filepath); std::cout << "Enter the three cards (e.g., 'HEARTS 2', 'CLUBS 5', 'HEARTS 10'):" << std::endl; std::vector<Card> givenCards; for (int i = 0; i < 3; ++i) { std::string suitStr; int value; std::cin >> suitStr >> value; Card::Suit suit; if (suitStr == "Hearts") suit = Card::HEARTS; else if (suitStr == "Diamonds") suit = Card::DIAMONDS; else if (suitStr == "Clubs") suit = Card::CLUBS; else if (suitStr == "Spades") suit = Card::SPADES; givenCards.push_back({suit, value}); } auto probabilities = calculateProbabilities(history, givenCards); std::cout << "Probabilities of remaining cards:" << std::endl; for (const auto& entry : probabilities) { std::cout << entry.first.toString() << ": " << entry.second * 100 << "%" << std::endl; } return 0; }总结通过上述步骤,我们实现了一个扑克算牌工具,可以根据给定的三张牌计算后续牌的出现概率。该工具使用Java和C++分别实现了数据结构、历史数据导入、概率计算以及用户交互。
-
介绍Spring Boot 提供了事件驱动的编程模型,允许你在应用程序中定义和监听自定义事件。这种机制使得组件之间可以进行松耦合的通信。应用使用场景解耦业务逻辑:通过事件机制,可以让不同模块或组件之间的业务逻辑解耦。异步处理:某些操作可以通过事件机制异步处理,提升系统响应速度。扩展性:可以很容易地添加新的事件处理器,而不会影响现有代码。原理解释Spring 的事件机制基于 ApplicationEvent 和 ApplicationListener。ApplicationEvent 是事件的基类,所有自定义事件都需要继承它,而 ApplicationListener 是事件监听器接口,用于接收事件通知。算法原理流程图+-----------------+| Application || Context |+--------+--------+ | v+--------+--------+| Raise Event || (ApplicationEvent)|+--------+--------+ | v+--------+--------+| Event Listener || (ApplicationListener) |+--------+--------+ | v+-----------------+| Handle Event |+-----------------+算法原理解释启动上下文:Spring 容器启动并初始化 ApplicationContext。触发事件:当某个事件发生时,调用 ApplicationEventPublisher#publishEvent() 方法发布事件。监听事件:实现 ApplicationListener 接口的 bean 会自动注册为事件监听器,当事件被触发时,监听器会收到事件通知并执行对应的业务逻辑。应用场景代码示例实现1. 定义自定义事件public class CustomEvent extends ApplicationEvent { private String message; public CustomEvent(Object source, String message) { super(source); this.message = message; } public String getMessage() { return message; }}2. 定义事件监听器import org.springframework.context.event.EventListener;import org.springframework.stereotype.Component;@Componentpublic class CustomEventListener { @EventListener public void handleCustomEvent(CustomEvent event) { System.out.println("Received custom event - " + event.getMessage()); }}3. 发布事件import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.ApplicationEventPublisher;import org.springframework.stereotype.Service;@Servicepublic class CustomEventPublisher { @Autowired private ApplicationEventPublisher applicationEventPublisher; public void publishEvent(String message) { CustomEvent customEvent = new CustomEvent(this, message); applicationEventPublisher.publishEvent(customEvent); }}4. 启动应用程序并测试创建一个简单的 REST Controller 来触发事件:import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class EventController { @Autowired private CustomEventPublisher customEventPublisher; @GetMapping("/trigger-event") public String triggerEvent(@RequestParam String message) { customEventPublisher.publishEvent(message); return "Event triggered!"; }}部署测试场景启动应用程序:使用 mvn spring-boot:run 启动 Spring Boot 应用。触发事件:访问 http://localhost:8080/trigger-event?message=HelloWorld,检查控制台输出。材料链接Spring 官方文档:事件Spring Boot 官方文档总结Spring 的事件机制提供了一种优雅的方式来解耦组件间的依赖关系,同时也能方便地实现异步处理。通过 ApplicationEvent 和 ApplicationListener,我们可以方便地定义、发布和监听事件,从而构建灵活且可扩展的系统架构。未来展望随着微服务架构的普及,事件驱动架构将会越来越重要。未来,我们可能会看到更多基于消息队列(如 Kafka 或 RabbitMQ)的事件驱动解决方案与 Spring 集成,以更好地支持分布式系统中的事件处理。此外,结合云计算平台(如 AWS Lambda)和无服务器架构,也将进一步拓展事件驱动编程的应用范围。
-
前言 快速排序算法可以分为两部分来看: 第一部分:将枢轴元素移动到最终位置 第二部分:分别处理枢轴元素左右两边的元素 tips:上面的第一、二步是不断递归的过程。读者可以去某站看一下王道的数据结构课 建议:1.学习算法最重要的是理解算法的每一步,而不是记住算法。 2.建议读者学习算法的时候,自己手动一步一步地运行算法。 1. 快速排序简介 快速排序是一种分治法(Divide and Conquer)的排序算法,由英国计算机科学家Tony Hoare于1960年提出。其基本思想是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有元素均比另一部分的元素小,然后分别对这两部分继续进行排序,最终达到整个序列有序的效果。 2. 快速排序的基本原理 快速排序的基本原理可以总结为以下三个步骤: 2.1 选择基准元素 从待排序的数组中选择一个元素作为基准元素。选择基准元素的方式有多种,常见的方法包括选择第一个元素、最后一个元素或者随机选择一个元素。 2.2 分割操作 将数组中比基准元素小的元素移到基准元素的左边,比基准元素大的元素移到右边。这个过程称为分割操作。 2.3 递归排序 递归地对基准元素左右两侧的子数组进行快速排序。 3. Java中的快速排序实现 下面是一个简单的Java实现快速排序的例子: public class QuickSort { public static void quickSort(int[] arr, int low, int high) { if (low < high) { // 找到划分点 int pivotIndex = partition(arr, low, high); // 递归地对左右两部分进行快速排序 quickSort(arr, low, pivotIndex - 1); quickSort(arr, pivotIndex + 1, high); } } private static int partition(int[] arr, int low, int high) { // 选择最后一个元素作为基准 int pivot = arr[high]; int i = (low - 1); // 小于基准值的索引 for (int j = low; j < high; j++) { // 当前元素小于或等于基准值时 if (arr[j] <= pivot) { i++; // 交换元素,使得小于基准值的元素移到左边 int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } // 把基准值放到正确的位置上(即i+1) int temp = arr[i + 1]; arr[i + 1] = arr[high]; arr[high] = temp; return i + 1; } public static void main(String[] args) { int[] arrayToSort = {9, 7, 5, 11, 12, 2, 14, 3, 10, 6}; int n = arrayToSort.length; quickSort(arrayToSort, 0, n - 1); System.out.println("Sorted array: "); for (int num : arrayToSort) { System.out.print(num + " "); } } } 在上述代码中: quickSort 方法是用于递归调用的主要方法,它接受数组、以及待排序区间的起始和结束索引。 partition 方法负责执行分区操作,返回基准元素最终应该所在的位置。 在 partition 方法内部,使用了一个指针 i 来跟踪小于基准值的所有元素的边界,并通过遍历整个区间来进行元素的移动和交换,最后确保基准元素处于正确位置,并返回其索引。 通过这样的方式,快速排序能够以平均时间复杂度为 的效率完成对数据的排序。 4.时空复杂度 4.1 时间复杂度: 最好情况(平均情况): 当每次划分都能将数组大致均等地分为两部分时,快速排序的时间复杂度为 ,其中n是待排序数组中的元素个数。这是由于每次递归调用都会减少问题规模大约一半,并且需要层递归。 最坏情况: 在最坏情况下,即输入数组已经有序或接近有序时,每次划分只能将数组划分为一个元素和剩余元素两部分,这会导致递归树退化为线性结构,时间复杂度上升到 。不过,通过随机化选择基准元素或采用三数取中等优化方法,可以在实际应用中避免这种情况的发生,使得快速排序在实践中通常能保持接近其平均时间复杂度。 4.2 空间复杂度: 原地排序: 快速排序在理想情况下只需要常数级别的额外空间,即空间复杂度为 O(1)。这是因为交换操作可以直接在原数组上进行,不需要额外存储空间。 递归栈空间: 然而,在实际实现中,快速排序是递归实现的,因此递归调用会占用一定的栈空间。在最坏的情况下,递归深度可达 n 层,所以空间复杂度可能达到 。然而,在平均情况下,由于快速排序的平衡特性,递归树的高度约为,故空间复杂度通常是。 4.3 总结: 总结来说,快速排序在理想情况下具有非常高的效率,尤其在处理大型数据集时表现出色。但要注意的是,对于小规模数据或者近乎有序的数据,其他排序算法(如插入排序)可能会更优。同时,为了保证算法性能稳定,通常会对基准元素的选择进行优化以减小最坏情况发生的概率。 5.优缺点 5.1 优点: 高效性: 在平均情况下,快速排序的时间复杂度为,这使得它在处理大规模数据时表现出极高的效率,并且是实践中最快的通用内部排序算法之一。 原地排序(In-place): 快速排序不需要额外的存储空间来保存临时数组,只需要一个很小的栈空间用于递归调用,所以空间复杂度在理想情况下可以达到。 广泛应用: 由于其高效的性能和灵活的实现,快速排序被广泛应用于实际系统中的各种排序场景,尤其是在内存受限或需要实时排序的情况下。 适应性强: 对于不同的输入数据分布有较好的适应性,尤其是当采用随机化版本选择基准元素时,能有效地避免最坏情况的发生。 易于理解与实现: 基本思想简单直观,通过分治策略将大问题划分为小问题,便于理解和编程实现。 5.2 缺点: 最坏情况下的性能: 当待排序的数据已经部分有序或者完全有序时,快速排序可能退化成的时间复杂度。不过,可以通过优化基准元素的选择(例如三数取中、随机化选取等方法)来减少这种情况发生的概率。 不稳定排序: 快速排序不是稳定的排序算法,即相等元素的相对顺序可能会在排序过程中改变。 递归深度: 由于使用了递归,如果数据规模很大且递归树不平衡时,可能会导致栈溢出的问题。虽然通过尾递归优化或其他非递归实现方式可以缓解这个问题,但这也增加了实现的复杂性。 边界条件处理: 对于非常小的数据集(如小于一定阈值),快速排序可能不如插入排序等其他简单的排序算法高效,因此通常会结合其他算法进行混合排序以提升整体性能。 6.现实中的应用 快速排序算法在现实中的应用非常广泛,因其高效性、通用性和易于实现的特点,被大量用于各种软件和硬件系统中。以下是一些典型的应用场景: 数据处理与分析: 在大数据处理、数据分析以及数据库管理系统中,快速排序是内部排序操作的核心算法之一,尤其适用于大规模数据集的排序,比如SQL查询语句执行过程中的ORDER BY子句。 编程语言标准库: 许多现代编程语言的标准库或内置函数都包含了快速排序算法,例如Java的Arrays类、C++的std::sort函数等,为开发者提供了现成的排序工具。 操作系统内核: 操作系统内核在处理内存管理、文件系统排序目录结构、进程调度时可能使用快速排序或其他优化过的排序算法来提高效率。 机器学习与人工智能: 在机器学习领域,快速排序可应用于特征选择、模型训练前的数据预处理阶段,例如对输入样本进行排序以构建更高效的索引结构。 编译器优化: 编译器在符号表管理和代码生成阶段,可能会用到快速排序对变量名、函数名等进行排序,以加速查找和比较操作。 科学计算与工程应用: 在数值计算、信号处理、图像处理等领域,快速排序可用于数组和矩阵元素的排序,尤其是在处理稀疏矩阵和大型向量时。 嵌入式系统与实时系统: 尽管快速排序在最坏情况下的性能不理想,但在许多实际应用中通过随机化选取基准值等方法可以有效避免性能退化,因此也被用于嵌入式系统或实时系统的数据排序。 竞赛编程与算法研究: 在ACM国际大学生程序设计竞赛(ACM-ICPC)以及其他算法竞赛中,快速排序作为基础排序算法,经常出现在解题方案中。 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/weixin_56154577/article/details/136596325
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签