1.Docker安装Nginx并启动#没有安装的情况下会自动安装，可以指定版本 docker run -p 80:80 --name nginx -d nginx:1.10 #将容器内的配置文件拷贝到当前目录 #别忘了后面的点 docker container cp nginx:/etc/nginx . #修改文件名称并把这个 conf 移动到/mydata/nginx 下 mv nginx conf #终止原容器 docker stop nginx #执行命令删除原容器 docker rm nginx #创建新的 nginx；执行以下命令 docker run -p 80:80 --name nginx \ -v /mydata/nginx/html:/usr/share/nginx/html \ -v /mydata/nginx/logs:/var/log/nginx \ -v /mydata/nginx/conf:/etc/nginx \ -d nginx:1.10 #给 nginx 的 html 下面放的所有资源可以直接访问 drwxr-xr-x. 3 root root 34 Oct 17 07:49 html2.搭建域名访问要求是 ： 访问gulimall.com 能访问到我们自己本机idea中启动的product服务思路：我们通过配置本地的host域名映射我们虚拟机的中的nginx，然后nginx在根据转发规把请求转发回我们自己的idea中的本地服务。前置说明：因为我们的nginx的安装在云服务器上的，本地Host文件解析域名后访问云服务器, 结果请求被拦截虽然可以用谷歌浏览器安装ModHeader插件解决，但是我们在把请求映射到nginx后，在让nginx转发回我们本地的服务是无法完成的，因为云服务器无法ping通我们本地的ip 原因 本地pc属于移动宽带的内网环境，说白了就是cmd下ipconfig里的ip是你电脑宽带运营商的一个大网络（外网）下的一个内网ip，服务器无法主动与内网ip建立连接，连路由都做不到。所以不可能ping通的。可以考虑使用VPN在您的电脑和服务器之间建立VPN连接，这样服务器和您的电脑就能像在同一个内网一样互相访问了2.1配置本地host可以利用SwitchHost配置工具。需要注意的是这个软件要用管理员打开，然后C:\Windows\System32\drivers\etc把hosts的文件权限放开，才能进行修改如下图配置，前面是虚拟机的地址，后面是需要解析的域名2.2配置nginx# 查看我们nginx的总配置文件 [root@ecs-284198 conf]# cat /mydata/nginx/conf/nginx.conf user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; } [root@ecs-284198 conf]# # 当前总文件的最后一行 include /etc/nginx/conf.d/*.conf; 他会加载这个路径下的所有以 .conf结尾的文件作为配置文件，并且在包含到我们的当前的总配置文件中，这样就不会导致一个配置文件很大，所以接下来我们到 conf.d 的目录下新建一个gulimall.conf文件 # cd 到 /mydata/nginx/conf/conf.d/会有一个default.conf，然后我们复制一个default.conf为gulimall.conf [root@ecs-284198 conf.d]# cd /mydata/nginx/conf/conf.d/ [root@ecs-284198 conf.d]# ll total 8 -rw-r--r-- 1 root root 1097 Jan 31 2017 default.conf -rw-r--r-- 1 root root 1065 Apr 28 21:56 gulimall.conf [root@ecs-284198 conf.d]# # 修改后的gulimall.conf 如下 server { listen 80; server_name gulimall.com; #charset koi8-r; #access_log /var/log/nginx/log/host.access.log main; location / { proxy_pass http://192.168.56.1:15000; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} } # 其中 proxy_pass http://192.168.56.1:15000; 中的IP使我们本地的ip,访问会我们自己本地idea启动的product服务 # 但是这样会有一个问题 如果我们的product服务有多个实列，就不好办了，其实这个proxy_pass http://192.168.56.1:15000应该配置成我们的网卡 gateway， 让gateway负载均衡到我们的服务中去，下一节我们会解决这个问题3.Nginx-搭建域名访问（负载就均衡到网关）3.1修改总nginx的配置文件# 在http 模块中配置 upstream [root@ecs-284198 conf]# cat nginx.conf user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; + upstream gulimall{ + server 192.168.56.1:88; + } include /etc/nginx/conf.d/*.conf; } # 配置文件中upstream 叫上有服务器 然后取了一个名字叫gulimall，然后里面的里面配置的是我们网关的地址，就是以后所有请求我们上游服务器gulimall 的地址都会转发到网关去3.2修改gulimall.conf# 在子配置文件gulimall.conf中，的location 块添加 proxy_set_header Host $host; 表示nginx在转发的时候把host也带上，如果不加，nginx会丢失请求头，host，cookies等信息 把原来请求我们product的服务改成proxy_pass http://gulimall;其中 http://gulimall中的gulimall 就是我们在总配置文件中取的上游服务器的名字， # 整配置文件的意思，就是监听到gulimall域名发送过来的80请求，都会转到上游服务器为gulimall中去，并且带上host信息，而上游服务器又是转发到网关的。 [root@ecs-284198 conf]# cat conf.d/gulimall.conf server { listen 80; server_name gulimall.com; #charset koi8-r; #access_log /var/log/nginx/log/host.access.log main; location / { + proxy_set_header Host $host; + proxy_pass http://gulimall; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} }3.3修改gateway服务的yaml配置spring: cloud: gateway: routes: # - id: test_route # uri: https://www.baidu.com # predicates: # - Query=url,baidu # # - id: qq_route # uri: https://www.qq.com # predicates: # - Query=url,qq - id: product_route uri: lb://gulimall-product predicates: - Path=/api/product/**,/hello filters: - RewritePath=/api/(?<segment>.*),/$\{segment} - id: third_party_route uri: lb://gulimall-third-party predicates: - Path=/api/thirdparty/** filters: - RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment} - id: member_route uri: lb://gulimall-member predicates: - Path=/api/member/** filters: - RewritePath=/api/(?<segment>.*),/$\{segment} - id: ware_route uri: lb://gulimall-ware predicates: - Path=/api/ware/** filters: - RewritePath=/api/(?<segment>.*),/$\{segment} - id: admin_route uri: lb://renren-fast predicates: - Path=/api/** filters: - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment} # 注意要放最后 - id: gulimall_host_route uri: lb://gulimall-product predicates: - Host=gulimall.com4.简单认识一下Nginx和工作原理5.性能压测-优化-Nginx动静分离由于动态资源和静态资源目前都处于服务端，所以为了减轻服务器压力，我们将js、css、img等静态资源放置在Nginx端，以减轻服务器压力。5.1动静态文件上传到 mydata/nginx/html/static/index/css，这种格式5.2修改index.html的静态资源路径修改index.html的静态资源路径，加上static前缀src=“/static/index/img/img_09.png”5.3修改/mydata/nginx/conf/conf.d/gulimall.conf如果遇到有/static为前缀的请求，转发至html文件夹location /static/ { root /usr/share/nginx/html; } location / { proxy_pass http://gulimall; proxy_set_header Host $host; }
1.给es中建立索引。product,建立好映射关系PUT product { "mappings": { "properties": { "skuId": { "type": "long" }, "spuId": { "type": "long" }, "skuTitle": { "type": "text", "analyzer": "ik_smart" }, "skuPrice": { "type": "keyword" }, "skuImg": { "type": "keyword", "index": false, "doc_values": false }, "saleCount": { "type": "long" }, "hosStock": { "type": "boolean" }, "hotScore": { "type": "long" }, "brandId": { "type": "long" }, "catelogId": { "type": "long" }, "brandName": { "type": "keyword", "index": false, "doc_values": false }, "brandImg": { "type": "keyword", "index": false, "doc_values": false }, "catelogName": { "type": "keyword", "index": false, "doc_values": false }, "attrs": { "type": "nested", "properties": { "attrId": { "type": "long" }, "attrName": { "type": "keyword", "index": false, "doc_values": false }, "attrValue": { "type": "keyword" } } } } } }2.Service类package com.atguigu.gulimall.lsearch.service; import com.atguigu.common.to.es.SkuEsModel; import java.io.IOException; import java.util.List; public interface ProductSaveService { boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException; }3.ServiceImpl类package com.atguigu.gulimall.lsearch.service.iml; import com.alibaba.fastjson.JSON; import com.atguigu.common.to.es.SkuEsModel; import com.atguigu.gulimall.lsearch.config.GulimallElasticSearchConfig; import com.atguigu.gulimall.lsearch.constant.EsConstant; import com.atguigu.gulimall.lsearch.service.ProductSaveService; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.xcontent.XContentType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @Slf4j @Service("productSaveService") public class ProductSaveServiceImpl implements ProductSaveService { @Autowired RestHighLevelClient restHighLevelClient; @Override public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException { //保存到es //1.给es中建立索引。product,建立好映射关系 //2.给es中保存数据 BulkRequest bulkRequest, RequestOptions options BulkRequest bulkRequest = new BulkRequest(); for (SkuEsModel model : skuEsModels) { //构造保存请求 IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX); indexRequest.id(model.getSkuId().toString()); String s = JSON.toJSONString(model); indexRequest.source(s, XContentType.JSON); bulkRequest.add(indexRequest); } BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); //TODO 如果批量保存错误则需要处理 boolean b = bulk.hasFailures(); List<String> collect = Arrays.stream(bulk.getItems()).map(item -> { return item.getId(); }).collect(Collectors.toList()); log.info("商品上架成功:{}，返回数据:{}", collect,bulk.toString()); return b; } }4.Controller类package com.atguigu.gulimall.lsearch.controller; import com.atguigu.common.exception.BizCodeEnume; import com.atguigu.common.to.es.SkuEsModel; import com.atguigu.common.utils.R; import com.atguigu.gulimall.lsearch.service.ProductSaveService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @Slf4j @RequestMapping("/search/save") @RestController public class ElasticSaveController { @Autowired ProductSaveService productSaveService; @PostMapping("/product") public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) { boolean b = false; try { b = productSaveService.productStatusUp(skuEsModels); } catch (Exception e) { log.error("ElasticSaveController商品商家错误:{}", e); return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMessage()); } if (!b) { return R.ok(); } else { return R.error(BizCodeEnume.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnume.PRODUCT_UP_EXCEPTION.getMessage()); } } }5.BizCodeEnume枚举package com.atguigu.common.exception; /** * @Description: 错误状态码枚举 * @Created: with IntelliJ IDEA. * @author: 夏沫止水 * @createTime: 2020-05-27 17:29 * * 错误码和错误信息定义类 * 1. 错误码定义规则为5为数字 * 2. 前两位表示业务场景，最后三位表示错误码。例如：100001。10:通用 001:系统未知异常 * 3. 维护错误码后需要维护错误描述，将他们定义为枚举形式 * 错误码列表： * 10: 通用 * 001：参数格式校验 * 002：短信验证码频率太高 * 11: 商品 * 12: 订单 * 13: 购物车 * 14: 物流 * 15：用户 **/ public enum BizCodeEnume { UNKNOW_EXCEPTION(10000, "系统未知异常"), VAILD_EXCEPTION(10001, "参数格式校验失败"), TO_MANY_REQUEST(10002, "请求流量过大，请稍后再试"), SMS_CODE_EXCEPTION(10002, "验证码获取频率太高，请稍后再试"), PRODUCT_UP_EXCEPTION(11000, "商品上架异常"), USER_EXIST_EXCEPTION(15001, "存在相同的用户"), PHONE_EXIST_EXCEPTION(15002, "存在相同的手机号"), NO_STOCK_EXCEPTION(21000, "商品库存不足"), LOGINACCT_PASSWORD_EXCEPTION(15003, "账号或密码错误"), ; private Integer code; private String message; BizCodeEnume(Integer code, String message) { this.code = code; this.message = message; } public Integer getCode() { return code; } public String getMessage() { return message; } }6.其他测试功能package com.atguigu.gulimall.lsearch; import com.alibaba.fastjson.JSON; import com.atguigu.gulimall.lsearch.config.GulimallElasticSearchConfig; import lombok.Data; import lombok.ToString; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.Avg; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import java.io.IOException; import java.util.Map; @SpringBootTest public class GulimallSearchApplicationTests { @Autowired private RestHighLevelClient client; /** * 测试复杂查询功能 */ @Test public void searchDate() throws IOException { //1.创建检索请求 SearchRequest searchRequest = new SearchRequest(); //指定索引 searchRequest.indices("bank"); //指定DSL，检索条件 //SearchSourceBuilder sourceBuilder 封装的条件 SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); //1.1构造检索条件 searchSourceBuilder.query(); searchSourceBuilder.from(); searchSourceBuilder.size(); searchSourceBuilder.aggregation(); searchSourceBuilder.query(QueryBuilders.matchQuery("address", "mill")); //1.2按照年龄的值分布进行聚合 TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10); searchSourceBuilder.aggregation(ageAgg); //1.3计算平均薪资 TermsAggregationBuilder balanceAvg = AggregationBuilders.terms("balanceAvg").field("balance"); searchSourceBuilder.aggregation(balanceAvg); System.out.println("检索条件" + searchSourceBuilder.toString()); searchRequest.source(searchSourceBuilder); //2.执行检索 SearchResponse searchResponce = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); //3.分析结果 System.out.println(searchResponce.toString()); //Map map = JSON.parseObject(searchResponce.toString(), Map.class); 这种方式太麻烦 //3.1获取所有查询到的数据 SearchHits hits = searchResponce.getHits(); SearchHit[] searchHits = hits.getHits(); for (SearchHit hit : searchHits) { String sourceAsString = hit.getSourceAsString(); Account account = JSON.parseObject(sourceAsString, Account.class); System.out.println("account" + account); } //3.2获取这次检索到的分析信息 Aggregations aggregations = searchResponce.getAggregations(); Terms ageAgg1 = aggregations.get("ageAgg"); for (Terms.Bucket bucket : ageAgg1.getBuckets()) { String keyAsString = bucket.getKeyAsString(); System.out.println("年龄" + keyAsString + "===>" + bucket.getDocCount()); } Avg balanceAvg1 = aggregations.get("balanceAvg"); System.out.println("平均工资" + balanceAvg1.getValue()); } @ToString @Data static class Account { private int account_number; private int balance; private String firstname; private String lastname; private int age; private String gender; private String address; private String employer; private String email; private String city; private String state; } /** * 测试存储数据到es,保存更新二合一 */ @Test public void indexDate() throws IOException { IndexRequest indexRequest = new IndexRequest("users"); //数据的id indexRequest.id("1"); // indexRequest.source("userName", "youzi", "age", 18); User user = new User(); user.setUserName("Youzi"); user.setAge(28); user.setGender("男"); String jsonString = JSON.toJSONString(user); //要保存的内容 indexRequest.source(jsonString, XContentType.JSON); //执行index操作 IndexResponse index = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS); //提取有用的响应数据 System.out.println(index); } @Data class User { private String userName; private String gender; private int age; } }7.工具类package com.atguigu.gulimall.lsearch.config; import org.apache.http.HttpHost; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 1.导入配置 * 2.编写配置,给容器内注入一个RestHighLevelClient */ @Configuration public class GulimallElasticSearchConfig { @Bean public RestHighLevelClient esRestClient() { RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( //第一个参数是ElasticSearch安装机地址，第二个参数是端口，第三个参数是协议"http"或"https" new HttpHost("192.168.56.10", 9200, "http") ) ); return client; } public static final RequestOptions COMMON_OPTIONS; static { RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder(); // builder.addHeader("Authorization", "Bearer " + TOKEN); // builder.setHttpAsyncResponseConsumerFactory( // new HttpAsyncResponseConsumerFactory // .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024)); COMMON_OPTIONS = builder.build(); } }
1.安装和原服务器一样的环境1.1我用的是CentOS 系统，首先安装系统环境1.2我使用的是宝塔面板，所以宝塔面板也要安装一致（版本不一致也可以用，不过最好用的是一样的，避免出现未知原因引起的错误）2.迁移前做好备案准备分两种情况在同一个服务器厂商的情况下，只需要把网站的文件迁移到新的服务器（网站文件的路径要和旧服务器的一致）。在不同的服务器厂商的情况下，首先要先进行网站的备案，大概七个工作日，所以不要等到旧服务器快要过期才迁移，这样会导致时间上的不充裕，在迁移过程中会使网站暂停运行，剩下的就是把文件从旧服务器上的文件上传到新的服务器（路径要一致）。3.数据库的迁移在旧服务器上怎么安装的数据库，在新服务器上就怎么安装，最好就是版本都是一样的，然后导出旧数据上的表结构和数据，然后在新服务器上导入，要注意的就是数据库名称和密码要和旧数据库的要一致，这样就方便在新的服务器上运行的时候，不用重新修改代码。4.迁移完成迁移结束后，域名解析要换成新服务器的公网IP，会有缓存，并不是一修改好就马上解析上去的，24-72小时内，所以解析成功后要记得有空看看你的网站能否正常运行。PS1.迁移工具在宝塔面板有一个这个工具，我测试了一下，也是可以迁移成功的2.迁移时图床出现的问题我在迁移过程中是没有发现什么问题，主要是我自己的两个图床工具突然就是上传失败了，完全不能用，报错信息是Call to undefined function think\finfo open()，主要是php中的一个拓展没有安装。2.1找到这个2.2在设置里找到fileinfo并安装2.3重新测试
监听器（用MyBatis的IService接口做了封装）package io.github.talelin.latticy.listener; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.alibaba.excel.util.ListUtils; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.extension.service.IService; import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.Map; @Slf4j public class ExcelReaderListener<T> extends AnalysisEventListener<T> { /** * 每隔5条存储数据库，实际使用中可以100条，然后清理list ，方便内存回收 */ private static final int BATCH_COUNT = 100; /** * 缓存的数据 */ private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); /** * 假设这个是一个DAO，当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。 */ private IService<T> service; /** * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来 * * @param service */ public ExcelReaderListener(IService<T> service) { this.service = service; } /** * 这个每一条数据解析都会来调用 * * @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()} * @param context */ @Override public void invoke(T data, AnalysisContext context) { log.info("解析到一条数据:{}", JSON.toJSONString(data)); cachedDataList.add(data); // 达到BATCH_COUNT了，需要去存储一次数据库，防止数据几万条数据在内存，容易OOM if (cachedDataList.size() >= BATCH_COUNT) { saveData(); // 存储完成清理 list //注意ListUtils.newArrayListWithExpectedSize是在EasyExcel3.1.1版本之后才有的 cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); } } @Override public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) { log.info("表头数据 excelHead= {}", headMap); } /** * 所有数据解析完成了 都会来调用 * * @param context */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 这里也要保存数据，确保最后遗留的数据也存储到数据库 saveData(); log.info("所有数据解析完成！"); } /** * 加上存储数据库 */ private void saveData() { log.info("{}条数据，开始存储数据库！", cachedDataList.size()); service.saveBatch(cachedDataList); log.info("存储数据库成功！"); } }控制层代码@PostMapping("/import") public AjaxResult importExcel(MultipartFile file) throws IOException { AjaxResult ajaxResult = new AjaxResult(); try { //file.getInputStream()是获取导入的文件数据 //SStbkSmbsnFncDtlI01ADO.class是对应的实体类 //YwtzTyrzmxCwbListener是对应的监听器，这里是自定义对导入的数据做判断限制 //sStbkSmbsnFncDtlI01AService是对应的Service类 //.sheet()第一个参数是Excel哪个中sheet，第二个参数是sheet名称 //.headRowNumber()是表头行数 //.doRead()开始读取 EasyExcel.read(file.getInputStream(), SStbkSmbsnFncDtlI01ADO.class, new YwtzTyrzmxCwbListener(sStbkSmbsnFncDtlI01AService)).sheet(1, "02表内容").headRowNumber(3).doRead(); return ajaxResult.success("success"); } catch (ExcelAnalysisException e) { e.printStackTrace(); return ajaxResult.fail("500", e.getMessage()); } }实体类package io.github.talelin.latticy.model; import com.alibaba.excel.annotation.ExcelIgnore; import com.alibaba.excel.annotation.ExcelProperty; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; import java.io.Serializable; import java.math.BigDecimal; /** * B_H01_同业融资信息_财务部_补录 * * @author generator@TaleLin * @since 2022-09-05 */ @Data public class SStbkSmbsnFncInfH01ADO implements Serializable { /** * 客户号 */ //强制读取第一个 //这里不建议 index 和 name 同时用，要么一个对象只用index，要么一个对象只用name去匹配 //用名字去匹配，这里需要注意，如果名字重复，会导致只有一个字段读取到数据 @ExcelProperty(index = 0) private String cstNo; /** * 业务编码 */ @ExcelProperty(index = 1) private String bsnEcd; /** * 业务种类 */ @ExcelProperty(index = 2) private String bsnCtlg; /** * 起始日期 */ @ExcelProperty(index = 3) private String begDt; /** * 到期日 */ @ExcelProperty(index = 4) private String exdy; /** * 实际终止日期 */ @ExcelProperty(index = 5) private String actTmdt; /** * 期限 */ @ExcelProperty(index = 6) private String ddln; /** * 利率类型 */ @ExcelProperty(index = 7) private String intrtTp; /** * 实际利率 */ @ExcelProperty(index = 8) private BigDecimal actIntrt; /** * 借贷定价基准类型 */ @ExcelProperty(index = 9) private String dbtcrPrcgBssTp; /** * 基准利率 */ @ExcelProperty(index = 10) private BigDecimal bssIntrt; /** * 计息方式 */ @ExcelProperty(index = 11) private String intarMod; /** * 利率浮动频率 */ @ExcelProperty(index = 12) private String intrtFltFrq; /** * 余额 */ @ExcelProperty(index = 13) private BigDecimal bal; /** * 内部机构号 */ @ExcelProperty(index = 14) private String inrInsno; /** * 币种 */ @ExcelProperty(index = 15) private String ccy; /** * 数据来源 */ @ExcelIgnore private String dtsrc; /** * 业务日期 */ //忽略这个字段 @ExcelIgnore private String parDate; /** * 源系统代码 */ @ExcelIgnore private String srcSysCd; /** * 加载时间戳 */ @ExcelIgnore private String loadTime; /** * 金融机构代码 */ @ExcelProperty(index = 16) private String fncLcnsNo; /** * 交易对手代码 */ @ExcelProperty(index = 17) private String txnOpntCd; /** * 交易对手证件类别 */ @ExcelProperty(index = 18) private String txnOpntCrdtCgy; /** * 资产负债类型 */ @ExcelProperty(index = 19) private String alTp; /** * 余额折人民币 */ @ExcelProperty(index = 20) private BigDecimal balFoldCny; /** * 存款协议代码 */ @ExcelProperty(index = 21) private String depAgrmCd; /** * 缴存准备金方式 */ @ExcelProperty(index = 22) private String depRsrvfndMtd; /** * 合同编码 */ @ExcelProperty(index = 23) private String ctrEcd; }
1、消息应答的方法：Channel.basicAck(用于肯定确认)，RabbitMQ已知道该消息并且成功处理，可以将其丢弃Channel.basicNack(用于否定确认)Channel.basicReject(用于否定确认)，与Channel.basicNack相比少了一个参数，不处理该消息了，直接拒绝，可以将其丢弃了。2、Multiple的解释true表示批量应答channel上未应答的消息，比如channel上有传送tag的消息5,6,7,8,，当前tag是8，那么此时5-8的这些还未应答的消息就会被确认收到消息应答false同上面相比只会应答tag=8的消息，5,6,7这三个消息依然不会被确认收到消息应答手动应答的好处是可以批量应答并且减少网络拥堵 。3、消息手动应答代码//getEnvelope()属性，getDeliveryTag()标记 /* * 参数1：消息的标记 tag * 参数2：是否批量应答，false：不批量应答；true：批量 * */ channel.basicAck(message.getEnvelope().getDeliveryTag(), false); // 采用手动应答 boolean autoAck = false; channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,(consumerTag) -> { System.out.println(consumerTag+"消费者取消消费接口回调逻辑"); });4、持久化// 声明队列 // 持久化 需要让Queue持久化 boolean durable = true; channel.queueDeclare(TASK_QUEUE_NAME,durable,false,false,null);注意：如果之前声明的队列不是持久化的，需要把原先队列先删除，或者重新创建一个持久化的队列，不然就会出现错误。将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘，但是这里依然存在当消息刚准备存储在磁盘的时候但是还没有存储完，消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强，但是对于我们的简单任务队列而言，这已经绰绰有余了。5、不公平分发//不公平分发channel //basicQos(0) --> 轮询分发（公平分发），默认值为0 //channel.basicQos(1) --> 不公平分发，能者多劳 //channel.basicQos(>2) --> 预取值 int prefetchCount = 1; channel.basicQos(prefetchCount); //采用手动应答 boolean autoAck = false; channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, cancelCallback);6、发布确认发布确认的策略:开启发布确认的方法：发布确认默认是没有开启的，如果要开启，需要调用方法 confirmSelect，每当你要想使用发布确认，都需要在 channel 上调用该方法。//开启发布确认 channel.confirmSelect();6.1单个确认发布这是一种简单的确认方式，它是一种同步确认发布的方式，也就是发布一个消息之后只有它被确认发布，后续的消息才能继续发布，waitForConfirmsOrDie(long) 这个方法只有在消息被确认的时候才返回，如果在指定时间范围内这个消息没有被确认那么它将抛出异常。这种确认方式有一个最大的缺点就是：发布速度特别的慢，因为如果没有确认发布的消息就会阻塞所有后续消息的发布，这种方式最多提供每秒不超过数百条发布消息的吞吐量。/** * 单个发送 */ public static void publishMessageIndividually() throws Exception { Channel channel = RabbitMqUtils.getChannel(); //队列声明 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, true, false, false, null); //开启发布确认 channel.confirmSelect(); long begin = System.currentTimeMillis(); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = i + ""; channel.basicPublish("", queueName, null, message.getBytes()); //服务端返回 false 或超时时间内未返回，生产者可以消息重发 //单个消息马上进行发布确认 boolean flag = channel.waitForConfirms(); if (flag) { System.out.println("消息发送成功"); } } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms"); }6.2批量确认发布上面那种方式非常慢，与单个等待确认消息相比，先发布一批消息然后一起确认可以极大地提高吞吐量。缺点就是：当发生故障导致发布出现问题时，不知道是哪个消息出问题了，我们必须将整个批处理保存在内存中，以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的，也一样阻塞消息的发布。/** * 批量确认发送 */ public static void publishMessageBatch() throws Exception { Channel channel = RabbitMqUtils.getChannel(); //队列声明 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, true, false, false, null); //开启发布确认 channel.confirmSelect(); //批量确认消息大小 int batchSize = 100; //未确认消息个数 int outstandingMessageCount = 0; long begin = System.currentTimeMillis(); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = i + ""; channel.basicPublish("", queueName, null, message.getBytes()); outstandingMessageCount++; if (outstandingMessageCount == batchSize) { channel.waitForConfirms(); outstandingMessageCount = 0; } } //为了确保还有剩余没有确认消息 再次确认 if (outstandingMessageCount > 0) { channel.waitForConfirms(); } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) + "ms"); }6.3异步确认发布异步确认虽然编程逻辑比上两个要复杂，但是性价比最高，无论是可靠性还是效率都没得说， 它是利用回调函数来达到消息可靠性传递的，这个中间件也是通过函数回调来保证是否投递成功。/** * 异步确认发布 * */ public static void publishMessageByAsync() throws Exception { Channel channel = RabbitMqUtils.getChannel(); String queueName = UUID.randomUUID().toString(); // 生成一个队列 channel.queueDeclare(queueName, false, false, false, null); // 开启发布确认 channel.confirmSelect(); long begin = System.currentTimeMillis(); // 消息确认成功回调 /** * 参数1。消息的标记 * 参数2.是否为批量确认 */ ConfirmCallback ackCallback = (deliveryTag, multiple) -> { System.out.println(String.format("确认的消息：" + deliveryTag)); }; // 消息确认失败回调 /** * 参数1。消息的标记 * 参数2.是否为批量确认 */ ConfirmCallback nackCallback = (deliveryTag, multiple) -> { System.out.println(String.format("未确认的消息：" + deliveryTag)); }; // 添加异步监听器 监听消息是否成功 /** * 参数1。监听哪些消息成功了 * 参数2.监听哪些消息失败了 */ channel.addConfirmListener(ackCallback, nackCallback); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = i + ""; channel.basicPublish("", queueName, null, message.getBytes()); } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) + "ms"); }处理异步未确认消息：最好的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列， 比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。/** * 异步确认 * 消息确认失败时的处理 * @throws Exception */ public static void publishMessageByAsyncNackCallback() throws Exception { Channel channel = RabbitMqUtils.getChannel(); String queueName = UUID.randomUUID().toString(); // 生成一个队列 channel.queueDeclare(queueName, false, false, false, null); // 开启发布确认 channel.confirmSelect(); /** * 线程安全有序的一个哈希表，适用于高并发的情况下 高并发下的hashMap * 1.轻松的将序号与消息进行关联 * 2.可以轻松的批量的删除条目，只需要给到序号 * 3.支持高并发（多线程） */ ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>(); // 消息确认成功回调 /** * 参数1。消息的标记 * 参数2.是否为批量确认 */ ConfirmCallback ackCallback = (deliveryTag, multiple) -> { //判断是否进行批量操作 if (multiple) { //2.删除已经确认的消息 剩下的就是确认失败的消息 //headMap标记在头部 ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliveryTag); confirmed.clear(); } else { outstandingConfirms.remove(deliveryTag); } System.out.println("确认的消息：" + deliveryTag); }; // 消息确认失败回调 /** * 参数1。消息的标记 * 参数2.是否为批量确认 */ ConfirmCallback nackCallback = (deliveryTag, multiple) -> { //3.打印一下未确认的消息 String message = outstandingConfirms.get(deliveryTag); System.out.println("未确认的消息是：" + message + "，未确认消息的tag是：" + deliveryTag); }; // 添加异步监听器 监听消息是否成功 /** * 参数1。监听哪些消息成功了 * 参数2.监听哪些消息失败了 */ channel.addConfirmListener(ackCallback, nackCallback); //开始时间 long begin = System.currentTimeMillis(); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = i + ""; channel.basicPublish("", queueName, null, message.getBytes()); //1.记录所有要发送的消息 //getNextPublishSeqNo下一次发布的序号 outstandingConfirms.put(channel.getNextPublishSeqNo(), message); } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) + "ms"); }完整代码：package com.youzi.rabbitmq.four; import com.rabbitmq.client.Channel; import com.rabbitmq.client.ConfirmCallback; import com.youzi.rabbitmq.utils.RabbitMqUtils; import java.util.UUID; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; /** * @Author youzi * @ClassName ConfirmMessage * @Date 2022/7/4 10:34 * @Description //TODO */ public class ConfirmMessage { public static final int MESSAGE_COUNT = 1000; public static void main(String[] args) throws Exception { //单个发送 22240ms ConfirmMessage.publishMessageIndividually(); //批量确认 311ms ConfirmMessage.publishMessageBatch(); //异步确认 51ms ConfirmMessage.publishMessageByAsync(); //异步确认加失败处理 49ms ConfirmMessage.publishMessageByAsyncNackCallback(); } /** * 单个发送 * * @throws Exception */ public static void publishMessageIndividually() throws Exception { Channel channel = RabbitMqUtils.getChannel(); String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, true, false, false, null); //开启发布确认 channel.confirmSelect(); long begin = System.currentTimeMillis(); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = i + ""; channel.basicPublish("", queueName, null, message.getBytes()); //服务端返回 false 或超时时间内未返回，生产者可以消息重发 //单个消息马上进行发布确认 boolean flag = channel.waitForConfirms(); if (flag) { System.out.println("消息发送成功"); } } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms"); } /** * 批量确认 * * @throws Exception */ public static void publishMessageBatch() throws Exception { Channel channel = RabbitMqUtils.getChannel(); String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, true, false, false, null); //开启发布确认 channel.confirmSelect(); long begin = System.currentTimeMillis(); //批量确认消息大小 int bachSize = 100; //批量发布消息 批量确认 for (int i = 0; i < MESSAGE_COUNT; i++) { String message = i + ""; channel.basicPublish("", queueName, null, message.getBytes()); //判断达到100条消息的时候，批量确认一次 if (i % bachSize == 0) { //发布确认 channel.waitForConfirms(); } } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) + "ms"); } /** * 异步确认 * * @throws Exception */ public static void publishMessageByAsync() throws Exception { Channel channel = RabbitMqUtils.getChannel(); String queueName = UUID.randomUUID().toString(); // 生成一个队列 channel.queueDeclare(queueName, false, false, false, null); // 开启发布确认 channel.confirmSelect(); long begin = System.currentTimeMillis(); // 消息确认成功回调 /** * 参数1。消息的标记 * 参数2.是否为批量确认 */ ConfirmCallback ackCallback = (deliveryTag, multiple) -> { System.out.println(String.format("确认的消息：" + deliveryTag)); }; // 消息确认失败回调 /** * 参数1。消息的标记 * 参数2.是否为批量确认 */ ConfirmCallback nackCallback = (deliveryTag, multiple) -> { System.out.println(String.format("未确认的消息：" + deliveryTag)); }; // 添加异步监听器 监听消息是否成功 /** * 参数1。监听哪些消息成功了 * 参数2.监听哪些消息失败了 */ channel.addConfirmListener(ackCallback, nackCallback); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = i + ""; channel.basicPublish("", queueName, null, message.getBytes()); } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) + "ms"); } /** * 异步确认 * 消息确认失败时的处理 * @throws Exception */ public static void publishMessageByAsyncNackCallback() throws Exception { Channel channel = RabbitMqUtils.getChannel(); String queueName = UUID.randomUUID().toString(); // 生成一个队列 channel.queueDeclare(queueName, false, false, false, null); // 开启发布确认 channel.confirmSelect(); /** * 线程安全有序的一个哈希表，适用于高并发的情况下 高并发下的hashMap * 1.轻松的将序号与消息进行关联 * 2.可以轻松的批量的删除条目，只需要给到序号 * 3.支持高并发（多线程） */ ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>(); // 消息确认成功回调 /** * 参数1。消息的标记 * 参数2.是否为批量确认 */ ConfirmCallback ackCallback = (deliveryTag, multiple) -> { //判断是否进行批量操作 if (multiple) { //2.删除已经确认的消息 剩下的就是确认失败的消息 //headMap标记在头部 ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliveryTag); confirmed.clear(); } else { outstandingConfirms.remove(deliveryTag); } System.out.println("确认的消息：" + deliveryTag); }; // 消息确认失败回调 /** * 参数1。消息的标记 * 参数2.是否为批量确认 */ ConfirmCallback nackCallback = (deliveryTag, multiple) -> { //3.打印一下未确认的消息 String message = outstandingConfirms.get(deliveryTag); System.out.println("未确认的消息是：" + message + "，未确认消息的tag是：" + deliveryTag); }; // 添加异步监听器 监听消息是否成功 /** * 参数1。监听哪些消息成功了 * 参数2.监听哪些消息失败了 */ channel.addConfirmListener(ackCallback, nackCallback); //开始时间 long begin = System.currentTimeMillis(); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = i + ""; channel.basicPublish("", queueName, null, message.getBytes()); //1.记录所有要发送的消息 //getNextPublishSeqNo下一次发布的序号 outstandingConfirms.put(channel.getNextPublishSeqNo(), message); } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) + "ms"); } }以上三种发布确认对比发布确认方案对比单独发布确认消息同步等待确认，简单，但吞吐量非常有限批量发布确认消息批量同步等待，简单，合理的吞吐量，出现问题无法确认是哪条消息异常异步发布确认消息最佳性能和资源使用，可以异步处理成功和失败的消息7、交换机7.1、Fanout模式（发布/订阅模式）概念：它是将接收到的所有消息广播到它知道的所有队列中。生产者代码：package com.youzi.rabbitmq.five; import com.rabbitmq.client.Channel; import com.youzi.rabbitmq.utils.RabbitMqUtils; import java.util.Scanner; /** * @Author youzi * @ClassName Emitlog * @Date 2022/7/4 16:15 * @Description //TODO */ public class Emitlog { //交换机名称 public static final String EXCHANGE_NAME = "logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明一个交换机 channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String message = scanner.next(); channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8")); System.out.println("生产者发出消息：" + message); } } }消费者1代码：package com.youzi.rabbitmq.five; import com.rabbitmq.client.Channel; import com.rabbitmq.client.ConfirmCallback; import com.rabbitmq.client.DeliverCallback; import com.youzi.rabbitmq.utils.RabbitMqUtils; import com.youzi.rabbitmq.utils.SleepUtils; /** * @Author youzi * @ClassName ReceiveLogs01 * @Date 2022/7/4 16:15 * @Description //TODO 消费者1 */ public class ReceiveLogs01 { //交换机名称 public static final String EXCHANGE_NAME = "logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明一个交换机 channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); //声明一个临时队列 /** * 生成一个临时队列,队列的名称是随机的 * 当消费者断开与队列的连接时，队列就自动删除了 */ String queueName = channel.queueDeclare().getQueue(); //绑定交换机与队列 /** * 参数1.队列名称 * 参数2.交换机名称 * 参数3.routingKey值 */ channel.queueBind(queueName, EXCHANGE_NAME, ""); System.out.println("等待接收消息，把接收到的消息放在屏幕上。。。。。。"); DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println("ReceiveLogs01接收到的消息：" + new String(message.getBody())); }; channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { }); } }消费者2代码：package com.youzi.rabbitmq.five; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; import com.youzi.rabbitmq.utils.RabbitMqUtils; /** * @Author youzi * @ClassName ReceiveLogs02 * @Date 2022/7/4 16:15 * @Description //TODO */ public class ReceiveLogs02 { //交换机名称 public static final String EXCHANGE_NAME = "logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明一个交换机 channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); //声明一个临时队列 /** * 生成一个临时队列,队列的名称是随机的 * 当消费者断开与队列的连接时，队列就自动删除了 */ String queueName = channel.queueDeclare().getQueue(); //绑定交换机与队列 /** * 参数1.队列名称 * 参数2.交换机名称 * 参数3.routingKey值 */ channel.queueBind(queueName, EXCHANGE_NAME, ""); System.out.println("等待接收消息，把接收到的消息放在屏幕上。。。。。。"); DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println("ReceiveLogs02接收到的消息：" + new String(message.getBody())); }; channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { }); } } 7.2、Direct 模式（路由模式）概念：消息只去到它绑定的 routingKey 队列中去。生产者代码：package com.youzi.rabbitmq.six; import com.rabbitmq.client.Channel; import com.youzi.rabbitmq.utils.RabbitMqUtils; import java.util.Scanner; /** * @Author youzi * @ClassName DirectLogs * @Date 2022/7/4 17:23 * @Description //TODO */ public class DirectLogs { private static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { String message = scanner.next(); //第二个参数可以填入info，warning，error，填入哪个就是哪个消费者接收 channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8")); System.out.println("生产者发出消息：" + message); } } }消费者1代码：package com.youzi.rabbitmq.six; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; import com.youzi.rabbitmq.utils.RabbitMqUtils; /** * @Author youzi * @ClassName ReceiveLogsDirect01 * @Date 2022/7/4 17:22 * @Description //TODO */ public class ReceiveLogsDirect01 { private static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //队列声明 channel.queueDeclare("console", false, false, false, null); //队列绑定 channel.queueBind("console", EXCHANGE_NAME, "info"); channel.queueBind("console", EXCHANGE_NAME, "warning"); //发送回调 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println("ReceiveLogsDirect01接收到的消息：" + new String(message.getBody())); }; channel.basicConsume("console", true, deliverCallback, consumerTag -> { }); } }消费者2代码：package com.youzi.rabbitmq.six; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; import com.youzi.rabbitmq.utils.RabbitMqUtils; /** * @Author youzi * @ClassName ReceiveLogsDirect02 * @Date 2022/7/4 17:22 * @Description //TODO */ public class ReceiveLogsDirect02 { private static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //队列声明 channel.queueDeclare("disk", false, false, false, null); //队列绑定 channel.queueBind("disk", EXCHANGE_NAME, "error"); //发送回调 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println("ReceiveLogsDirect02接收到的消息：" + new String(message.getBody())); }; channel.basicConsume("disk", true, deliverCallback, consumerTag -> { }); } }7.3、Topic 模式（主题模式）主题交换机是通过匹配路由键进行消息的转发，对于key匹配上的，转发相应队列，全都不匹配的直接丢弃；规则：所有的单词均是由 . 分隔；可以匹配任意一个单词；可以匹配0个或多个单词；注意：当一个队列的routeKey是#，那么这个队列将接收所有消息，类似于FANOUT;当一个队列的routeKey没有包含#和*,那么这个队列类似于DIRECT;routeKey匹配*.hello.*匹配三个单词，且中间的必须是hellowelcome.*匹配两个单词，第一个必须是welcomehello.world匹配两个单词，第一个是hello，第二个是world#.happy匹配至少一个单词，且最后一个单词必须是happyhappy.*.#匹配至少两个单词，第一个必须是happy生产者代码：package com.youzi.rabbitmq.seven; import com.rabbitmq.client.Channel; import com.youzi.rabbitmq.utils.RabbitMqUtils; import java.util.HashMap; import java.util.Map; public class EmitLogTopic { //交换机的名称 public static final String EXCHANGE_NAME = "topic_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * Q1-->绑定的是 * 中间带 orange 带 3 个单词的字符串(*.orange.*) * Q2-->绑定的是 * 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit) * 第一个单词是 lazy 的多个单词(lazy.#) */ HashMap<String, String> bindingKeyMap = new HashMap<>(); bindingKeyMap.put("quick.orange.rabbit", "被队列 Q1Q2 接收到"); bindingKeyMap.put("lazy.orange.elephant", "被队列 Q1Q2 接收到"); bindingKeyMap.put("quick.orange.fox", "被队列 Q1 接收到"); bindingKeyMap.put("lazy.brown.fox", "被队列 Q2 接收到"); bindingKeyMap.put("lazy.pink.rabbit", "虽然满足两个绑定但只被队列 Q2 接收一次"); bindingKeyMap.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃"); bindingKeyMap.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃"); bindingKeyMap.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2"); for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) { String bindingKey = bindingKeyEntry.getKey(); String message = bindingKeyEntry.getValue(); channel.basicPublish(EXCHANGE_NAME, bindingKey, null, message.getBytes("UTF-8")); System.out.println("生产者发出消息：" + message); } } }消费者代码1：package com.youzi.rabbitmq.seven; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; import com.youzi.rabbitmq.utils.RabbitMqUtils; /** * @Author youzi * @ClassName ReceiveLogsTopic01 * @Date 2022/7/5 14:11 * @Description //TODO */ public class ReceiveLogsTopic01 { //交换机名称 private static final String EXCHANGE_NAME = "topic_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明交换机 channel.exchangeDeclare(EXCHANGE_NAME, "topic"); //声明队列 String queueName = "Q1"; channel.queueDeclare(queueName, false, false, false, null); channel.queueBind(queueName, EXCHANGE_NAME, "*.orange.*"); System.out.println("等待接收消息...."); //发送回调 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println("ReceiveLogsTopic01接收到的消息：" + new String(message.getBody())); System.out.println("接收队列：" + queueName + "绑定的键：" + message.getEnvelope().getRoutingKey()); }; channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { }); } }消费者代码2：package com.youzi.rabbitmq.seven; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; import com.youzi.rabbitmq.utils.RabbitMqUtils; /** * @Author youzi * @ClassName ReceiveLogsTopic01 * @Date 2022/7/5 14:11 * @Description //TODO */ public class ReceiveLogsTopic02 { //交换机名称 private static final String EXCHANGE_NAME = "topic_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明交换机 channel.exchangeDeclare(EXCHANGE_NAME, "topic"); //声明队列 String queueName = "Q2"; channel.queueDeclare(queueName, false, false, false, null); channel.queueBind(queueName, EXCHANGE_NAME, "*.*.rabbit"); channel.queueBind(queueName, EXCHANGE_NAME, "lazy.#"); System.out.println("等待接收消息...."); //发送回调 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println("ReceiveLogsTopic02接收到的消息：" + new String(message.getBody())); System.out.println("接收队列：" + queueName + "绑定的键：" + message.getEnvelope().getRoutingKey()); }; channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { }); } }8、死信队列死信就是无法被消费的消息；当生产者把消息投递到broker或者直接到队列中，消费者从队列中取出消息进行消费，但由于特殊情况，队列中的个别消息无法被消费，这样的消息如果没有后续的处理，就会变成死信，存放在死信队列中；通常进入死信队列的消息要么是异常消息，要么是超时消息；死信队列的来源：消息TTL时间过期；队列达到最大长度，无法添加新的消息到mq中；消息被拒绝（basic.reject或者nack）并且requeue=false;8.1、消息TTL时间过期场景模拟：声明两个直接交换机和队列，逻辑交换机绑定逻辑队列，死信交换机绑定死信队列，然后生产者发送消息到逻辑交换机，由于逻辑交换机接收超时，导致消息进入死信队列，被消费者2处理；声明TTL时间过期在Consumer中可以queueDeclare队列声明处增加参数map，使用keyx-message-ttl设置；（不推荐）在生产者发送消息的时候，通过BasicProperties设置发送消息的TTL，推荐使用，不同消息可以设置不同的消息TTL；生产者代码：package com.youzi.rabbitmq.eight; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; import com.youzi.rabbitmq.utils.RabbitMqUtils; /** * @Author youzi * @ClassName Producer * @Date 2022/7/11 14:27 * @Description //TODO */ public class Producer { //正常的交换机 public static final String NORMAL_EXCHANGE = "normal_exchange"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //死信消息 设置TTL时间（过期时间） 单位是ms AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build(); //发送消息 for (int i = 1; i < 11; i++) { String message = "info" + i; channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties, message.getBytes()); } } }消费者1代码：package com.youzi.rabbitmq.eight; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; import com.youzi.rabbitmq.utils.RabbitMqUtils; import java.util.HashMap; import java.util.Map; /** * @Author youzi * @ClassName Consumer01 * @Date 2022/7/11 10:53 * @Description //TODO 死信队列 */ public class Consumer01 { //正常的交换机 public static final String NORMAL_EXCHANGE = "normal_exchange"; //死信的交换机 public static final String DEAD_EXCHANGE = "dead_exchange"; //正常的队列 public static final String NORMAL_QUEUE = "normal_queue"; //死信的交换机 public static final String DEAD_QUEUE = "dead_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明正常和死信交换机 channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT); channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT); //声明正常队列 Map<String, Object> arg = new HashMap<>(); //过期时间 单位ms 10s = 10000 //(两个地方可以设置，建议在生产者设置，因为消费者这边设置的是固定值，不可更改，消费者可以设置每条消息的过期时间) arg.put("x-message-ttl", 10000); //正常交换机设置死信交换机 arg.put("x-dead-letter-exchange", DEAD_EXCHANGE); //设置死信RoutingKey arg.put("x-dead-letter-routing-key", "lisi"); channel.queueDeclare(NORMAL_QUEUE, false, false, false, arg); //声明死信队列 channel.queueDeclare(DEAD_QUEUE, false, false, false, null); //普通队列绑定普通交换机 channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan"); //死信队列绑定死信交换机 channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi"); System.out.println("等待接收消息....."); //发送回调 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println("Consumer01接受的消息是：" + new String(message.getBody())); }; channel.basicConsume(NORMAL_QUEUE, true, deliverCallback, consumerTag -> { }); } }消费者2代码：package com.youzi.rabbitmq.eight; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; import com.youzi.rabbitmq.utils.RabbitMqUtils; import java.util.HashMap; import java.util.Map; /** * @Author youzi * @ClassName Consumer01 * @Date 2022/7/11 10:53 * @Description //TODO 死信队列 */ public class Consumer02 { //死信的交换机 public static final String DEAD_QUEUE = "dead_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); System.out.println("等待接收消息....."); //发送回调 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println("Consumer02接受的消息是：" + new String(message.getBody())); }; channel.basicConsume(DEAD_QUEUE, true, deliverCallback, consumerTag -> { }); } }8.2、队列达到最大长度，无法添加新的消息到MQ操作：在声明logic逻辑队列的参数中，增加key值为x-max-length；标识队列最大消息长度；先要把原来的normal_queue队列删除，否则启动会报错，代码与上面的差不多，只需要把生产者的超时时间删除// 在消费者1的代码中，参数map增加设置超时时间的key即可 // 声明队列最大长度6条消息 arg.put("x-max-length", 6);8.3、消息被拒绝消费者1需要手动确认消息，把确认消息设置成reject；则该消息进入死信队列；消费者1代码：package com.youzi.rabbitmq.eight; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; import com.youzi.rabbitmq.utils.RabbitMqUtils; import java.util.HashMap; import java.util.Map; /** * @Author youzi * @ClassName Consumer01 * @Date 2022/7/11 10:53 * @Description //TODO 死信队列 */ public class Consumer01 { //正常的交换机 public static final String NORMAL_EXCHANGE = "normal_exchange"; //死信的交换机 public static final String DEAD_EXCHANGE = "dead_exchange"; //正常的队列 public static final String NORMAL_QUEUE = "normal_queue"; //死信的交换机 public static final String DEAD_QUEUE = "dead_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明正常和死信交换机 channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT); channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT); //声明正常队列 Map<String, Object> arg = new HashMap<>(); //过期时间 单位ms 10s = 10000 //(两个地方可以设置，建议在生产者设置，因为消费者这边设置的是固定值，不可更改，消费者可以设置每条消息的过期时间) //arg.put("x-message-ttl", 10000); //正常交换机设置死信交换机 arg.put("x-dead-letter-exchange", DEAD_EXCHANGE); //设置死信RoutingKey arg.put("x-dead-letter-routing-key", "lisi"); //声明队列最大长度6条消息 //arg.put("x-max-length", 6); channel.queueDeclare(NORMAL_QUEUE, false, false, false, arg); //声明死信队列 channel.queueDeclare(DEAD_QUEUE, false, false, false, null); //普通队列绑定普通交换机 channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan"); //死信队列绑定死信交换机 channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi"); System.out.println("等待接收消息....."); //发送回调 DeliverCallback deliverCallback = (consumerTag, message) -> { String msg = new String(message.getBody()); if (msg.equals("info5")) { System.out.println("Consumer01接受的消息是：" + msg + ":此消息是被C1拒绝的"); //false表示不塞回队列，即进入死信队列 channel.basicReject(message.getEnvelope().getDeliveryTag(), false); } else { System.out.println("Consumer01接受的消息是：" + msg); //false指的是不批量应答 channel.basicAck(message.getEnvelope().getDeliveryTag(), false); } }; //开启手动应答 channel.basicConsume(NORMAL_QUEUE, false, deliverCallback, consumerTag -> { }); } }消费者2和生产者代码不变
1.Docker安装RabbitMQ1.1下载RabbitMQ镜像docker pull rabbitmq1.2创建并启动RabbitMQ容器docker run -id --hostname myrabbit --name rabbitmq -p 15672:15672 -p 5672:5672 rabbitmq1.3进入容器交互页面docker exec -it rabbitmq /bin/bash1.4下载插件rabbitmq-plugins enable rabbitmq_management1.5登录ip+端口号访问账号密码均为：guest1.6拓展# 创建账号和密码 rabbitmqctl add_user 用户名 密码 # 设置用户角色 rabbitmqctl set_user_tags admin administrator # 为用户添加资源权限 # set_permissions [-p <vhostpath>] <user> <conf> <write> <read> # 添加配置、写、读权限 rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"用户级别：administrator：可以登录控制台、查看所有信息、可以对 rabbitmq 进行管理。monitoring：监控者 登录控制台，查看所有信息。policymaker：策略制定者 登录控制台，指定策略。managment：普通管理员 登录控制台。注：有可能会出现下面这个问题访问Docker的rabbitMq容器的Web-UI界面无法查看消息的状态及出现Stats in management UI are disabled on this node解决办法：进入RabbitMQ容器docker exec -it rabbitmq /bin/bash进入容器后，cd到路径/etc/rabbitmq/conf.d/cd /etc/rabbitmq/conf.d/再修改management_agent.disable_metrics_collectorecho management_agent.disable_metrics_collector = false > management_agent.disable_metrics_collector.conf退出容器exit输入指令docker exec -it rabbitmq rabbitmq-plugins enable rabbitmq_management重启RabbitMQdocker restart rabbitmq2.简单使用2.1pom.xml文件添加依赖(本环境是jdk1.8的)<dependencies> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.8.0</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency> </dependencies>2.2新建Producer类为生产者package com.youzi.rabbitmq.one; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @Author youzi * @ClassName Producer * @Date 2022/6/30 15:41 * @Description //TODO 生产者 */ public class Producer { //设置队列名称 public static final String QUEUE_NAME = "hello"; public static void main(String[] args) throws IOException, TimeoutException { //1.创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2.设置IP factory.setHost("ip地址"); //3.设置用户名密码 factory.setUsername("用户名"); factory.setPassword("密码"); //4.创建连接 Connection connection = factory.newConnection(); //5.获取信道 Channel channel = connection.createChannel(); /** * 参数1.队列名称 * 参数2.队列里面的消息是否持久化，默认是不持久化 * 参数3.该队列是否只供一个消费之进行消费，是否进行消息共享，true表示可以多个消费者消费 * 参数4.是否自动删除，最后一个消费者断开连接以后，该队列是否自动删除，true表示自动删除 * 参数5.其他参数 */ //生成一个队列 channel.queueDeclare(QUEUE_NAME,false,false,false,null); //消息 String message = "hello youzi"; /** * 参数1.交换机名称 * 参数2.路由的key值，队列名称 * 参数3.其他参数 * 参数4.消息体 */ channel.basicPublish("",QUEUE_NAME,null,message.getBytes()); System.out.println("消息发送成功"); channel.close(); connection.close(); } }2.3新建Consumer类为消费者package com.youzi.rabbitmq.one; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @Author youzi * @ClassName Consumer * @Date 2022/6/30 16:44 * @Description //TODO 消费者 */ public class Consumer { //设置队列名称 一定要和生产者的一样才能找到对应的关系 public static final String QUEUE_NAME = "hello"; public static void main(String[] args) throws IOException, TimeoutException { //1.创建连接工厂 ConnectionFactory factory = new ConnectionFactory(); //2.设置IP factory.setHost("IP地址"); //3.设置用户名密码 factory.setUsername("用户名"); factory.setPassword("密码"); //4.创建连接 Connection connection = factory.newConnection(); //5.获取信道 Channel channel = connection.createChannel(); //申明回调方法 未成功消费 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println(new String(message.getBody())); }; //申明回调方法 取消消费 CancelCallback cancelCallback = consumerTag -> { System.out.println("消费中断"); }; /** * 消费者接收消息 * 参数1.队列名称 * 参数2.消费成功之后是否要自动应答，true表示自动应答 * 参数3.消费者未成功消费的回调方法 * 参数4.消费者取消消费时的回调方法 */ channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback); } }2.4访问RabbitMQ的后台管理页面
首先<dependencies> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.10.1</version> </dependency> </dependencies>其次import org.joda.time.Interval; import org.joda.time.Period; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @Author youzi * @ClassName TestDateUtil * startDate 开始时间 * endDate 结束时间 * dateFormat 时间格式（形如 yyyy-MM-dd） * @return 相差的月份数和天数 months 、 days * @throws ParseException * @Date 2022/6/22 14:02 * @Description 计算两个时间差 */ public class TestDateUtil { public static void main(String[] args) throws ParseException { DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); Date dateStart = df.parse("2022-03-04"); Date dateEnd = df.parse("2022-06-03"); if (dateStart.getTime() > dateEnd.getTime()) { Date temp = dateStart; dateStart = dateEnd; dateEnd = temp; } Calendar c = Calendar.getInstance(); c.setTime(dateEnd); c.add(Calendar.DAY_OF_MONTH, 1); //这是明天 dateEnd = c.getTime(); Interval interval = new Interval(dateStart.getTime(), dateEnd.getTime()); Period p = interval.toPeriod(); Integer years = p.getYears(); Integer months = p.getMonths(); Integer weeks = p.getWeeks(); Integer days = p.getDays(); Integer resultMonths = years * 12 + months; Integer resultDays = weeks * 7 + days; Map<String, Integer> result = new HashMap<>(); result.put("months", resultMonths); result.put("days", resultDays); System.out.println(result); } }
重载（Overload）和重写（Override）的区别方法的重载和重写都是实现多态的方式，区别在于前者实现的是编译时的多态性，而后者实现的是运行时的多态性。重载：一个类中有多个同名的方法，但是具有有不同的参数列表（参数类型不同、参数个数不同或者二者都不同）。重写：发生在子类与父类之间，子类对父类的方法进行重写，参数都不能改变，返回值类型可以不相同，但是必须是父类返回值的派生类。即外壳不变，核心重写！重写的好处在于子类可以根据需要，定义特定于自己的行为。注：重载的概念是：方法名称相同，参数个数、次序、类型不同因此重载对返回值没有要求，可以相同，也可以不同，但是如果参数的个数、类型、次序都相同，方法名也相同，仅返回值不同，则无法构成重载如：//这2个方法不能构成重载，会有编译错误。 public int A(int i); public double A(int i); //这2个方法可以形成重载 public int A(int i); public double A(double i);不能根据返回类型来区分重载的原因：如果我们有两个方法如下，当我们调用：test(1) 时，编译器无法确认要调用的是哪个。方法的返回值只是作为方法运行之后的一个“状态”，但是并不是所有调用都关注返回值，所以不能将返回值作为重载的唯一区分条件。// 方法1 int test(int a); // 方法2 long test(int a);抽象类（abstract class）和接口（interface）的区别抽象类只能单继承，接口可以多实现。抽象类可以有构造方法，接口中不能有构造方法。抽象类中可以有成员变量，接口中没有成员变量，只能有常量（默认就是 public static final）。抽象类中可以包含非抽象的方法，在 Java 7 之前接口中的所有方法都是抽象的，在 Java 8 之后，接口支持非抽象方法：default 方法、静态方法等。Java 9 支持私有方法、私有静态方法。抽象类中的方法类型可以是任意修饰符，Java 8 之前接口中的方法只能是 public 类型，Java 9 支持 private 类型。设计思想的区别：接口是自上而下的抽象过程，接口规范了某些行为，是对某一行为的抽象。我需要这个行为，我就去实现某个接口，但是具体这个行为怎么实现，完全由自己决定。抽象类是自下而上的抽象过程，抽象类提供了通用实现，是对某一类事物的抽象。我们在写实现类的时候，发现某些实现类具有几乎相同的实现，因此我们将这些相同的实现抽取出来成为抽象类，然后如果有一些差异点，则可以提供抽象方法来支持自定义实现。打个比方：普通类像亲爹 ，他有啥都是你的。抽象类像叔伯，有一部分会给你，还能指导你做事的方法。接口像干爹，可以给你指引方法，但是做成啥样得你自己努力实现。wait() 和 sleep() 方法的区别来源不同：sleep() 来自 Thread 类，wait() 来自 Object 类。对于同步锁的影响不同：sleep() 不会该表同步锁的行为，如果当前线程持有同步锁，那么 sleep 是不会让线程释放同步锁的。wait() 会释放同步锁，让其他线程进入 synchronized 代码块执行。使用范围不同：sleep() 可以在任何地方使用。wait() 只能在同步控制方法或者同步控制块里面使用，否则会抛 IllegalMonitorStateException。恢复方式不同：两者会暂停当前线程，但是在恢复上不太一样。sleep() 在时间到了之后会重新恢复；wait() 则需要其他线程调用同一对象的 notify()、nofityAll() 才能重新恢复。为什么要使用线程池？为什么不直接new个线程？如果我们在方法中直接new一个线程来处理，当这个方法被调用频繁时就会创建很多线程，不仅会消耗系统资源，还会降低系统的稳定性，一不小心把系统搞崩了，就可以直接去财务那结帐了。如果我们合理的使用线程池，则可以避免把系统搞崩的窘境。总得来说，使用线程池可以带来以下几个好处：降低资源消耗。通过重复利用已创建的线程，降低线程创建和销毁造成的消耗。提高响应速度。当任务到达时，任务可以不需要等到线程创建就能立即执行。增加线程的可管理型。线程是稀缺资源，使用线程池可以进行统一分配，调优和监控。List、Set、MapHashMap 和Hashtable 的区别HashMap 允许 key 和 value 为 null，Hashtable 不允许。HashMap 的默认初始容量为 16，Hashtable 为 11。HashMap 的扩容为原来的 2 倍，Hashtable 的扩容为原来的 2 倍加 1。HashMap 是非线程安全的，Hashtable是线程安全的。HashMap 的 hash 值重新计算过，Hashtable 直接使用 hashCode。HashMap 去掉了 Hashtable 中的 contains 方法。HashMap 继承自 AbstractMap 类，Hashtable 继承自 Dictionary 类。类加载的过程类加载的过程包括：加载、验证、准备、解析、初始化，其中验证、准备、解析统称为连接。加载：通过一个类的全限定名来获取定义此类的二进制字节流，在内存中生成一个代表这个类的java.lang.Class对象。验证：确保Class文件的字节流中包含的信息符合当前虚拟机的要求，并且不会危害虚拟机自身的安全。准备：为静态变量分配内存并设置静态变量初始值，这里所说的初始值“通常情况”下是数据类型的零值。解析：将常量池内的符号引用替换为直接引用。初始化：到了初始化阶段，才真正开始执行类中定义的 Java 初始化程序代码。主要是静态变量赋值动作和静态语句块（static{}）中的语句。饿汉模式、懒汉模式和静态内部类（延迟初始化占位类）饿汉://线程安全、非懒加载、效率高。 public class Singleton { // 1.饿汉 private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }饿汉模式，比较常见的一种写法。在类加载的时候就对实例进行初始化，没有线程安全问题；获取实例的静态方法没有使用同步，调用效率高；但是没有使用懒加载，如果该实例从始至终都没被使用过，则会造成内存浪费。懒汉://线程安全、懒加载、效率低。 public class Singleton { // 2.懒汉 private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }线程安全的懒汉模式，比较常见的一种写法。在第一次使用的时候才进行初始化，达到了懒加载的效果；由于获取实例的静态方法用synchronized修饰，所以也没有线程安全的问题；但是，这种写法每次获取实例都要进行同步（加锁），因此效率较低，并且可能很多同步都是没必要的。注：该模式还有另一种常见写法，就是把getInstance方法上的synchronized去掉，这种方法有线程安全问题，不能使用。静态内部类(延迟初始化占位类)://线程安全、懒加载、效率高。 public class Singleton { // 4.静态内部类 private static class Holder { private static final Singleton INSTANCE = new Singleton(); } private Singleton() {} public static final Singleton getInstance() { return Holder.INSTANCE; } }静态内部类（延迟初始化占位类），比较常见的一种写法。JVM将推迟SingletonHolder的初始化操作，直到开始使用这个类时才初始化，并且由于通过一个静态初始化来初始化Singleton，因此不需要额外的同步。当任何一个线程第一次调用getInstance时，都会使SingletonHolder被加载和被初始化，此时静态初始化器将执行Singleton的初始化操作。排序算法冒泡排序:public static void bubbleSort(int[] array) { if (array == null || array.length == 0) { return; } for (int i = 0; i < array.length - 1; i++) { for (int j = 0; j < array.length - i - 1; j++) { if (array[j] > array[j + 1]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; } } } }时间复杂度:在通常情况下。冒泡排序的比较次数 = (n - 1) + (n - 2) + ... + 2 + 1，即：n * (n - 1) / 2，所以冒泡排序的时间复杂度为：O(n^2)。插入排序public class InsertSort { public static void insertSort(int array[]) { if (array == null || array.length == 0) { return; } for (int i = 1; i < array.length; i++) { // 当array[i]比arra[i - 1]小时才进入处理 if (array[i] < array[i - 1]) { // 临时变量存储array[i]的值 int temp = array[i]; // 从当前位置开始处理 int j = i; // 将比temp大的数往后挪一个位置，为temp腾出一个合适位置 while (j > 0 && temp < array[j - 1]) { array[j] = array[j - 1]; j--; // 填充完后, 继续向前比较 } // 将temp放在属于它的位置上 array[j] = temp; } } } }时间复杂度:在最坏的情况下，即整个数组是倒序的，比较次数 = 1 + 2 + 3 + ... + (n - 2) + (n - 1) = n * (n - 1) / 2，此时的时间复杂度为：O(n^2)。在最好的情况下，即整个数组是正序的，比较次数 = n - 1，此时的时间复杂度为：O(n)。选择排序public class SelectSort { public static void selectSort(int[] array) { if (array == null || array.length == 0) { return; } for (int i = 0; i < array.length - 1; i++) { // 记录当次遍历值最小的数的位置 int minIndex = i; for (int j = i + 1; j < array.length; j++) { // 如果array[j]比array[minIndex]小, 则将minIndex修改为j if (array[j] < array[minIndex]) { minIndex = j; } } if (i != minIndex) { // 将当次遍历的最小值交换到对应位置 int temp = array[i]; array[i] = array[minIndex]; array[minIndex] = temp; } } } }时间复杂度:如果将数组的长度看作n，第一次循环时，j 从1开始直到n - 1，因此比较次数为n - 1；第二次为n - 2，依此类推。最终得到选择排序的比较次数 = (n - 1) + (n - 2) + ... + 2 + 1 = n * (n - 1) / 2，因此时间复杂度为：O(n^2)。快速排序public class QuickSort { private static void quickSort(int[] array, int low, int high) { if (low >= high) { return; } int i = low, j = high, index = array[i]; // 取最左边的数作为基准数 while (i < j) { while (i < j && array[j] >= index) { // 向左寻找第一个小于index的数 j--; } if (i < j) { array[i++] = array[j]; // 将array[j]填入array[i]，并将i向右移动 } while (i < j && array[i] < index) {// 向右寻找第一个大于index的数 i++; } if (i < j) { array[j--] = array[i]; // 将array[i]填入array[j]，并将j向左移动 } } array[i] = index; // 将基准数填入最后的坑 quickSort(array, low, i - 1); // 递归调用，分治 quickSort(array, i + 1, high); // 递归调用，分治 } public static void quickSort(int[] array) { if (array == null || array.length == 0) { return; } quickSort(array, 0, array.length - 1); } }时间复杂度:最好情况的时间复杂度为O(nlogn)。最差情况下每一次取到的数（基准数）都是当前要比较的数中的最大/最小值，在这种情况下，每次都只能得到比上一次少1个数的子序列（即要么全比基准数大，要么全比基准小），此时相当于一个冒泡排序，比较的次数 = (n - 1) + (n - 2) + ... + 2 + 1 = (n - 1) * n / 2，此时的时间复杂度为：O(n^2)。最差情况一般出现在：待排序的数据本身已经是正序或反序排好了。归并排序public class MergeSort { public static void mergeSort(int[] array) { if (array == null || array.length == 0) return; int[] temp = new int[array.length]; mergeSort(array, 0, array.length - 1, temp); } // 归并 private static void mergeSort(int array[], int first, int last, int temp[]) { if (first < last) { int mid = (first + last) / 2; mergeSort(array, first, mid, temp); // 递归归并左边元素 mergeSort(array, mid + 1, last, temp); // 递归归并右边元素 mergeArray(array, first, mid, last, temp); // 再将二个有序数列合并 } } /** * 合并两个有序数列 * array[first]~array[mid]为第一组 * array[mid+1]~array[last]为第二组 * temp[]为存放两组比较结果的临时数组 */ private static void mergeArray(int array[], int first, int mid, int last, int temp[]) { int i = first, j = mid + 1; // i为第一组的起点, j为第二组的起点 int m = mid, n = last; // m为第一组的终点, n为第二组的终点 int k = 0; // k用于指向temp数组当前放到哪个位置 while (i <= m && j <= n) { // 将两个有序序列循环比较, 填入数组temp if (array[i] <= array[j]) temp[k++] = array[i++]; else temp[k++] = array[j++]; } while (i <= m) { // 如果比较完毕, 第一组还有数剩下, 则全部填入temp temp[k++] = array[i++]; } while (j <= n) {// 如果比较完毕, 第二组还有数剩下, 则全部填入temp temp[k++] = array[j++]; } for (i = 0; i < k; i++) {// 将排好序的数填回到array数组的对应位置 array[first + i] = temp[i]; } } }时间复杂度:归并排序的时间复杂度为O(nlogn)。
Spring AOP 中的动态代理主要有两种方式，JDK 动态代理和 CGLIB 动态代理：JDK动态代理只提供接口的代理，不支持类的代理。核心 InvocationHandler接口和Proxy类，InvocationHandler 通过 invoke()方法反射来调用目标类中的代码，动态地将横切逻辑和业务编织在一起；接着，Proxy 利用 InvocationHandler 动态创建 一个符合某一接口的的实例, 生成目标类的代理对象。如果代理类没有实现 InvocationHandler 接口，那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB（Code Generation Library），是一个代码生成的类库，可以在运行时动态的生成指定类的一个子类对象，并覆盖其中特定方法并添加增强代码，从而实现 AOP。CGLIB 是通过继承的方式做的动态代理，因此如果某个类被标记为 final，那么它是无法使用 CGLIB 做动态代理的。静态代理与动态代理区别在于生成 AOP 代理对象的时机不同，相对来说 AspectJ 的静态代理方式具有更好的性能，但是 AspectJ 需要特定的编译器进行处理，而 Spring AOP 则无 需特定的编译器处理。注：InvocationHandler 的 invoke(Object proxy,Method method,Object[] args)：proxy 是最终生成的代理实例; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
AOP实现的关键在于代理模式，AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ AOP；动态代理则以Spring AOP为代表。AspectJ AOP使用的静态代理的增强，所谓静态代理，就是AOP框架会在编译阶段生成AOP代理类，因此也称为编译时增强，他会在编译阶段将AspectJ(切面)织入到Java字节码中， 运行的时候就是增强之后的AOP对象。Spring AOP 使用的是动态代理，所谓的动态代理就是说AOP框架不会去修改字节码，而是每次运行时在内存中临时为方法生成一个AOP对象，这个AOP对象包含了目标对象的全部方法，并且在特定的切点做了增强处理，并回调原对象的方法。