Spring-boot-整合-Elasticsearch

1. 概述前面学习了 Elasticsearch 的简单基本操作,例如安装,基本的操作命令等,今天就来看看 es 和 Spring boot 的简单整合,实现增删改查的功能。众所周知,Spring boot 支持多种 NoSql 数据库,例如 redis、mongodb,elasticsearch 也是其中的一种。并且实现了 Spring boot 一贯的自动化配置,使用起来也是十分方便的。 2. 新建项目新建一个 spring boot 项目,在 NoSql 这一栏选中 Elasticsearch 。 然后在配置文件中加上 es 的配置: spring: data: elasticsearch: cluster-nodes: 192.168.66.135:9300 repositories: enabled: true cluster-name: elasticsearch注意这里使用的是集群的 9300 端口,而不是 es 默认的 9200 端口。cluster-name 默认就是 elasticsearch,不写也可以的。 3. 简单操作项目建好之后,可以来试试简单的操作。只要你使用过 Spring Data Jpa,那么理解起来就非常的容易了,因为用法都是类似的。 1.首先需要新建一个实体类,表示这种实体类的数据,做为 es 的文档存放。 @Data@Builder@NoArgsConstructor@AllArgsConstructor@Document(indexName = "product", type = "computer")public class Product { private String id; private String name; private double price; private String brand; private String color;}@Document 注解上面可以指定 index 以及 type 的名称。 ...

April 30, 2019 · 1 min · jiezi

APOLLO配置中心

MySQLApollo的表结构对timestamp使用了多个default声明,所以需要5.6.5以上版本。Apollo服务端共需要两个数据库:ApolloPortalDB和ApolloConfigDB ApolloConfigDB: /* Navicat Premium Data Transfer Source Server : mylocal Source Server Type : MySQL Source Server Version : 50724 Source Host : localhost:3306 Source Schema : ApolloConfigDB Target Server Type : MySQL Target Server Version : 50724 File Encoding : 65001 Date: 30/04/2019 14:07:01*/SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for App-- ----------------------------DROP TABLE IF EXISTS `App`;CREATE TABLE `App` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `Name` varchar(500) NOT NULL DEFAULT 'default' COMMENT '应用名', `OrgId` varchar(32) NOT NULL DEFAULT 'default' COMMENT '部门Id', `OrgName` varchar(64) NOT NULL DEFAULT 'default' COMMENT '部门名字', `OwnerName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerName', `OwnerEmail` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerEmail', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId` (`AppId`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Name` (`Name`(191))) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='应用表';-- ------------------------------ Records of App-- ----------------------------BEGIN;INSERT INTO `App` VALUES (1, 'SampleApp', 'Sample App', 'TEST1', '样例部门1', 'apollo', 'apollo@acme.com', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `App` VALUES (2, 'myapp', '测试应用', 'TEST1', '样例部门1', 'apollo', 'apollo@acme.com', b'0', 'apollo', '2019-04-16 13:41:28', 'apollo', '2019-04-16 13:41:28');COMMIT;-- ------------------------------ Table structure for AppNamespace-- ----------------------------DROP TABLE IF EXISTS `AppNamespace`;CREATE TABLE `AppNamespace` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Name` varchar(32) NOT NULL DEFAULT '' COMMENT 'namespace名字,注意,需要全局唯一', `AppId` varchar(32) NOT NULL DEFAULT '' COMMENT 'app id', `Format` varchar(32) NOT NULL DEFAULT 'properties' COMMENT 'namespace的format类型', `IsPublic` bit(1) NOT NULL DEFAULT b'0' COMMENT 'namespace是否为公共', `Comment` varchar(64) NOT NULL DEFAULT '' COMMENT '注释', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_AppId` (`AppId`), KEY `Name_AppId` (`Name`,`AppId`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='应用namespace定义';-- ------------------------------ Records of AppNamespace-- ----------------------------BEGIN;INSERT INTO `AppNamespace` VALUES (1, 'application', 'SampleApp', 'properties', b'0', 'default app namespace', b'0', '', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `AppNamespace` VALUES (2, 'application', 'myapp', 'properties', b'0', 'default app namespace', b'0', 'apollo', '2019-04-16 13:41:28', 'apollo', '2019-04-16 13:41:28');INSERT INTO `AppNamespace` VALUES (3, 'test', 'myapp', 'properties', b'1', '', b'0', 'apollo', '2019-04-16 13:57:05', 'apollo', '2019-04-16 13:57:05');COMMIT;-- ------------------------------ Table structure for Audit-- ----------------------------DROP TABLE IF EXISTS `Audit`;CREATE TABLE `Audit` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `EntityName` varchar(50) NOT NULL DEFAULT 'default' COMMENT '表名', `EntityId` int(10) unsigned DEFAULT NULL COMMENT '记录ID', `OpName` varchar(50) NOT NULL DEFAULT 'default' COMMENT '操作类型', `Comment` varchar(500) DEFAULT NULL COMMENT '备注', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COMMENT='日志审计表';-- ------------------------------ Records of Audit-- ----------------------------BEGIN;INSERT INTO `Audit` VALUES (1, 'App', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:28', NULL, '2019-04-16 13:41:28');INSERT INTO `Audit` VALUES (2, 'AppNamespace', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:28', NULL, '2019-04-16 13:41:28');INSERT INTO `Audit` VALUES (3, 'Cluster', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:28', NULL, '2019-04-16 13:41:28');INSERT INTO `Audit` VALUES (4, 'Namespace', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:28', NULL, '2019-04-16 13:41:28');INSERT INTO `Audit` VALUES (5, 'Release', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:41', NULL, '2019-04-16 13:41:41');INSERT INTO `Audit` VALUES (6, 'ReleaseHistory', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:41:41', NULL, '2019-04-16 13:41:41');INSERT INTO `Audit` VALUES (7, 'Item', 2, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:42:16', NULL, '2019-04-16 13:42:16');INSERT INTO `Audit` VALUES (8, 'Release', 3, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:42:23', NULL, '2019-04-16 13:42:23');INSERT INTO `Audit` VALUES (9, 'ReleaseHistory', 3, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:42:23', NULL, '2019-04-16 13:42:23');INSERT INTO `Audit` VALUES (10, 'Namespace', 3, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:57:05', NULL, '2019-04-16 13:57:05');INSERT INTO `Audit` VALUES (11, 'AppNamespace', 3, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:57:05', NULL, '2019-04-16 13:57:05');INSERT INTO `Audit` VALUES (12, 'Release', 4, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:57:30', NULL, '2019-04-16 13:57:30');INSERT INTO `Audit` VALUES (13, 'ReleaseHistory', 4, 'INSERT', NULL, b'0', 'apollo', '2019-04-16 13:57:30', NULL, '2019-04-16 13:57:30');COMMIT;-- ------------------------------ Table structure for Cluster-- ----------------------------DROP TABLE IF EXISTS `Cluster`;CREATE TABLE `Cluster` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Name` varchar(32) NOT NULL DEFAULT '' COMMENT '集群名字', `AppId` varchar(32) NOT NULL DEFAULT '' COMMENT 'App id', `ParentClusterId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '父cluster', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_AppId_Name` (`AppId`,`Name`), KEY `IX_ParentClusterId` (`ParentClusterId`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='集群';-- ------------------------------ Records of Cluster-- ----------------------------BEGIN;INSERT INTO `Cluster` VALUES (1, 'default', 'SampleApp', 0, b'0', '', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `Cluster` VALUES (2, 'default', 'myapp', 0, b'0', 'apollo', '2019-04-16 13:41:28', 'apollo', '2019-04-16 13:41:28');COMMIT;-- ------------------------------ Table structure for Commit-- ----------------------------DROP TABLE IF EXISTS `Commit`;CREATE TABLE `Commit` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `ChangeSets` longtext NOT NULL COMMENT '修改变更集', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `NamespaceName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'namespaceName', `Comment` varchar(500) DEFAULT NULL COMMENT '备注', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `AppId` (`AppId`(191)), KEY `ClusterName` (`ClusterName`(191)), KEY `NamespaceName` (`NamespaceName`(191))) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='commit 历史表';-- ------------------------------ Records of Commit-- ----------------------------BEGIN;INSERT INTO `Commit` VALUES (1, '{\"createItems\":[{\"namespaceId\":2,\"key\":\"spring.test\",\"value\":\"测试apollo\",\"lineNum\":1,\"id\":2,\"isDeleted\":false,\"dataChangeCreatedBy\":\"apollo\",\"dataChangeCreatedTime\":\"2019-04-16 13:42:15\",\"dataChangeLastModifiedBy\":\"apollo\",\"dataChangeLastModifiedTime\":\"2019-04-16 13:42:15\"}],\"updateItems\":[],\"deleteItems\":[]}', 'myapp', 'default', 'application', NULL, b'0', 'apollo', '2019-04-16 13:42:16', 'apollo', '2019-04-16 13:42:16');COMMIT;-- ------------------------------ Table structure for GrayReleaseRule-- ----------------------------DROP TABLE IF EXISTS `GrayReleaseRule`;CREATE TABLE `GrayReleaseRule` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `AppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Cluster Name', `NamespaceName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Namespace Name', `BranchName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'branch name', `Rules` varchar(16000) DEFAULT '[]' COMMENT '灰度规则', `ReleaseId` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '灰度对应的release', `BranchStatus` tinyint(2) DEFAULT '1' COMMENT '灰度分支状态: 0:删除分支,1:正在使用的规则 2:全量发布', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Namespace` (`AppId`,`ClusterName`,`NamespaceName`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='灰度规则表';-- ------------------------------ Table structure for Instance-- ----------------------------DROP TABLE IF EXISTS `Instance`;CREATE TABLE `Instance` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `AppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `DataCenter` varchar(64) NOT NULL DEFAULT 'default' COMMENT 'Data Center Name', `Ip` varchar(32) NOT NULL DEFAULT '' COMMENT 'instance ip', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), UNIQUE KEY `IX_UNIQUE_KEY` (`AppId`,`ClusterName`,`Ip`,`DataCenter`), KEY `IX_IP` (`Ip`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='使用配置的应用实例';-- ------------------------------ Records of Instance-- ----------------------------BEGIN;INSERT INTO `Instance` VALUES (1, 'myapp', 'default', '', '10.153.111.139', '2019-04-16 14:02:53', '2019-04-16 14:02:53');COMMIT;-- ------------------------------ Table structure for InstanceConfig-- ----------------------------DROP TABLE IF EXISTS `InstanceConfig`;CREATE TABLE `InstanceConfig` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `InstanceId` int(11) unsigned DEFAULT NULL COMMENT 'Instance Id', `ConfigAppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Config App Id', `ConfigClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Config Cluster Name', `ConfigNamespaceName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'Config Namespace Name', `ReleaseKey` varchar(64) NOT NULL DEFAULT '' COMMENT '发布的Key', `ReleaseDeliveryTime` timestamp NULL DEFAULT NULL COMMENT '配置获取时间', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), UNIQUE KEY `IX_UNIQUE_KEY` (`InstanceId`,`ConfigAppId`,`ConfigNamespaceName`), KEY `IX_ReleaseKey` (`ReleaseKey`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Valid_Namespace` (`ConfigAppId`,`ConfigClusterName`,`ConfigNamespaceName`,`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='应用实例的配置信息';-- ------------------------------ Records of InstanceConfig-- ----------------------------BEGIN;INSERT INTO `InstanceConfig` VALUES (1, 1, 'myapp', 'default', 'application', '20190416134223-7f43754dbb4ca14b', '2019-04-16 14:02:52', '2019-04-16 14:02:52', '2019-04-16 14:02:52');INSERT INTO `InstanceConfig` VALUES (2, 1, 'myapp', 'default', 'test', '20190416135729-be85754dbb4ca14c', '2019-04-16 14:02:52', '2019-04-16 14:02:52', '2019-04-16 14:02:52');COMMIT;-- ------------------------------ Table structure for Item-- ----------------------------DROP TABLE IF EXISTS `Item`;CREATE TABLE `Item` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `NamespaceId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '集群NamespaceId', `Key` varchar(128) NOT NULL DEFAULT 'default' COMMENT '配置项Key', `Value` longtext NOT NULL COMMENT '配置项值', `Comment` varchar(1024) DEFAULT '' COMMENT '注释', `LineNum` int(10) unsigned DEFAULT '0' COMMENT '行号', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_GroupId` (`NamespaceId`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='配置项目';-- ------------------------------ Records of Item-- ----------------------------BEGIN;INSERT INTO `Item` VALUES (1, 1, 'timeout', '100', 'sample timeout配置', 1, b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `Item` VALUES (2, 2, 'spring.test', '测试apollo', NULL, 1, b'0', 'apollo', '2019-04-16 13:42:16', 'apollo', '2019-04-16 13:42:16');COMMIT;-- ------------------------------ Table structure for Namespace-- ----------------------------DROP TABLE IF EXISTS `Namespace`;CREATE TABLE `Namespace` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'Cluster Name', `NamespaceName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'Namespace Name', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId_ClusterName_NamespaceName` (`AppId`(191),`ClusterName`(191),`NamespaceName`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_NamespaceName` (`NamespaceName`(191))) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='命名空间';-- ------------------------------ Records of Namespace-- ----------------------------BEGIN;INSERT INTO `Namespace` VALUES (1, 'SampleApp', 'default', 'application', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `Namespace` VALUES (2, 'myapp', 'default', 'application', b'0', 'apollo', '2019-04-16 13:41:28', 'apollo', '2019-04-16 13:41:28');INSERT INTO `Namespace` VALUES (3, 'myapp', 'default', 'test', b'0', 'apollo', '2019-04-16 13:57:05', 'apollo', '2019-04-16 13:57:05');COMMIT;-- ------------------------------ Table structure for NamespaceLock-- ----------------------------DROP TABLE IF EXISTS `NamespaceLock`;CREATE TABLE `NamespaceLock` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', `NamespaceId` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '集群NamespaceId', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT 'default' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', `IsDeleted` bit(1) DEFAULT b'0' COMMENT '软删除', PRIMARY KEY (`Id`), UNIQUE KEY `IX_NamespaceId` (`NamespaceId`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='namespace的编辑锁';-- ------------------------------ Table structure for Release-- ----------------------------DROP TABLE IF EXISTS `Release`;CREATE TABLE `Release` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `ReleaseKey` varchar(64) NOT NULL DEFAULT '' COMMENT '发布的Key', `Name` varchar(64) NOT NULL DEFAULT 'default' COMMENT '发布名字', `Comment` varchar(256) DEFAULT NULL COMMENT '发布说明', `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `NamespaceName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'namespaceName', `Configurations` longtext NOT NULL COMMENT '发布配置', `IsAbandoned` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否废弃', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `AppId_ClusterName_GroupName` (`AppId`(191),`ClusterName`(191),`NamespaceName`(191)), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_ReleaseKey` (`ReleaseKey`)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='发布';-- ------------------------------ Records of Release-- ----------------------------BEGIN;INSERT INTO `Release` VALUES (1, '20161009155425-d3a0749c6e20bc15', '20161009155424-release', 'Sample发布', 'SampleApp', 'default', 'application', '{\"timeout\":\"100\"}', b'0', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `Release` VALUES (2, '20190416134141-7f43754dbb4ca14a', '20190416134137-release', '', 'myapp', 'default', 'application', '{}', b'0', b'0', 'apollo', '2019-04-16 13:41:41', 'apollo', '2019-04-16 13:41:41');INSERT INTO `Release` VALUES (3, '20190416134223-7f43754dbb4ca14b', '20190416134221-release', '', 'myapp', 'default', 'application', '{\"spring.test\":\"测试apollo\"}', b'0', b'0', 'apollo', '2019-04-16 13:42:23', 'apollo', '2019-04-16 13:42:23');INSERT INTO `Release` VALUES (4, '20190416135729-be85754dbb4ca14c', '20190416135728-release', '', 'myapp', 'default', 'test', '{}', b'0', b'0', 'apollo', '2019-04-16 13:57:30', 'apollo', '2019-04-16 13:57:30');COMMIT;-- ------------------------------ Table structure for ReleaseHistory-- ----------------------------DROP TABLE IF EXISTS `ReleaseHistory`;CREATE TABLE `ReleaseHistory` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `AppId` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'AppID', `ClusterName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'ClusterName', `NamespaceName` varchar(32) NOT NULL DEFAULT 'default' COMMENT 'namespaceName', `BranchName` varchar(32) NOT NULL DEFAULT 'default' COMMENT '发布分支名', `ReleaseId` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关联的Release Id', `PreviousReleaseId` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '前一次发布的ReleaseId', `Operation` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '发布类型,0: 普通发布,1: 回滚,2: 灰度发布,3: 灰度规则更新,4: 灰度合并回主分支发布,5: 主分支发布灰度自动发布,6: 主分支回滚灰度自动发布,7: 放弃灰度', `OperationContext` longtext NOT NULL COMMENT '发布上下文信息', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_Namespace` (`AppId`,`ClusterName`,`NamespaceName`,`BranchName`), KEY `IX_ReleaseId` (`ReleaseId`), KEY `IX_DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='发布历史';-- ------------------------------ Records of ReleaseHistory-- ----------------------------BEGIN;INSERT INTO `ReleaseHistory` VALUES (1, 'SampleApp', 'default', 'application', 'default', 1, 0, 0, '{}', b'0', 'apollo', '2019-04-14 14:13:47', 'apollo', '2019-04-14 14:13:47');INSERT INTO `ReleaseHistory` VALUES (2, 'myapp', 'default', 'application', 'default', 2, 0, 0, '{\"isEmergencyPublish\":false}', b'0', 'apollo', '2019-04-16 13:41:41', 'apollo', '2019-04-16 13:41:41');INSERT INTO `ReleaseHistory` VALUES (3, 'myapp', 'default', 'application', 'default', 3, 2, 0, '{\"isEmergencyPublish\":false}', b'0', 'apollo', '2019-04-16 13:42:23', 'apollo', '2019-04-16 13:42:23');INSERT INTO `ReleaseHistory` VALUES (4, 'myapp', 'default', 'test', 'default', 4, 0, 0, '{\"isEmergencyPublish\":false}', b'0', 'apollo', '2019-04-16 13:57:30', 'apollo', '2019-04-16 13:57:30');COMMIT;-- ------------------------------ Table structure for ReleaseMessage-- ----------------------------DROP TABLE IF EXISTS `ReleaseMessage`;CREATE TABLE `ReleaseMessage` ( `Id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `Message` varchar(1024) NOT NULL DEFAULT '' COMMENT '发布的消息内容', `DataChange_LastTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `DataChange_LastTime` (`DataChange_LastTime`), KEY `IX_Message` (`Message`(191))) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='发布消息';-- ------------------------------ Records of ReleaseMessage-- ----------------------------BEGIN;INSERT INTO `ReleaseMessage` VALUES (2, 'myapp+default+application', '2019-04-16 13:42:23');INSERT INTO `ReleaseMessage` VALUES (3, 'myapp+default+test', '2019-04-16 13:57:30');COMMIT;-- ------------------------------ Table structure for ServerConfig-- ----------------------------DROP TABLE IF EXISTS `ServerConfig`;CREATE TABLE `ServerConfig` ( `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增Id', `Key` varchar(64) NOT NULL DEFAULT 'default' COMMENT '配置项Key', `Cluster` varchar(32) NOT NULL DEFAULT 'default' COMMENT '配置对应的集群,default为不针对特定的集群', `Value` varchar(2048) NOT NULL DEFAULT 'default' COMMENT '配置项值', `Comment` varchar(1024) DEFAULT '' COMMENT '注释', `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal', `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '创建人邮箱前缀', `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人邮箱前缀', `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', PRIMARY KEY (`Id`), KEY `IX_Key` (`Key`), KEY `DataChange_LastTime` (`DataChange_LastTime`)) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='配置服务自身配置';-- ------------------------------ Records of ServerConfig-- ----------------------------BEGIN;INSERT INTO `ServerConfig` VALUES (1, 'eureka.service.url', 'default', 'http://localhost:8080/eureka/', 'Eureka服务Url,多个service以英文逗号分隔', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `ServerConfig` VALUES (2, 'namespace.lock.switch', 'default', 'false', '一次发布只能有一个人修改开关', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `ServerConfig` VALUES (3, 'item.value.length.limit', 'default', '20000', 'item value最大长度限制', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `ServerConfig` VALUES (4, 'config-service.cache.enabled', 'default', 'false', 'ConfigService是否开启缓存,开启后能提高性能,但是会增大内存消耗!', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');INSERT INTO `ServerConfig` VALUES (5, 'item.key.length.limit', 'default', '128', 'item key 最大长度限制', b'0', 'default', '2019-04-14 14:13:47', '', '2019-04-14 14:13:47');COMMIT;SET FOREIGN_KEY_CHECKS = 1;ApolloPortalDB: ...

April 30, 2019 · 24 min · jiezi

SpringBoot-2X-Kotlin系列之数据校验和异常处理

在开发项目时,我们经常需要在前后端都校验用户提交的数据,判断提交的数据是否符合我们的标准,包括字符串长度,是否为数字,或者是否为手机号码等;这样做的目的主要是为了减少SQL注入攻击的风险以及脏数据的插入。提到数据校验我们通常还会提到异常处理,因为为了安全起见,后端出现的异常我们通常不希望直接抛到客户端,而是经过我们的处理之后再返回给客户端,这样做主要是提升系统安全性,另外就是给予用户友好的提示。定义实体并加上校验注解class StudentForm() { @NotBank(message = '生日不能为空') var birthday: String = "" @NotBlank(message = "Id不能为空") var id:String = "" @NotBlank(message = "年龄不能为空") var age:String = "" @NotEmpty(message = "兴趣爱好不能为空") var Interests:List<String> = Collections.emptyList() @NotBlank(message = "学校不能为空") var school: String = "" override fun toString(): String { return ObjectMapper().writeValueAsString(this) }}这里首先使用的是基础校验注解,位于javax.validation.constraints下,常见注解有@NotNull、@NotEmpty、@Max、@Email、@NotBank、@Size、@Pattern,当然出了这些还有很多注解,这里就不在一一讲解,想了解更多的可以咨询查看jar包。 这里简单讲解一下注解的常见用法: @NotNull: 校验一个对象是否为Null@NotBank: 校验字符串是否为空串@NotEmpty: 校验List、Map、Set是否为空@Email: 校验是否为邮箱格式@Max @Min: 校验Number或String是否在指定范围内@Size: 通常需要配合@Max @Min一期使用@Pattern: 配合自定义正则表达式校验定义返回状态枚举enum class ResultEnums(var code:Int, var msg:String) { SUCCESS(200, "成功"), SYSTEM_ERROR(500, "系统繁忙,请稍后再试"),}自定义异常这里主要是参数校验,所以定义一个运行时异常,代码如下: ...

April 30, 2019 · 2 min · jiezi

五分钟快速了解ActiveMQ案例简单且详细

最近得闲,探索了一下ActiveMQ。 ActiveMQ消息队列,信息收发的容器,作用有异步消息,流量削锋,应用耦合。同行还有 Kafka、RabbitMQ、RocketMQ、ZeroMQ、MetaMQ 。安装下载地址:http://activemq.apache.org/co... window版本的解压后双击/bin/activemq.bat 即可启动 它有自己的可视化页面:http://localhost:8161/admin/ 默认访问密码是:admin/admin如果需要修改在:/conf/jetty-realm.properties 中修改 JmsTemplate在springboot上整合的,使用spring 的JmsTemplate来操作ActiveMQ一、首先在pom文件中导入所需的jar包坐标: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-activemq</artifactId> </dependency> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-pool</artifactId> </dependency>二、新增一个ActiveMQ的配置文件spring-jms.xml <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> <!-- 配置JMS连接工厂 --> <bean id="innerConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="${spring.activemq.broker-url}" /> </bean> <!--配置连接池--> <bean id="pooledConnectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop"> <property name="connectionFactory" ref="innerConnectionFactory" /> <property name="maxConnections" value="100"></property> </bean> <!-- 配置JMS模板,Spring提供的JMS工具类 --> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> <property name="connectionFactory" ref="pooledConnectionFactory" /> <property name="defaultDestination" ref="JmsSenderDestination" /> <property name="receiveTimeout" value="10000" /> </bean></beans>三、在启动类上配置以生效 @ImportResource(locations={"classpath:/config/spring-jms.xml"})四、在application.properties中配置ActiveMQ 的连接地址 spring.activemq.broker-url=tcp://localhost:61616准备就绪;开始写生产者和消费者,我这里把生产者和消费者写在一个项目里面。在这之前需要明白两个概念队列(Queue)和主题(Topic)传递模型队列(Queue)和主题(Topic)是JMS支持的两种消息传递模型: 点对点(point-to-point,简称PTP)Queue消息传递模型:一个消息生产者对应一个消费者发布/订阅(publish/subscribe,简称pub/sub)Topic消息传递模型:一个消息生产者对应多个个消费者QUEUE先在spring-jms.xml里添加配置一个队列名称Queue_love <bean id="JmsSenderDestination" class="org.apache.activemq.command.ActiveMQQueue"><constructor-arg> <value>Queue_love</value></constructor-arg></bean> 创建一个生产者来发送消息;@Qualifier("JmsSenderDestination")指定了发送到上面配置的Queue_love队列 @Componentpublic class JmsSender {@Autowiredprivate JmsTemplate jmsTemplate;@Qualifier("JmsSenderDestination")@Autowiredprotected Destination destination;public void sendMessage(final String msg) { logger.info("QUEUE destination :" + destination.toString() + ", 发送消息:" + msg); jmsTemplate.send(destination, new MessageCreator() { @Override public Message createMessage(final Session session) throws JMSException { return session.createTextMessage(msg); } });}}创建一个消费者来消费消息: ...

April 29, 2019 · 2 min · jiezi

Spring-Boot-Vue-前后端分离两种文件上传方式总结

在Vue.js 中,如果网络请求使用 axios ,并且使用了 ElementUI 库,那么一般来说,文件上传有两种不同的实现方案: 通过 Ajax 实现文件上传通过 ElementUI 里边的 Upload 组件实现文件上传两种方案,各有优缺点,我们分别来看。 准备工作首先我们需要一点点准备工作,就是在后端提供一个文件上传接口,这是一个普通的 Spring Boot 项目,如下: SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/");@PostMapping("/import")public RespBean importData(MultipartFile file, HttpServletRequest req) throws IOException { String format = sdf.format(new Date()); String realPath = req.getServletContext().getRealPath("/upload") + format; File folder = new File(realPath); if (!folder.exists()) { folder.mkdirs(); } String oldName = file.getOriginalFilename(); String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf(".")); file.transferTo(new File(folder,newName)); String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/upload" + format + newName; System.out.println(url); return RespBean.ok("上传成功!");}这里的文件上传比较简单,上传的文件按照日期进行归类,使用 UUID 给文件重命名。 ...

April 28, 2019 · 2 min · jiezi

springboot五springboot中的拦截器和过滤器小结

前言关于过滤器Filter和拦截器Interceptor,大家都不会陌生,从一开始的servelet,到springmvc,再到现在的springboot,都有接触到,记得刚接触的时候,会容易弄混淆,想写这篇文章做个小的总结 拦截器和过滤器的异同相同点 都是aop编程思想的体现,可以在程序执行前后做一些操作,如权限操作,日志记录等不同点: Filter是Servlet规范中定义的,拦截器是Spring框架中的触发时机不一样,过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的拦截器可以获取IOC容器中的各个bean,而过滤器就不行,拦截器归Spring管理Springboot实现过滤器和拦截器第一步:定义Filter @Slf4jpublic class TestFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { log.info("TestFilter filter。。。。。。。。"); filterChain.doFilter(servletRequest, servletResponse); }}第二步:注入springboot容器当中 @Configurationpublic class FilterConfig { @Bean Filter testFilter(){ return new TestFilter(); } @Bean public FilterRegistrationBean<TestFilter> filterRegistrationBean1(){ FilterRegistrationBean<TestFilter> filterRegistrationBean=new FilterRegistrationBean<>(); filterRegistrationBean.setFilter((TestFilter) testFilter()); filterRegistrationBean.addUrlPatterns("/*"); //filterRegistrationBean.setOrder();多个filter的时候order的数值越小 则优先级越高 return filterRegistrationBean; }}第三步:定义拦截器 @Slf4j@Service(value = "testInterceptor")public class TestInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("TestInterceptor preHandle...."); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { log.info("TestInterceptor postHandle...."); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { log.info("TestInterceptor afterCompletion...."); }}第四步:加入springboot容器 ...

April 27, 2019 · 1 min · jiezi

SpringBoot统一配置中心

一直使用springboot搭建后端项目,所有的配置都写到自己的resource目录下,随着微服务的项目越来越多,每个项目都需要自己的各种配置文件。而且后期一旦想要修改配置文件,就得重新发布一遍非常的麻烦,现在就来教教大家怎么统一在github上管理 这些配置,并做到一处修改处处生效,不需要重新发布项目。1 创建统一服务项目可以使用STS来初始化项目,选择自己的以来就好。 <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.mike</groupId> <artifactId>config-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>config-server</name> <description>config server</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.SR1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>创建bootstrap.yml文件,当然你可以使用application.yml或application.properties spring: application: name: config-repo cloud: config: server: git: uri: https://github.com/mike/config-repo.git #github仓库地址 username: mike # 用户名 password: 123456 # 密码在github上创建一个config-repo仓库,并添加配置文件:两个不同环境的配置hello-pj-dev.yml ...

April 26, 2019 · 2 min · jiezi

springboot-整合mybatisplus-组成后台开发基本框架

一直想搞一套后台基本开发框架出来,无奈太忙(其实太懒),最近受到两位大佬的启发,就改动了一下大佬做好的东西。初始版本:https://github.com/lihengming... 改进版本:https://github.com/tangzhiman... 我是直接从改进版本开始开发的,出于尊重,应当把初始版本的链接也放上来。 改进版本主要用的是TKmybatis 由于公司一直用的是mybatis-plus,所以我这里的版本是基于mybatis-plus开发的。 放大招!可不许再加班了 具体技术点不再赘述!springboot以及mybatis-plus的资源已经很多。 欢迎喷! 计划在之后把spring-cloud方面的常用技术点也整合进来。 github地址:https://github.com/sjf1256754...

April 25, 2019 · 1 min · jiezi

第二讲SpringSpring-MVCSpring-Boot三者之间的区别与联系

Spring Framework的诞生让开发人员的工作从石器时代跨域到了工业时代,你是否还能记起手撸Servlet和JDBC的岁月?,你是否还对Struts1以及Struts2莫名其妙的404错误记忆犹新?从2004年3月Spring 1.0发布到至今,Spring的发展已经走过了15个年头,其创造的价值让人瞩目。今天,带着这样一个背景来梳理一下Spring Framework,Spring MVC和Spring Boot三者之间的区别。 我们使用Spring家族的系列产品这么长时间,不禁会问这样几个问题:Spring Framework是什么?Spring MVC是什么?Spring Boot又是什么?它们被设计出来的目的是什么? 你需要了解的知识在接下来的内容中,将梳理这样几个知识点: Spring Framework基本概述Spring Framework主要解决的问题是什么?Spring MVC基本概述Spring MVC主要解决的问题是什么?Spring Boot主要解决的问题是什么?Spring,Spring MVC和Spring Boot三者之间的区别是什么?Spring Framework 解决了哪些核心问题?当你仔细思考这个问题的时候你会发现,很多地方它都有渗透到,貌似一个Spring就可以撑起开发的半边天,以至于很难一下子回答这个问题。那Spring Framework到底解决了哪些核心问题? Spring Framework最重要也是最核心的特性是依赖注入。所有的Spring模块的核心就是DI(依赖注入)或者IoC(控制反转)。依赖注入或控制反转是Spring Framework最大的特性,当我们正确使用DI(依赖注入)或IoC时,可以开发出一个高内聚低耦合的应用程序,而这一一个低耦合的应用程序可以轻松的对其实施单元测试。这就是Spring Framework解决的最核心的问题。 无依赖注入请考虑这一一个案例:UserAction依赖于UserService来获取用户信息,在没有依赖注入的情况下,我们需要手动在UserAction中实例化一个UserService对象,这样的手工作业意味着UserAction和UserService必须精密的联系在一起,才能正常工作。如果一个Action需要多个Service提供服务,那实例化这些Service将是一个繁重的工作。下面我们给出一个不使用依赖注入的代码片段加以说明: UserService.java public interface UserService{ User profile();}UserServiceImpl.java public class UserServiceImpl implements UserService{ @Override User profile(){ // TODO }}UserAction.java @RestControllerpublic class UserAction{ private UserService userService = new UserServiceImpl(); // other services... @GetMapping("/profile") public User profile(){ return userService.profile(); }}引入依赖注入引入依赖注入将会使整个代码看起来很清爽。为了能够开发出高内聚低耦合的应用程序,Spring Framework为我们做了大量的准备工作。下面我们使用两个简单的注解@Component和@Autowired来实现依赖注入。 @Component : 该注解将会告诉Spring Framework,被此注解标注的类需要纳入到Bean管理器中。@Autowired : 告诉Spring Framework需要找到一与其类型匹配的对象,并将其自动引入到所需要的类中。在接下来的示例代码中,我们会看到Spring Framework将为UserService创建一个Bean对象,并将其自动引入到UserAction中。 ...

April 24, 2019 · 2 min · jiezi

springboot三applicationproperties和applicationyml是何时解析的

前言用过springboot的肯定很熟悉,它其中有个重要的特性,就是自动配置(平时习惯的一些设置的配置作为默认配置)。springboot提倡无XML配置文件的理念,使用springboot生成的应用完全不会生成任何配置代码与XML配置文件。下面先看一个springboot集成mybatis的例子。第一步: 引入pom文件 <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>第二步: 因为我使用的xml配置文件去使用mybatis,在application.properties文件加入如下配置: #指定mapper文件位置mybatis.mapper-locations=classpath:mapper/*.xml#数据源信息spring.datasource.url=jdbc:mysql://127.0.0.1:3306/zplxjj?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8spring.datasource.username=rootspring.datasource.password=123456spring.datasource.driver-class-name=com.mysql.jdbc.Driver第三步: 加入实体类、dao、mapper文件第四步:启动类上面加入注解 @SpringBootApplication@MapperScan("com.stone.zplxjj.dao")public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}第五步:至此,配置完成,只需要写个单侧,springboot已经完美集成mybatis @RunWith(SpringRunner.class)@SpringBootTestpublic class ApplicationTests { @Autowired UserMapper userMapper; @Test public void testMybatis() { System.out.println(userMapper.selectByPrimaryKey(1L)); }}@EnableAutoConfiguration通过上面的例子,我们发现集成mybatis特别简单,那些繁琐的类的注入都没有写,只需要加入数据库的一些配置即可,那这其中@EnableAutoConfiguration功不可没。@EnableAutoConfiguration 注解已经在@SpringBootApplication里面了 @Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class}), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class})})public @interface SpringBootApplication { @AliasFor( annotation = EnableAutoConfiguration.class ) Class<?>[] exclude() default {}; @AliasFor( annotation = EnableAutoConfiguration.class ) String[] excludeName() default {}; @AliasFor( annotation = ComponentScan.class, attribute = "basePackages" ) String[] scanBasePackages() default {}; @AliasFor( annotation = ComponentScan.class, attribute = "basePackageClasses" ) Class<?>[] scanBasePackageClasses() default {};}我们看到@EnableAutoConfiguration结构如下: ...

April 23, 2019 · 2 min · jiezi

SpringBoot高级篇JdbcTemplate之数据插入使用姿势详解

db操作可以说是java后端的必备技能了,实际项目中,直接使用JdbcTemplate的机会并不多,大多是mybatis,hibernate,jpa或者是jooq,然后前几天写一个项目,因为db操作非常简单,就直接使用JdbcTemplate,然而悲催的发现,对他的操作并没有预期中的那么顺畅,所以有必要好好的学一下JdbcTemplate的CURD;本文为第一篇,插入数据 <!-- more --> I. 环境1. 配置相关使用SpringBoot进行db操作引入几个依赖,就可以愉快的玩耍了,这里的db使用mysql,对应的pom依赖如 <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency></dependencies>接着就是db的配置信息,下面是连接我本机的数据库配置 ## DataSourcespring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=falsespring.datasource.driver-class-name= com.mysql.jdbc.Driverspring.datasource.username=rootspring.datasource.password=2. 测试db创建一个测试db CREATE TABLE `money` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名', `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱', `is_deleted` tinyint(1) NOT NULL DEFAULT '0', `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `name` (`name`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;II. 使用姿势直接引入jdbcTemplate,注入即可,不需要其他的操作 ...

April 23, 2019 · 5 min · jiezi

SpringBoot高级篇JdbcTemplate之数据查询上篇

前面一篇介绍如何使用JdbcTemplate实现插入数据,接下来进入实际业务中,最常见的查询篇。由于查询的姿势实在太多,对内容进行了拆分,本篇主要介绍几个基本的使用姿势 queryForMapqueryForListqueryForObject<!-- more --> I. 环境准备环境依然借助前面一篇的配置,链接如: 190407-SpringBoot高级篇JdbcTemplate之数据插入使用姿势详解 或者直接查看项目源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate 我们查询所用数据,正是前面一篇插入的结果,如下图 II. 查询使用说明1. queryForMapqueryForMap,一般用于查询单条数据,然后将db中查询的字段,填充到map中,key为列名,value为值 a. 基本使用姿势最基本的使用姿势,就是直接写完整的sql,执行 String sql = "select * from money where id=1";Map<String, Object> map = jdbcTemplate.queryForMap(sql);System.out.println("QueryForMap by direct sql ans: " + map);这种用法的好处是简单,直观;但是有个非常致命的缺点,如果你提供了一个接口为 public Map<String, Object> query(String condition) { String sql = "select * from money where name=" + condition; return jdbcTemplate.queryForMap(sql);}直接看上面代码,会发现问题么??? 有经验的小伙伴,可能一下子就发现了sql注入的问题,如果传入的参数是 '一灰灰blog' or 1=1 order by id desc limit 1, 这样输出和我们预期的一致么? b. 占位符替换正是因为直接拼sql,可能到只sql注入的问题,所以更推荐的写法是通过占位符 + 传参的方式 ...

April 23, 2019 · 3 min · jiezi

SpringBoot高级篇JdbcTemplate之数据查询下篇

SpringBoot高级篇JdbcTemplate之数据查询上篇 讲了如何使用JdbcTemplate进行简单的查询操作,主要介绍了三种方法的调用姿势 queryForMap, queryForList, queryForObject 本篇则继续介绍剩下的两种方法使用说明 queryForRowSetquery<!-- more --> I. 环境准备环境依然借助前面一篇的配置,链接如: 190407-SpringBoot高级篇JdbcTemplate之数据插入使用姿势详解 或者直接查看项目源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate 我们查询所用数据,正是前面一篇插入的结果,如下图 II. 查询使用说明1. queryForRowSet查询上篇中介绍的三种方法,返回的记录对应的结构要么是map,要么是通过RowMapper进行结果封装;而queryForRowSet方法的调用,返回的则是SqlRowSet对象,这是一个集合,也就是说,可以查询多条记录 使用姿势也比较简单,如下 public void queryForRowSet() { String sql = "select * from money where id > 1 limit 2"; SqlRowSet result = jdbcTemplate.queryForRowSet(sql); while (result.next()) { MoneyPO moneyPO = new MoneyPO(); moneyPO.setId(result.getInt("id")); moneyPO.setName(result.getString("name")); moneyPO.setMoney(result.getInt("money")); moneyPO.setDeleted(result.getBoolean("is_deleted")); moneyPO.setCreated(result.getDate("create_at").getTime()); moneyPO.setUpdated(result.getDate("update_at").getTime()); System.out.println("QueryForRowSet by DirectSql: " + moneyPO); }}对于使用姿势而言与之前的区别不大,还有一种就是sql也支持使用占位方式,如 // 采用占位符方式查询sql = "select * from money where id > ? limit ?";result = jdbcTemplate.queryForRowSet(sql, 1, 2);while (result.next()) { MoneyPO moneyPO = new MoneyPO(); moneyPO.setId(result.getInt("id")); moneyPO.setName(result.getString("name")); moneyPO.setMoney(result.getInt("money")); moneyPO.setDeleted(result.getBoolean("is_deleted")); moneyPO.setCreated(result.getDate("create_at").getTime()); moneyPO.setUpdated(result.getDate("update_at").getTime()); System.out.println("QueryForRowSet by ? sql: " + moneyPO);}重点关注下结果的处理,需要通过迭代器的方式进行数据遍历,获取每一列记录的值的方式和前面一样,可以通过序号的方式获取(序号从1开始),也可以通过制定列名方式(db列名) ...

April 23, 2019 · 3 min · jiezi

SpringBoot高级篇JdbcTemplate之数据更新与删除

前面介绍了JdbcTemplate的插入数据和查询数据,占用CURD中的两项,本文则将主要介绍数据更新和删除。从基本使用上来看,姿势和前面的没啥两样 <!-- more --> I. 环境准备环境依然借助前面一篇的配置,链接如: 190407-SpringBoot高级篇JdbcTemplate之数据插入使用姿势详解 或者直接查看项目源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate 我们查询所用数据,正是前面一篇插入的结果,如下图 II. 更新使用说明对于数据更新,这里会分为两种进行说明,单个和批量;这个单个并不是指只能一条记录,主要针对的是sql的数量而言 1. update 方式看过第一篇数据插入的童鞋,应该也能发现,新增数据也是用的这个方法,下面会介绍三种不同的使用姿势 先提供一个数据查询的转换方法,用于对比数据更新前后的结果 private MoneyPO queryById(int id) { return jdbcTemplate.queryForObject( "select id, `name`, money, is_deleted as isDeleted, unix_timestamp(create_at) as " + "created, unix_timestamp(update_at) as updated from money where id=?", new BeanPropertyRowMapper<>(MoneyPO.class), id);}a. 纯sql更新这个属于最基本的方式了,前面几篇博文中大量使用了,传入一条完整的sql,执行即可 int id = 10;// 最基本的sql更新String sql = "update money set money=money + 999 where id =" + id;int ans = jdbcTemplate.update(sql);System.out.println("basic update: " + ans + " | db: " + queryById(id));b. 占位sql问好占位,实际内容通过参数传递方式 ...

April 23, 2019 · 3 min · jiezi

springboot(二)——springboot自动配置解析

前言用过springboot的肯定很熟悉,它其中有个重要的特性,就是自动配置(平时习惯的一些设置的配置作为默认配置)。springboot提倡无XML配置文件的理念,使用springboot生成的应用完全不会生成任何配置代码与XML配置文件。下面先看一个springboot集成mybatis的例子。第一步: 引入pom文件 <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>第二步: 因为我使用的xml配置文件去使用mybatis,在application.properties文件加入如下配置: #指定mapper文件位置mybatis.mapper-locations=classpath:mapper/*.xml#数据源信息spring.datasource.url=jdbc:mysql://127.0.0.1:3306/zplxjj?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8spring.datasource.username=rootspring.datasource.password=123456spring.datasource.driver-class-name=com.mysql.jdbc.Driver第三步: 加入实体类、dao、mapper文件第四步:启动类上面加入注解 @SpringBootApplication@MapperScan("com.stone.zplxjj.dao")public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}第五步:至此,配置完成,只需要写个单侧,springboot已经完美集成mybatis @RunWith(SpringRunner.class)@SpringBootTestpublic class ApplicationTests { @Autowired UserMapper userMapper; @Test public void testMybatis() { System.out.println(userMapper.selectByPrimaryKey(1L)); }}@EnableAutoConfiguration通过上面的例子,我们发现集成mybatis特别简单,那些繁琐的类的注入都没有写,只需要加入数据库的一些配置即可,那这其中@EnableAutoConfiguration功不可没。@EnableAutoConfiguration 注解已经在@SpringBootApplication里面了 @Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class}), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class})})public @interface SpringBootApplication { @AliasFor( annotation = EnableAutoConfiguration.class ) Class<?>[] exclude() default {}; @AliasFor( annotation = EnableAutoConfiguration.class ) String[] excludeName() default {}; @AliasFor( annotation = ComponentScan.class, attribute = "basePackages" ) String[] scanBasePackages() default {}; @AliasFor( annotation = ComponentScan.class, attribute = "basePackageClasses" ) Class<?>[] scanBasePackageClasses() default {};}我们看到@EnableAutoConfiguration结构如下: ...

April 23, 2019 · 2 min · jiezi

springboot(一)——搭建自己的springboot项目(附带日志配置)

idea使用spring Initalizr 快速构建spring boot点击新建项目,选择如图所示 点击next后 点击next,之后按照图中所示选择 选择路径 点击完成,如图所示,删除自己不想要的,项目构建完成 构建一个controller,启动项目就可以看到返回结果了 在自己的服务器搭建自己的springboot项目使用idea向远程服务传递项目设置idea 配置相关信息 上传到指定机器 配置启动脚本,基于java -jar命令start.sh#!/bin/bashnohup java -jar target/zplxjj.jar &stop.sh#!/bin/bashPID=$(ps -ef | grep target/zplxjj.jar | grep -v grep | awk '{ print $2 }')if [ -z "$PID" ]then echo Application is already stoppedelse echo kill $PID kill $PIDfi~run.sh#!/bin/bashecho stop applicationsource stop.shecho start applicationsource start.sh启动自己的项目只需要执行run.sh就行,一个自己的spring boot就搭建起来了 logback配置实际项目中,我们希望日志可以记录在服务器上面,这边用的是logback,是springboot自带的,我这边集成方式是加入logback-spring.xml文件,加入后启动项目即可,文件内容如下: <?xml version="1.0" encoding="UTF-8"?><configuration> <!--用来定义变量值的标签--> <property name="LOG_HOME" value="./logs"/> <property name="encoding" value="UTF-8"/> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度;%M:%L是方法和行号;%msg是日志消息;%n是换行符--> <property name="normal-pattern" value="%d{yyyy-MM-dd/HH:mm:ss.SSS}|%X{localIp}|%X{requestId}|%X{requestSeq}|%X{country}|%X{deviceType}|%X{deviceId}|%X{userId}|^_^|[%t] %-5level %logger{50} %line - %m%n"/> <property name="plain-pattern" value="%d{yyyy-MM-dd.HH:mm:ss} %msg%n"/> <!-- 按照每天生成日志文件 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--日志文件输出的文件名--> <file>${LOG_HOME}/zplxjj.log</file> <Append>true</Append> <prudent>false</prudent> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${normal-pattern}</pattern> <charset>${encoding}</charset> </encoder> <!--按时间分割--> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/zplxjj.log.%d{yyyy-MM-dd}.%i</fileNamePattern> <maxFileSize>128MB</maxFileSize> <maxHistory>15</maxHistory> <totalSizeCap>32GB</totalSizeCap> </rollingPolicy> </appender> <!--控制台输出--> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> <pattern>${normal-pattern}</pattern> </encoder> </appender> <!-- log file error --> <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> </filter> <file>${LOG_HOME}/zplxjj-error.log</file> <prudent>false</prudent> <Append>true</Append> <encoder> <pattern>${normal-pattern}</pattern> <charset>${encoding}</charset> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/zplxjj-error.log.%d{yyyy-MM-dd}.%i</fileNamePattern> <maxFileSize>128MB</maxFileSize> <maxHistory>15</maxHistory> <totalSizeCap>32GB</totalSizeCap> </rollingPolicy> </appender> <root level="INFO"> <appender-ref ref="STDOUT"/> <appender-ref ref="FILE"/> <appender-ref ref="ERROR"/> </root></configuration>效果如图: ...

April 23, 2019 · 1 min · jiezi

SpringMVC 中 @ControllerAdvice 注解的三种使用场景!

@ControllerAdvice ,很多初学者可能都没有听说过这个注解,实际上,这是一个非常有用的注解,顾名思义,这是一个增强的 Controller。使用这个 Controller ,可以实现三个方面的功能: 全局异常处理全局数据绑定全局数据预处理灵活使用这三个功能,可以帮助我们简化很多工作,需要注意的是,这是 SpringMVC 提供的功能,在 Spring Boot 中可以直接使用,下面分别来看。 全局异常处理使用 @ControllerAdvice 实现全局异常处理,只需要定义类,添加该注解即可定义方式如下: @ControllerAdvicepublic class MyGlobalExceptionHandler { @ExceptionHandler(Exception.class) public ModelAndView customException(Exception e) { ModelAndView mv = new ModelAndView(); mv.addObject("message", e.getMessage()); mv.setViewName("myerror"); return mv; }}在该类中,可以定义多个方法,不同的方法处理不同的异常,例如专门处理空指针的方法、专门处理数组越界的方法...,也可以直接向上面代码一样,在一个方法中处理所有的异常信息。 @ExceptionHandler 注解用来指明异常的处理类型,即如果这里指定为 NullpointerException,则数组越界异常就不会进到这个方法中来。 全局数据绑定全局数据绑定功能可以用来做一些初始化的数据操作,我们可以将一些公共的数据定义在添加了 @ControllerAdvice 注解的类中,这样,在每一个 Controller 的接口中,就都能够访问导致这些数据。 使用步骤,首先定义全局数据,如下: @ControllerAdvicepublic class MyGlobalExceptionHandler { @ModelAttribute(name = "md") public Map<String,Object> mydata() { HashMap<String, Object> map = new HashMap<>(); map.put("age", 99); map.put("gender", "男"); return map; }}使用 @ModelAttribute 注解标记该方法的返回数据是一个全局数据,默认情况下,这个全局数据的 key 就是返回的变量名,value 就是方法返回值,当然开发者可以通过 @ModelAttribute 注解的 name 属性去重新指定 key。 ...

April 22, 2019 · 1 min · jiezi

(第一讲)Spring Initializr-快速入门Spring Boot的最好选择

Spring Initializr [http://start.spring.io/]是引导你快速构建Spring Boot项目的不二选择。 它允许你通过简单的操作步骤,就可以构建出一个完整的Spring Boot应用程序。你可以通过Spring Initializr引导界面构建如下类型的Spring Boot应用: Web应用程序Restful 应用程序Batch应用程序Spring Boot对很多第三方框架提供了良好的支持,可以通过对应的artifactId获得他们,这里列举其中的一部分供参考: spring-boot-starter-web-services:用于构建可与外界交互的SOAP Web服务应用程序spring-boot-starter-web:可用于构建Web应用程序或者基于Restful风格的应用程序spring-boot-starter-test:可用于构建并编写单元测试和集成测试spring-boot-starter-jdbc:构建基于JDBC的应用程序spring-boot-starter-hateoas:通过引入HATEOAS功能,让你轻松实现RESTful服务spring-boot-starter-security:使用Spring Security对系统用户进行身份验证和鉴权spring-boot-starter-data-jpa:基于Hibernate实现的Spring Data JPAspring-boot-starter-cache:开启基于Spring Framework的缓存支持spring-boot-starter-data-rest:使用Spring Data REST提供REST服务在本讲中,我将通过使用Spring Initializr来演示如何快速创建一个简单的Web应用程序。 使用Spring Initializr构建Web应用程序使用Spring Initializr构建Web应用程序是一件非常简单快速的事情。 如上图所示,我们需要执行如下的几个操作: 通过浏览器访问Spring Initializr官网 ,然后再执行下面的几个选择项 设置groupId : com.ramostear.spring.boot设置artifactId: spring-boot-quick-start项目名称:默认为spring-boot-quick-start基础包名:默认即可(你也可以选择修改)(通过点击More options展开)在搜索框中分别检索并选择如下几个组件:Web,Actuator,DevTools最后,点击“Generate Project”生成并下载项目将项目导入到IntelleJ IDEA中Spring Boot项目目录结构下图显示了在IDEA中导入刚才下载的项目目录结构: SpringBootQuickStartApplication.java:Spring Boot运行的主文件,它负责初始化Spring Boot自动配置和Spring应用程序上下文application.properties : 应用程序配置文件SpringBootQuickStartApplicationTests : 用于单元测试的简单启动器pom.xml : Maven构建项目的配置文件,包括了Spring Boot Starter Web等相关依赖项。特别指出,它会自动将Spring Boot Starter Parent作为整个工程的父依赖。核心的代码src/main/java 包下放置我们主要的逻辑代码,src/test/java包下放置项目的测试代码,src/main/resources包下放置项目的配置文件以及一些静态资源文件,如页html文件,css文件和js文件等。我们从上到下依次进行介绍。 SpringBootQuickStartApplication.javapackage com.ramostear.spring.boot.springbootquickstart;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class SpringBootQuickStartApplication { public static void main(String[] args) { SpringApplication.run(SpringBootQuickStartApplication.class, args); }}@SpringBootApplication : 负责初始化Spring Boot 自动化配置项和Spring应用程序上下文SpringApplication.run() : 负责启动Spring Boot应用程序的静态方法application.propertiesSpring Boot应用程序的配置文件,这里我们简单的设置一下项目启动的端口为8080(默认端口8080)和应用名称为Spring Boot Quick Start: ...

April 21, 2019 · 2 min · jiezi

spring boot学习(7)— 自定义中的 HttpMessageConverter

在我们开发自己的应用时,有时候,我们可能需要自定义一些自己的数据格式来传输,这时,自定义的数据传输和类的实例之间进行转化就需要统一起来了, Spring MVC 中的 HttpMessageConverter 就派上用场了。 HttpMessageConverter 的声明:public interface HttpMessageConverter<T> { /** * Indicates whether the given class can be read by this converter. * @param clazz the class to test for readability * @param mediaType the media type to read (can be {@code null} if not specified); * typically the value of a {@code Content-Type} header. * @return {@code true} if readable; {@code false} otherwise */ boolean canRead(Class<?> clazz, @Nullable MediaType mediaType); /** * Indicates whether the given class can be written by this converter. * @param clazz the class to test for writability * @param mediaType the media type to write (can be {@code null} if not specified); * typically the value of an {@code Accept} header. * @return {@code true} if writable; {@code false} otherwise */ boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType); /** * Return the list of {@link MediaType} objects supported by this converter. * @return the list of supported media types */ List<MediaType> getSupportedMediaTypes(); /** * Read an object of the given type from the given input message, and returns it. * @param clazz the type of object to return. This type must have previously been passed to the * {@link #canRead canRead} method of this interface, which must have returned {@code true}. * @param inputMessage the HTTP input message to read from * @return the converted object * @throws IOException in case of I/O errors * @throws HttpMessageNotReadableException in case of conversion errors */ T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; /** * Write an given object to the given output message. * @param t the object to write to the output message. The type of this object must have previously been * passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}. * @param contentType the content type to use when writing. May be {@code null} to indicate that the * default content type of the converter must be used. If not {@code null}, this media type must have * previously been passed to the {@link #canWrite canWrite} method of this interface, which must have * returned {@code true}. * @param outputMessage the message to write to * @throws IOException in case of I/O errors * @throws HttpMessageNotWritableException in case of conversion errors */ void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;}里面有四个方法: ...

April 21, 2019 · 3 min · jiezi

一个Java程序猿眼中的前后端分离以及Vue.js入门

松哥的书里边,其实有涉及到 Vue,但是并没有详细说过,原因很简单,Vue 的资料都是中文的,把 Vue.js 官网的资料从头到尾浏览一遍该懂的基本就懂了,个人感觉这个是最好的 Vue.js 学习资料 ,因此在我的书里边就没有多说。但是最近总结小伙伴遇到的问题,感觉很多人对前后端分离开发还是两眼一抹黑,所以今天松哥想和大家聊一下前后端分离以及 Vue.js 的一点事,算是一个简单的入门科普吧。前后端不分后端模板:Jsp、FreeMarker、Velocity前端模板:Thymeleaf前后端不分,Jsp 是一个非常典型写法,Jsp 将 HTML 和 Java 代码结合在一起,刚开始的时候,确实提高了生产力,但是时间久了,大伙就发现 Jsp 存在的问题了,对于后端工程师来说,可能不太精通 css ,所以流程一般是这样前端设计页面–>后端把页面改造成 Jsp –> 后端发现问题 –> 页面给前端 –> 前端不会Jsp。这种方式效率低下。特别是在移动互联网兴起后,公司的业务,一般除了 PC 端,还有手机端、小程序等,通常,一套后台系统需要对应多个前端,此时就不可以继续使用前后端不分的开发方式了。在前后端不分的开发方式中,一般来说,后端可能返回一个 ModelAndView ,渲染成 HTML 之后,浏览器当然可以展示,但是对于小程序、移动端来说,并不能很好的展示 HTML(实际上移动端也支持HTML,只不过运行效率低下)。这种时候,后端和前端数据交互,主流方案就是通过 JSON 来实现。前后端分离前后端分离后,后端不再写页面,只提供 JSON 数据接口(XML数据格式现在用的比较少),前端可以移动端、小程序、也可以是 PC 端,前端负责 JSON 的展示,页面跳转等都是通过前端来实现的。前端后分离后,前端目前有三大主流框架:Vue作者尤雨溪,Vue本身借鉴了 Angular,目前GitHubstar数最多,建议后端工程师使用这个,最大的原因是Vue上手容易,可以快速学会,对于后端工程师来说,能快速搭建页面解决问题即可,但是如果你是专业的前端工程师,我会推荐你三个都去学习 。就目前国内前端框架使用情况来说,Vue 算是使用最多的。而且目前来说,有大量 Vue 相关的周边产品,各种 UI 框架,开源项目,学习资料非常多。ReactFacebook 的产品。是一个用于构建用户界面的 js 库,React 性能较好,代码逻辑简单。AngularAngularJS 是一款由 Google 维护的开源 JavaScript 库,用来协助单一页面应用程序运行。它的目标是透过 MVC 模式(MVC)功能增强基于浏览器的应用,使开发和测试变得更加容易。Vue简介Vue (读音 /vju/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。只关注视图层MVVM 框架大家在使用 jQuery 过程中,掺杂了大量的 DOM 操作,修改视图或者获取 value ,都需要 DOM 操作,MVVM 是一种视图和数据模型双向绑定的框架,即数据发生变化,视图会跟着变化,视图发生变化,数据模型也会跟着变化,开发者再也不需要操作 DOM 节点。如下一个简单的九九乘法表让大家感受一下 MVVM :<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Title</title> <script src=“https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script></head><body><div id=“app”> <input type=“text” v-model=“num”> <table border=“1”> <tr v-for=“i in parseInt(num)"> <td v-for=“j in i”>{{j}}{{i}}={{ij}}</td> </tr> </table></div><script> var app = new Vue({ el: “#app”, data: { num:9 } });</script></body></html>用户修改输入框中的数据,引起变量的变化,进而实现九九乘法表的更新。SPASPA(single page web application),单页面应用,是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,而非传统的从服务器重新加载整个新页面。这种方法避免了页面之间切换打断用户体验,使应用程序更像一个桌面应用程序。在单页应用中,所有必要的代码( HTML、JavaScript 和 CSS )都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面。SPA 有一个缺点,因为 SPA 应用部署后只有1个页面,而且这个页面只是一堆 js 、css 引用,没有其他有效价值,因此,SPA 应用不易被搜索引擎收录,所以,一般来说,SPA 适合做大型企业后台管理系统。Vue 使用方式大致上可以分为两大类:直接将Vue在页面中引入,不做 SPA 应用SPA应用基本环境搭建首先需要安装两个东西:NodeJSnpm直接搜索下载 NodeJS 即可,安装成功之后,npm 也就有了。安装成功之后,可以 在 cmd 命令哈验证是否安装成功:NodeJS 安装成功之后,接下来安装 Vue的工具:npm install -g vue-cli # 只需要第一次安装时执行vue init webpack my-project # 使用webpack模板创建一个vue项目cd my-project #进入到项目目录中npm install # 下载依赖(如果在项目创建的最后一步选择了自动执行npm install,则该步骤可以省略)npm run dev # 启动项目 启动成功后,浏览器输入 http://localhost:8080 就能看到如下页面: 执行 npm install 命令时,默认使用的是国外的下载源 ,可以通过如下代码配置为使用淘宝的镜像:npm config set registry https://registry.npm.taobao.org修改完成后,就能有效提高下载的成功率。Vue 项目结构介绍Vue 项目创建完成后,使用 Web Storm 打开项目,项目目录如下:build 文件夹,用来存放项目构建脚本config 中存放项目的一些基本配置信息,最常用的就是端口转发node_modules 这个目录存放的是项目的所有依赖,即 npm install 命令下载下来的文件src 这个目录下存放项目的源码,即开发者写的代码放在这里static 用来存放静态资源index.html 则是项目的首页,入口页,也是整个项目唯一的HTML页面package.json 中定义了项目的所有依赖,包括开发时依赖和发布时依赖对于开发者来说,以后 99.99% 的工作都是在 src 中完成的,src 中的文件目录如下:assets 目录用来存放资产文件components 目录用来存放组件(一些可复用,非独立的页面),当然开发者也可以在 components 中直接创建完整页面。推荐在 components 中存放组件,另外单独新建一个 page 文件夹,专门用来放完整页面。router 目录中,存放了路由的js文件App.vue 是一个Vue组件,也是项目的第一个Vue组件main.js相当于Java中的main方法,是整个项目的入口jsmain.js 内容如下:import Vue from ‘vue’import App from ‘./App’import router from ‘./router’Vue.config.productionTip = false/* eslint-disable no-new */new Vue({ el: ‘#app’, router, components: { App }, template: ‘<App/>’})在main.js 中,首先导入 Vue 对象导入 App.vue ,并且命名为 App导入router,注意,由于router目录下路由默认文件名为 index.js ,因此可以省略所有东西都导入成功后,创建一个Vue对象,设置要被Vue处理的节点是 ‘#app’,’#app’ 指提前在index.html 文件中定义的一个div将 router 设置到 vue 对象中,这里是一个简化的写法,完整的写法是 router:router,如果 key/value 一模一样,则可以简写。声明一个组件 App,App 这个组件在一开始已经导入到项目中了,但是直接导入的组件无法直接使用,必须要声明。template 中定义了页面模板,即将 App 组件中的内容渲染到 ‘#app’ 这个div 中。因此,可以猜测,项目启动成功后,看到的页面效果定义在 App.vue 中<template> <div id=“app”> <img src=”./assets/logo.png”> <router-view/> </div></template><script>export default { name: ‘App’}</script><style>#app { font-family: ‘Avenir’, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px;}</style>App.vue 是一个vue组件,这个组件中包含三部分内容:1.页面模板(template);2.页面脚本(script);3.页面样式(style)页面模板中,定义了页面的 HTML 元素,这里定义了两个,一个是一张图片,另一个则是一个 router-view页面脚本主要用来实现当前页面数据初始化、事件处理等等操作页面样式就是针对 template 中 HTML 元素的页面美化操作需要额外解释的是,router-view,这个指展示路由页面的位置,可以简单理解为一个占位符,这个占位符展示的内容将根据当前具体的 URL 地址来定。具体展示的内容,要参考路由表,即 router/index.js 文件,该文件如下:import Vue from ‘vue’import Router from ‘vue-router’import HelloWorld from ‘@/components/HelloWorld’Vue.use(Router)export default new Router({ routes: [ { path: ‘/’, name: ‘HelloWorld’, component: HelloWorld } ]})这个文件中,首先导入了Vue对象、Router对象以及 HelloWorld 组件,创建一个Router对象,并定义路由表这里定义的路由表,path为 / ,对应的组件为 HelloWorld,即浏览器地址为 / 时,在router-view位置显示 HelloWorld 组件WebStorm 中启动Vue也可以直接在 webstorm 中配置vue并启动,点击右上角进行配置:然后配置一下脚本 :配置完成后,点击右上角启动按钮,就可以启动一个 Vue 项目,如下:项目编译这么大一个前端项目,肯定没法直接发布运行,当开发者完成项目开发后,将 cmd 命令行定位到当前项目目录,然后执行如下命令对项目进行打包:npm run build打包成功后,当前项目目录下会生成一个 dist 文件夹,这个文件夹中有两个文件,分别是 index.html 和 static ,index.html 页面就是我们 SPA 项目中唯一的 HTML 页面了,static 中则保存了编译后的 js、css等文件,项目发布时,可以使用 nginx 独立部署 dist 中的静态文件,也可以将静态文件拷贝到 Spring Boot 项目的 static 目录下,然后对 Spring Boot 项目进行编译打包发布。总结因为松哥的读者以后端程序猿居多,也有少量前端程序猿,因此本文松哥想从一个后端程序猿的角度来带大家理解一下前后端分离以及 Vue 的一些基本用法,也欢迎专业的前端工程师出来拍砖。 关注公众号牧码小子,专注于 Spring Boot+微服务,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货! ...

April 19, 2019 · 2 min · jiezi

spring boot 集成swagger并且使用拦截器的配置问题

最近同事问我,spring boot集成了swagger,但是在使用拦截器的时候遇到了问题,页面无法访问。经过研究解决了这个问题。配置问题解决集成swagger就不啰嗦了,网上到处都是,直接看配置。同事从网上找到的配置:import com.xxx.xxxx.xxx.xxx.LoginInterceptor;import com.fasterxml.classmate.TypeResolver;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.ResponseEntity;import org.springframework.web.context.request.async.DeferredResult;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;import springfox.documentation.builders.ApiInfoBuilder;import springfox.documentation.builders.PathSelectors;import springfox.documentation.builders.RequestHandlerSelectors;import springfox.documentation.schema.WildcardType;import springfox.documentation.service.ApiInfo;import springfox.documentation.service.Contact;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.Docket;import springfox.documentation.swagger2.annotations.EnableSwagger2;import java.util.Collections;import static springfox.documentation.schema.AlternateTypeRules.newRule;@Configuration@EnableSwagger2public class Swagger2Config extends WebMvcConfigurationSupport { @Autowired private TypeResolver typeResolver; @Bean public Docket productApi() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(“com.xxx.xxx.controller”)) .paths(PathSelectors.any()) .build() .apiInfo(metaData()) .alternateTypeRules( //自定义规则,如果遇到DeferredResult,则把泛型类转成json newRule(typeResolver.resolve(DeferredResult.class, typeResolver.resolve(ResponseEntity.class, WildcardType.class)), typeResolver.resolve(WildcardType.class))) ; } private ApiInfo metaData() { return new ApiInfoBuilder() .title(“通用服务 APIs”) /.description(""REST API for Online Store"")/ .version(“1.0.0”) /* .license(“Apache License Version 2.0”) .licenseUrl(“https://www.apache.org/licenses/LICENSE-2.0"”)/ .contact(new Contact(“易保科技”, “”, “mail@mail”)) .build(); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(“swagger-ui.html”) .addResourceLocations(“classpath:/META-INF/resources/”); registry.addResourceHandler("/webjars/") .addResourceLocations(“classpath:/META-INF/resources/webjars/”); super.addResourceHandlers(registry); }}这是他从网上找到的拦截器的配置:@Configurationpublic class WebMvcConfig extends WebMvcConfigurationSupport { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) // addPathPatterns 用于添加拦截规则 , 先把所有路径都加入拦截, 再一个个排除 .addPathPatterns("/") .excludePathPatterns(Collections.singletonList("/swagger-ui.html")); super.addInterceptors(registry); }}现在的测试结果就是,打开http://host:port/path/swagger-ui.html,就是一个空白页面,无法使用,现在要解决的就是这个问题。打开谷歌浏览器的调试控制台,查看network,如图:可以明显看出,页面加载数据的时候,并没有报什么错误,只是加载的资源都被拦截器拦截了,无法加载资源,可想而知,资源都被拦截器拦截了。我分析了一下,加载资源的路径,修改了一下拦截器资源配置: @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) // addPathPatterns 用于添加拦截规则 , 先把所有路径都加入拦截, 再一个个排除 .addPathPatterns("/") .excludePathPatterns("/swagger-ui.html") .excludePathPatterns("/swagger-resources/") .excludePathPatterns("/error") .excludePathPatterns("/webjars/"); }另外两个类实际是同一个作用,所以合并两个类:@Configuration@EnableSwagger2public class TestMVCConfig extends WebMvcConfigurationSupport { @Resource private TypeResolver typeResolver; @Resource private LoginInterceptor loginInterceptor; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(“swagger-ui.html”) .addResourceLocations(“classpath:/META-INF/resources/”); registry.addResourceHandler("/webjars/") .addResourceLocations(“classpath:/META-INF/resources/webjars/”); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) // addPathPatterns 用于添加拦截规则 , 先把所有路径都加入拦截, 再一个个排除 .addPathPatterns("/") .excludePathPatterns("/swagger-ui.html") .excludePathPatterns("/swagger-resources/") .excludePathPatterns("/error") .excludePathPatterns("/webjars/**"); } @Bean public Docket productApi() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(“com.xxx.xxx.controller”)) .paths(PathSelectors.any()) .build() .apiInfo(metaData()) .alternateTypeRules( //自定义规则,如果遇到DeferredResult,则把泛型类转成json newRule(typeResolver.resolve(DeferredResult.class, typeResolver.resolve(ResponseEntity.class, WildcardType.class)), typeResolver.resolve(WildcardType.class))) ; } private ApiInfo metaData() { return new ApiInfoBuilder() .title(“通用服务 APIs”) /.description(""REST API for Online Store"")/ .version(“1.0.0”) / .license(“Apache License Version 2.0”) .licenseUrl(“https://www.apache.org/licenses/LICENSE-2.0"”)/ .contact(new Contact(“易保科技”, “”, “mail@mail”)) .build(); }}这样就没有问题了。另外的解决方案网上还有另外一种说法,可以实现WebMvcConfigurer接口,代码如下:@Configuration@EnableWebMvc@EnableSwagger2public class WebMVCConfig implements WebMvcConfigurer{ @Resource private TypeResolver typeResolver; @Resource private LoginInterceptor loginInterceptor; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(“swagger-ui.html”) .addResourceLocations(“classpath:/META-INF/resources/”); registry.addResourceHandler("/webjars/") .addResourceLocations(“classpath:/META-INF/resources/webjars/”); } @Override public void addInterceptors(InterceptorRegistry registry) { List<String> excludePathPatterns = new ArrayList<>(); excludePathPatterns.add("/swagger-ui.html"); excludePathPatterns.add("/swagger-resources/"); excludePathPatterns.add("/error"); excludePathPatterns.add("/webjars/"); // addPathPatterns 用于添加拦截规则 , 先把所有路径都加入拦截, 再一个个排除 registry.addInterceptor(loginInterceptor) .addPathPatterns("/") .excludePathPatterns(excludePathPatterns); } @Bean public Docket productApi() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(“com.xx.sss.controller”)) .paths(PathSelectors.any()) .build() .apiInfo(metaData()) .alternateTypeRules( //自定义规则,如果遇到DeferredResult,则把泛型类转成json newRule(typeResolver.resolve(DeferredResult.class, typeResolver.resolve(ResponseEntity.class, WildcardType.class)), typeResolver.resolve(WildcardType.class))) ; } private ApiInfo metaData() { return new ApiInfoBuilder() .title(“通用服务 APIs”) /.description(""REST API for Online Store"")/ .version(“1.0.0”) / .license(“Apache License Version 2.0”) .licenseUrl(“https://www.apache.org/licenses/LICENSE-2.0"”)*/ .contact(new Contact(“易保科技”, “”, “mail@mail”)) .build(); }}但是这种配置想要生效,必须加@EnableWebMvc注解,不然不起作用。为什么?官方源码注释:/** * Defines callback methods to customize the Java-based configuration for * Spring MVC enabled via {@code @EnableWebMvc}. * * <p>{@code @EnableWebMvc}-annotated configuration classes may implement * this interface to be called back and given a chance to customize the * default configuration. *通过@enableWebMVC启用Spring MVC,自定义基于Java config 定义回调方法。@EnableWebMVC带注释的配置类可以实现这个接口自定义默认配置。当然如果你觉得自己的配置没问题,但是仍然不起作用,这时候改怎么办?请按照一下步骤debug:1、找到InterceptorRegistration类;2、找到addInterceptor方法和excludePathPatterns方法,打上断点;3、debug模式启动项目;如果没有进入断点,那就说明你的配置根本没有起到作用,看看注解是否没写。如果进入了断点,就要看看断点处传进来的参数是否是你配置的参数,不是那就是有问题,这时候再根据参数查找问题。这样基本就能解决问题了。总结网上很多东西都是抄来抄去,也不知道有没有验证,让很多人摸不着头脑。 ...

April 18, 2019 · 2 min · jiezi

Spring Boot 中实现定时任务的两种方式

在 Spring + SpringMVC 环境中,一般来说,要实现定时任务,我们有两中方案,一种是使用 Spring 自带的定时任务处理器 @Scheduled 注解,另一种就是使用第三方框架 Quartz ,Spring Boot 源自 Spring+SpringMVC ,因此天然具备这两个 Spring 中的定时任务实现策略,当然也支持 Quartz,本文我们就来看下 Spring Boot 中两种定时任务的实现方式。@Scheduled使用 @Scheduled 非常容易,直接创建一个 Spring Boot 项目,并且添加 web 依赖 spring-boot-starter-web,项目创建成功后,添加 @EnableScheduling 注解,开启定时任务:@SpringBootApplication@EnableSchedulingpublic class ScheduledApplication { public static void main(String[] args) { SpringApplication.run(ScheduledApplication.class, args); }}接下来配置定时任务: @Scheduled(fixedRate = 2000) public void fixedRate() { System.out.println(“fixedRate>>>"+new Date()); } @Scheduled(fixedDelay = 2000) public void fixedDelay() { System.out.println(“fixedDelay>>>"+new Date()); } @Scheduled(initialDelay = 2000,fixedDelay = 2000) public void initialDelay() { System.out.println(“initialDelay>>>"+new Date()); }首先使用 @Scheduled 注解开启一个定时任务。fixedRate 表示任务执行之间的时间间隔,具体是指两次任务的开始时间间隔,即第二次任务开始时,第一次任务可能还没结束。fixedDelay 表示任务执行之间的时间间隔,具体是指本次任务结束到下次任务开始之间的时间间隔。initialDelay 表示首次任务启动的延迟时间。所有时间的单位都是毫秒。上面这是一个基本用法,除了这几个基本属性之外,@Scheduled 注解也支持 cron 表达式,使用 cron 表达式,可以非常丰富的描述定时任务的时间。cron 表达式格式如下:[秒] [分] [小时] [日] [月] [周] [年]具体取值如下:序号说明是否必填允许填写的值允许的通配符1秒是0-59- * /2分是0-59- * /3时是0-23- * /4日是1-31- * ? / L W5月是1-12 or JAN-DEC- * /6周是1-7 or SUN-SAT- * ? / L #7年否1970-2099- * /这一块需要大家注意的是,月份中的日期和星期可能会起冲突,因此在配置时这两个得有一个是 ?通配符含义:? 表示不指定值,即不关心某个字段的取值时使用。需要注意的是,月份中的日期和星期可能会起冲突,因此在配置时这两个得有一个是 ?* 表示所有值,例如:在秒的字段上设置 *,表示每一秒都会触发, 用来分开多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发- 表示区间,例如在秒上设置 “10-12”,表示 10,11,12秒都会触发/ 用于递增触发,如在秒上面设置"5/15” 表示从5秒开始,每增15秒触发(5,20,35,50)# 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六,(用 在母亲节和父亲节再合适不过了)周字段的设置,若使用英文字母是不区分大小写的 ,即 MON 与mon相同L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会自动判断是否是润年), 在周字段上表示星期六,相当于"7"或"SAT”(注意周日算是第一天)。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示"本月最后一个星期五"W 表示离指定日期的最近工作日(周一至周五),例如在日字段上设置"15W”,表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发,如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,“W"前只能设置具体的数字,不允许区间”-")L 和 W 可以一组合使用。如果在日字段上设置"LW",则表示在本月的最后一个工作日触发(一般指发工资 )例如,在 @Scheduled 注解中来一个简单的 cron 表达式,每隔5秒触发一次,如下:@Scheduled(cron = “0/5 * * * * *")public void cron() { System.out.println(new Date());}上面介绍的是使用 @Scheduled 注解的方式来实现定时任务,接下来我们再来看看如何使用 Quartz 实现定时任务。Quartz一般在项目中,除非定时任务涉及到的业务实在是太简单,使用 @Scheduled 注解来解决定时任务,否则大部分情况可能都是使用 Quartz 来做定时任务。在 Spring Boot 中使用 Quartz ,只需要在创建项目时,添加 Quartz 依赖即可:项目创建完成后,也需要添加开启定时任务的注解:@SpringBootApplication@EnableSchedulingpublic class QuartzApplication { public static void main(String[] args) { SpringApplication.run(QuartzApplication.class, args); }}Quartz 在使用过程中,有两个关键概念,一个是JobDetail(要做的事情),另一个是触发器(什么时候做),要定义 JobDetail,需要先定义 Job,Job 的定义有两种方式:第一种方式,直接定义一个Bean:@Componentpublic class MyJob1 { public void sayHello() { System.out.println(“MyJob1>>>"+new Date()); }}关于这种定义方式说两点:首先将这个 Job 注册到 Spring 容器中。这种定义方式有一个缺陷,就是无法传参。第二种定义方式,则是继承 QuartzJobBean 并实现默认的方法:public class MyJob2 extends QuartzJobBean { HelloService helloService; public HelloService getHelloService() { return helloService; } public void setHelloService(HelloService helloService) { this.helloService = helloService; } @Override protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException { helloService.sayHello(); }}public class HelloService { public void sayHello() { System.out.println(“hello service >>>"+new Date()); }}和第1种方式相比,这种方式支持传参,任务启动时,executeInternal 方法将会被执行。Job 有了之后,接下来创建类,配置 JobDetail 和 Trigger 触发器,如下:@Configurationpublic class QuartzConfig { @Bean MethodInvokingJobDetailFactoryBean methodInvokingJobDetailFactoryBean() { MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean(); bean.setTargetBeanName(“myJob1”); bean.setTargetMethod(“sayHello”); return bean; } @Bean JobDetailFactoryBean jobDetailFactoryBean() { JobDetailFactoryBean bean = new JobDetailFactoryBean(); bean.setJobClass(MyJob2.class); JobDataMap map = new JobDataMap(); map.put(“helloService”, helloService()); bean.setJobDataMap(map); return bean; } @Bean SimpleTriggerFactoryBean simpleTriggerFactoryBean() { SimpleTriggerFactoryBean bean = new SimpleTriggerFactoryBean(); bean.setStartTime(new Date()); bean.setRepeatCount(5); bean.setJobDetail(methodInvokingJobDetailFactoryBean().getObject()); bean.setRepeatInterval(3000); return bean; } @Bean CronTriggerFactoryBean cronTrigger() { CronTriggerFactoryBean bean = new CronTriggerFactoryBean(); bean.setCronExpression(“0/10 * * * * ?”); bean.setJobDetail(jobDetailFactoryBean().getObject()); return bean; } @Bean SchedulerFactoryBean schedulerFactoryBean() { SchedulerFactoryBean bean = new SchedulerFactoryBean(); bean.setTriggers(cronTrigger().getObject(), simpleTriggerFactoryBean().getObject()); return bean; } @Bean HelloService helloService() { return new HelloService(); }}关于这个配置说如下几点:JobDetail 的配置有两种方式:MethodInvokingJobDetailFactoryBean 和 JobDetailFactoryBean 。使用 MethodInvokingJobDetailFactoryBean 可以配置目标 Bean 的名字和目标方法的名字,这种方式不支持传参。使用 JobDetailFactoryBean 可以配置 JobDetail ,任务类继承自 QuartzJobBean ,这种方式支持传参,将参数封装在 JobDataMap 中进行传递。Trigger 是指触发器,Quartz 中定义了多个触发器,这里向大家展示其中两种的用法,SimpleTrigger 和 CronTrigger 。SimpleTrigger 有点类似于前面说的 @Scheduled 的基本用法。CronTrigger 则有点类似于 @Scheduled 中 cron 表达式的用法。全部定义完成后,启动 Spring Boot 项目就可以看到定时任务的执行了。总结这里主要向大家展示了 Spring Boot 中整合两种定时任务的方法,整合成功之后,剩下的用法基本上就和在 SSM 中使用一致了,不再赘述。 关注公众号牧码小子,专注于 Spring Boot+微服务,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货! ...

April 18, 2019 · 2 min · jiezi

聊聊springboot elasticsearch autoconfigure

序本文主要研究一下springboot elasticsearch autoconfigureElasticsearchAutoConfigurationspring-boot-autoconfigure-2.1.4.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchAutoConfiguration.java@Configuration@ConditionalOnClass({ Client.class, TransportClientFactoryBean.class })@ConditionalOnProperty(prefix = “spring.data.elasticsearch”, name = “cluster-nodes”, matchIfMissing = false)@EnableConfigurationProperties(ElasticsearchProperties.class)public class ElasticsearchAutoConfiguration { private final ElasticsearchProperties properties; public ElasticsearchAutoConfiguration(ElasticsearchProperties properties) { this.properties = properties; } @Bean @ConditionalOnMissingBean public TransportClient elasticsearchClient() throws Exception { TransportClientFactoryBean factory = new TransportClientFactoryBean(); factory.setClusterNodes(this.properties.getClusterNodes()); factory.setProperties(createProperties()); factory.afterPropertiesSet(); return factory.getObject(); } private Properties createProperties() { Properties properties = new Properties(); properties.put(“cluster.name”, this.properties.getClusterName()); properties.putAll(this.properties.getProperties()); return properties; }}ElasticsearchAutoConfiguration创建了TransportClientElasticsearchRepositoriesAutoConfigurationspring-boot-autoconfigure-2.1.4.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchRepositoriesAutoConfiguration.java@Configuration@ConditionalOnClass({ Client.class, ElasticsearchRepository.class })@ConditionalOnProperty(prefix = “spring.data.elasticsearch.repositories”, name = “enabled”, havingValue = “true”, matchIfMissing = true)@ConditionalOnMissingBean(ElasticsearchRepositoryFactoryBean.class)@Import(ElasticsearchRepositoriesRegistrar.class)public class ElasticsearchRepositoriesAutoConfiguration {}ElasticsearchRepositoriesAutoConfiguration主要是导入了ElasticsearchRepositoriesRegistrarElasticsearchRepositoriesRegistrarspring-boot-autoconfigure-2.1.4.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchRepositoriesRegistrar.javaclass ElasticsearchRepositoriesRegistrar extends AbstractRepositoryConfigurationSourceSupport { @Override protected Class<? extends Annotation> getAnnotation() { return EnableElasticsearchRepositories.class; } @Override protected Class<?> getConfiguration() { return EnableElasticsearchRepositoriesConfiguration.class; } @Override protected RepositoryConfigurationExtension getRepositoryConfigurationExtension() { return new ElasticsearchRepositoryConfigExtension(); } @EnableElasticsearchRepositories private static class EnableElasticsearchRepositoriesConfiguration { }}ElasticsearchRepositoriesRegistrar这里覆盖了getRepositoryConfigurationExtension方法,返回ElasticsearchRepositoryConfigExtensionElasticsearchRepositoryConfigExtensionspring-data-elasticsearch-3.1.6.RELEASE-sources.jar!/org/springframework/data/elasticsearch/repository/config/ElasticsearchRepositoryConfigExtension.javapublic class ElasticsearchRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getRepositoryFactoryBeanClassName() / @Override public String getRepositoryFactoryBeanClassName() { return ElasticsearchRepositoryFactoryBean.class.getName(); } / * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModulePrefix() / @Override protected String getModulePrefix() { return “elasticsearch”; } / * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource) / @Override public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) { AnnotationAttributes attributes = config.getAttributes(); builder.addPropertyReference(“elasticsearchOperations”, attributes.getString(“elasticsearchTemplateRef”)); } / * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.XmlRepositoryConfigurationSource) / @Override public void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource config) { Element element = config.getElement(); builder.addPropertyReference(“elasticsearchOperations”, element.getAttribute(“elasticsearch-template-ref”)); } / * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getIdentifyingAnnotations() / @Override protected Collection<Class<? extends Annotation>> getIdentifyingAnnotations() { return Collections.<Class<? extends Annotation>> singleton(Document.class); } / * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getIdentifyingTypes() */ @Override protected Collection<Class<?>> getIdentifyingTypes() { return Arrays.<Class<?>> asList(ElasticsearchRepository.class, ElasticsearchCrudRepository.class); }}ElasticsearchRepositoryConfigExtension覆盖了getIdentifyingTypes方法,返回的是ElasticsearchCrudRepository.class、ElasticsearchRepository.classElasticsearchCrudRepositoryspring-data-elasticsearch-3.1.6.RELEASE-sources.jar!/org/springframework/data/elasticsearch/repository/ElasticsearchCrudRepository.java@NoRepositoryBeanpublic interface ElasticsearchCrudRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> {}ElasticsearchCrudRepository接口继承自PagingAndSortingRepositoryElasticsearchRepositoryspring-data-elasticsearch-3.1.6.RELEASE-sources.jar!/org/springframework/data/elasticsearch/repository/ElasticsearchRepository.java@NoRepositoryBeanpublic interface ElasticsearchRepository<T, ID extends Serializable> extends ElasticsearchCrudRepository<T, ID> { <S extends T> S index(S entity); Iterable<T> search(QueryBuilder query); Page<T> search(QueryBuilder query, Pageable pageable); Page<T> search(SearchQuery searchQuery); Page<T> searchSimilar(T entity, String[] fields, Pageable pageable); void refresh(); Class<T> getEntityClass();}ElasticsearchRepository继承了ElasticsearchCrudRepository,支持了index、search、searchSimilar、refresh、getEntityClass方法ElasticsearchDataAutoConfigurationspring-boot-autoconfigure-2.1.4.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/data/elasticsearch/ElasticsearchDataAutoConfiguration.java@Configuration@ConditionalOnClass({ Client.class, ElasticsearchTemplate.class })@AutoConfigureAfter(ElasticsearchAutoConfiguration.class)public class ElasticsearchDataAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnBean(Client.class) public ElasticsearchTemplate elasticsearchTemplate(Client client, ElasticsearchConverter converter) { try { return new ElasticsearchTemplate(client, converter); } catch (Exception ex) { throw new IllegalStateException(ex); } } @Bean @ConditionalOnMissingBean public ElasticsearchConverter elasticsearchConverter( SimpleElasticsearchMappingContext mappingContext) { return new MappingElasticsearchConverter(mappingContext); } @Bean @ConditionalOnMissingBean public SimpleElasticsearchMappingContext mappingContext() { return new SimpleElasticsearchMappingContext(); }}ElasticsearchDataAutoConfiguration主要创建了ElasticsearchTemplate;创建SimpleElasticsearchMappingContext是为了创建ElasticsearchConverter,而创建ElasticsearchConverter是因为创建ElasticsearchTemplate需要ElasticsearchConverter小结spring-boot-autoconfigure module给elasticsearch提供了三个auto configuration,分别是ElasticsearchAutoConfiguration、ElasticsearchRepositoriesAutoConfiguration、ElasticsearchDataAutoConfigurationElasticsearchAutoConfiguration创建了TransportClient;ElasticsearchRepositoriesAutoConfiguration主要是导入了ElasticsearchRepositoriesRegistrar,给@EnableElasticsearchRepositories注解提供支持ElasticsearchDataAutoConfiguration主要创建了ElasticsearchTemplate;创建SimpleElasticsearchMappingContext是为了创建ElasticsearchConverter,而创建ElasticsearchConverter是因为创建ElasticsearchTemplate需要ElasticsearchConverterdocspring-boot-autoconfigure module ...

April 17, 2019 · 2 min · jiezi

Spring Boot 中关于自定义异常处理的套路!

在 Spring Boot 项目中 ,异常统一处理,可以使用 Spring 中 @ControllerAdvice 来统一处理,也可以自己来定义异常处理方案。Spring Boot 中,对异常的处理有一些默认的策略,我们分别来看。 默认情况下,Spring Boot 中的异常页面 是这样的: 我们从这个异常提示中,也能看出来,之所以用户看到这个页面,是因为开发者没有明确提供一个 /error 路径,如果开发者提供了 /error 路径 ,这个页面就不会展示出来,不过在 Spring Boot 中,提供 /error 路径实际上是下下策,Spring Boot 本身在处理异常时,也是当所有条件都不满足时,才会去找 /error 路径。那么我们就先来看看,在 Spring Boot 中,如何自定义 error 页面,整体上来说,可以分为两种,一种是静态页面,另一种是动态页面。静态异常页面自定义静态异常页面,又分为两种,第一种 是使用 HTTP 响应码来命名页面,例如 404.html、405.html、500.html ….,另一种就是直接定义一个 4xx.html,表示400-499 的状态都显示这个异常页面,5xx.html 表示 500-599 的状态显示这个异常页面。 默认是在 classpath:/static/error/ 路径下定义相关页面: 此时,启动项目,如果项目抛出 500 请求错误,就会自动展示 500.html 这个页面,发生 404 就会展示 404.html 页面。如果异常展示页面既存在 5xx.html,也存在 500.html ,此时,发生500异常时,优先展示 500.html 页面。动态异常页面动态的异常页面定义方式和静态的基本 一致,可以采用的页面模板有 jsp、freemarker、thymeleaf。动态异常页面,也支持 404.html 或者 4xx.html ,但是一般来说,由于动态异常页面可以直接展示异常详细信息,所以就没有必要挨个枚举错误了 ,直接定义 4xx.html(这里使用thymeleaf模板)或者 5xx.html 即可。 注意,动态页面模板,不需要开发者自己去定义控制器,直接定义异常页面即可 ,Spring Boot 中自带的异常处理器会自动查找到异常页面。 页面定义如下:页面内容如下:<!DOCTYPE html><html lang=“en” xmlns:th=“http://www.thymeleaf.org”><head> <meta charset=“UTF-8”> <title>Title</title></head><body><h1>5xx</h1><table border=“1”> <tr> <td>path</td> <td th:text="${path}"></td> </tr> <tr> <td>error</td> <td th:text="${error}"></td> </tr> <tr> <td>message</td> <td th:text="${message}"></td> </tr> <tr> <td>timestamp</td> <td th:text="${timestamp}"></td> </tr> <tr> <td>status</td> <td th:text="${status}"></td> </tr></table></body></html>默认情况下,完整的异常信息就是这5条,展示 效果如下 : 如果动态页面和静态页面同时定义了异常处理页面,例如 classpath:/static/error/404.html 和 classpath:/templates/error/404.html 同时存在时,默认使用动态页面。即完整的错误页面查找方式应该是这样: 发生了500错误–>查找动态 500.html 页面–>查找静态 500.html –> 查找动态 5xx.html–>查找静态 5xx.html。自定义异常数据默认情况下,在Spring Boot 中,所有的异常数据其实就是上文所展示出来的5条数据,这5条数据定义在 org.springframework.boot.web.reactive.error.DefaultErrorAttributes 类中,具体定义在 getErrorAttributes 方法中 :@Overridepublic Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<>(); errorAttributes.put(“timestamp”, new Date()); errorAttributes.put(“path”, request.path()); Throwable error = getError(request); HttpStatus errorStatus = determineHttpStatus(error); errorAttributes.put(“status”, errorStatus.value()); errorAttributes.put(“error”, errorStatus.getReasonPhrase()); errorAttributes.put(“message”, determineMessage(error)); handleException(errorAttributes, determineException(error), includeStackTrace); return errorAttributes;}DefaultErrorAttributes 类本身则是在org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration 异常自动配置类中定义的,如果开发者没有自己提供一个 ErrorAttributes 的实例的话,那么 Spring Boot 将自动提供一个ErrorAttributes 的实例,也就是 DefaultErrorAttributes 。 基于此 ,开发者自定义 ErrorAttributes 有两种方式 :直接实现 ErrorAttributes 接口继承 DefaultErrorAttributes(推荐),因为 DefaultErrorAttributes 中对异常数据的处理已经完成,开发者可以直接使用。具体定义如下:@Componentpublic class MyErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace); if ((Integer)map.get(“status”) == 500) { map.put(“message”, “服务器内部错误!”); } return map; }}定义好的 ErrorAttributes 一定要注册成一个 Bean ,这样,Spring Boot 就不会使用默认的 DefaultErrorAttributes 了,运行效果如下图: 自定义异常视图异常视图默认就是前面所说的静态或者动态页面,这个也是可以自定义的,首先 ,默认的异常视图加载逻辑在 org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController 类的 errorHtml 方法中,这个方法用来返回异常页面+数据,还有另外一个 error 方法,这个方法用来返回异常数据(如果是 ajax 请求,则该方法会被触发)。@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView(“error”, model);}在该方法中 ,首先会通过 getErrorAttributes 方法去获取异常数据(实际上会调用到 ErrorAttributes 的实例 的 getErrorAttributes 方法),然后调用 resolveErrorView 去创建一个 ModelAndView ,如果这里创建失败,那么用户将会看到默认的错误提示页面。 正常情况下, resolveErrorView 方法会来到 DefaultErrorViewResolver 类的 resolveErrorView 方法中:@Overridepublic ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { ModelAndView modelAndView = resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView;}在这里,首先以异常响应码作为视图名分别去查找动态页面和静态页面,如果没有查找到,则再以 4xx 或者 5xx 作为视图名再去分别查找动态或者静态页面。 要自定义异常视图解析,也很容易 ,由于 DefaultErrorViewResolver 是在 ErrorMvcAutoConfiguration 类中提供的实例,即开发者没有提供相关实例时,会使用默认的 DefaultErrorViewResolver ,开发者提供了自己的 ErrorViewResolver 实例后,默认的配置就会失效,因此,自定义异常视图,只需要提供 一个 ErrorViewResolver 的实例即可:@Componentpublic class MyErrorViewResolver extends DefaultErrorViewResolver { public MyErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) { super(applicationContext, resourceProperties); } @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { return new ModelAndView("/aaa/123", model); }}实际上,开发者也可以在这里定义异常数据(直接在 resolveErrorView 方法重新定义一个 model ,将参数中的model 数据拷贝过去并修改,注意参数中的 model 类型为 UnmodifiableMap,即不可以直接修改),而不需要自定义MyErrorAttributes。定义完成后,提供一个名为123的视图,如下图: 如此之后,错误试图就算定义成功了。总结实际上也可以自定义异常控制器 BasicErrorController ,不过松哥觉得这样太大动干戈了,没必要,前面几种方式已经可以满足我们的大部分开发需求了。 关注公众号牧码小子,专注于 Spring Boot+微服务,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货! ...

April 17, 2019 · 2 min · jiezi

SpringBoot JWT Token 跨域 Preflight response is not successful

一、Springboot实现token校验SpringBoot实现token校验,可以通过Filter或者HandlerInterceptor,两种方式都可以,Filter在最外层,请求首先会通过Filter,filter允许请求才会通过Intercept。下面以HandlerInterceptor实现为例1.实现HandlerInterceptor,拦截请求校验tokenpublic class AuthenticationInterceptor implements HandlerInterceptor { private static final String URI_PASS_TOKEN = “/user/login”; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { log.info(“authentication interceptor preHandle path:{} uri:{}",httpServletRequest.getServletPath(),httpServletRequest.getRequestURI());// if (“OPTIONS”.equalsIgnoreCase(httpServletRequest.getMethod())) {// return true;// } if (httpServletRequest.getRequestURI().endsWith(URI_PASS_TOKEN)) { return true; } //从http header里面获取token String token = httpServletRequest.getHeader(“token”); if (StringUtils.isEmpty(token)) { throw new AuthenticationException(CODE_AUTHENTICATION_FAILED,“token is empty”); } Algorithm algorithm = Algorithm.HMAC256(JwtConstant.TOKEN_CREATE_SECRET); JWTVerifier verifier = JWT.require(algorithm).build(); try { verifier.verify(token); }catch (Exception ex){ throw new AuthenticationException(CODE_AUTHENTICATION_FAILED,ex.getMessage()); } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { }}2.Configuration配置,实现自动注入@Configurationpublic class InterceptorConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns(”/**"); } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); }}二、前端调用 跨域 Preflight response is not successful通过单元测试、PostMan测试都可以调同,但是vue前端怎么都无法调用,错误如下:参考https://segmentfault.com/a/11…发现是浏览器发出的OPTIONS预检请求被HandlerInterceptor拦截了,因此在HandlerInterceptor添加如下代码: if (“OPTIONS”.equalsIgnoreCase(httpServletRequest.getMethod())) { return true; }对于options的请求不进行token检测即可 ...

April 16, 2019 · 1 min · jiezi

【SpringSecurity系列02】SpringSecurity 表单认证逻辑源码解读

概要前面一节,通过简单配置即可实现SpringSecurity表单认证功能,而今天这一节将通过阅读源码的形式来学习SpringSecurity是如何实现这些功能, 前方高能预警,本篇分析源码篇幅较长。<!– more –>过滤器链前面我说过SpringSecurity是基于过滤器链的形式,那么我解析将会介绍一下具体有哪些过滤器。Filter Class介绍SecurityContextPersistenceFilter判断当前用户是否登录CrsfFilter用于防止csrf攻击LogoutFilter处理注销请求UsernamePasswordAuthenticationFilter处理表单登录的请求(也是我们今天的主角)BasicAuthenticationFilter处理http basic认证的请求由于过滤器链中的过滤器实在太多,我没有一一列举,调了几个比较重要的介绍一下。通过上面我们知道SpringSecurity对于表单登录的认证请求是交给了UsernamePasswordAuthenticationFilter处理的,那么具体的认证流程如下:从上图可知,UsernamePasswordAuthenticationFilter继承于抽象类AbstractAuthenticationProcessingFilter。具体认证是:进入doFilter方法,判断是否要认证,如果需要认证则进入attemptAuthentication方法,如果不需要直接结束attemptAuthentication方法中根据username跟password构造一个UsernamePasswordAuthenticationToken对象(此时的token是未认证的),并且将它交给ProviderManger来完成认证。ProviderManger中维护这一个AuthenticationProvider对象列表,通过遍历判断并且最后选择DaoAuthenticationProvider对象来完成最后的认证。DaoAuthenticationProvider根据ProviderManger传来的token取出username,并且调用我们写的UserDetailsService的loadUserByUsername方法从数据库中读取用户信息,然后对比用户密码,如果认证通过,则返回用户信息也是就是UserDetails对象,在重新构造UsernamePasswordAuthenticationToken(此时的token是 已经认证通过了的)。接下来我们将通过源码来分析具体的整个认证流程。AbstractAuthenticationProcessingFilterAbstractAuthenticationProcessingFilter 是一个抽象类。所有的认证认证请求的过滤器都会继承于它,它主要将一些公共的功能实现,而具体的验证逻辑交给子类实现,有点类似于父类设置好认证流程,子类负责具体的认证逻辑,这样跟设计模式的模板方法模式有点相似。现在我们分析一下 它里面比较重要的方法1、doFilterpublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // 省略不相干代码。。。 // 1、判断当前请求是否要认证 if (!requiresAuthentication(request, response)) { // 不需要直接走下一个过滤器 chain.doFilter(request, response); return; } try { // 2、开始请求认证,attemptAuthentication具体实现给子类,如果认证成功返回一个认证通过的Authenticaion对象 authResult = attemptAuthentication(request, response); if (authResult == null) { return; } // 3、登录成功 将认证成功的用户信息放入session SessionAuthenticationStrategy接口,用于扩展 sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { //2.1、发生异常,登录失败,进入登录失败handler回调 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { //2.1、发生异常,登录失败,进入登录失败处理器 unsuccessfulAuthentication(request, response, failed); return; } // 3.1、登录成功,进入登录成功处理器。 successfulAuthentication(request, response, chain, authResult); }2、successfulAuthentication登录成功处理器protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //1、登录成功 将认证成功的Authentication对象存入SecurityContextHolder中 // SecurityContextHolder本质是一个ThreadLocal SecurityContextHolder.getContext().setAuthentication(authResult); //2、如果开启了记住我功能,将调用rememberMeServices的loginSuccess 将生成一个token // 将token放入cookie中这样 下次就不用登录就可以认证。具体关于记住我rememberMeServices的相关分析我 们下面几篇文章会深入分析的。 rememberMeServices.loginSuccess(request, response, authResult); // Fire event //3、发布一个登录事件。 if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //4、调用我们自己定义的登录成功处理器,这样也是我们扩展得知登录成功的一个扩展点。 successHandler.onAuthenticationSuccess(request, response, authResult); }3、unsuccessfulAuthentication登录失败处理器protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { //1、登录失败,将SecurityContextHolder中的信息清空 SecurityContextHolder.clearContext(); //2、关于记住我功能的登录失败处理 rememberMeServices.loginFail(request, response); //3、调用我们自己定义的登录失败处理器,这里可以扩展记录登录失败的日志。 failureHandler.onAuthenticationFailure(request, response, failed); }关于AbstractAuthenticationProcessingFilter主要分析就到这。我们可以从源码中知道,当请求进入该过滤器中具体的流程是判断该请求是否要被认证调用attemptAuthentication方法开始认证,由于是抽象方法具体认证逻辑给子类如果登录成功,则将认证结果Authentication对象根据session策略写入session中,将认证结果写入到SecurityContextHolder,如果开启了记住我功能,则根据记住我功能,生成token并且写入cookie中,最后调用一个successHandler对象的方法,这个对象可以是我们配置注入的,用于处理我们的自定义登录成功的一些逻辑(比如记录登录成功日志等等)。如果登录失败,则清空SecurityContextHolder中的信息,并且调用我们自己注入的failureHandler对象,处理我们自己的登录失败逻辑。UsernamePasswordAuthenticationFilter从上面分析我们可以知道,UsernamePasswordAuthenticationFilter是继承于AbstractAuthenticationProcessingFilter,并且实现它的attemptAuthentication方法,来实现认证具体的逻辑实现。接下来,我们通过阅读UsernamePasswordAuthenticationFilter的源码来解读,它是如何完成认证的。 由于这里会涉及UsernamePasswordAuthenticationToken对象构造,所以我们先看看UsernamePasswordAuthenticationToken的源码1、UsernamePasswordAuthenticationToken// 继承至AbstractAuthenticationToken // AbstractAuthenticationToken主要定义一下在SpringSecurity中toke需要存在一些必须信息// 例如权限集合 Collection<GrantedAuthority> authorities; 是否认证通过boolean authenticated = false;认证通过的用户信息Object details;public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { // 未登录情况下 存的是用户名 登录成功情况下存的是UserDetails对象 private final Object principal; // 密码 private Object credentials; /** * 构造函数,用户没有登录的情况下,此时的authenticated是false,代表尚未认证 / public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } /* * 构造函数,用户登录成功的情况下,多了一个参数 是用户的权限集合,此时的authenticated是true,代表认证成功 / public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); // must use super, as we override }}接下来我们就可以分析attemptAuthentication方法了。2、attemptAuthenticationpublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 1、判断是不是post请求,如果不是则抛出AuthenticationServiceException异常,注意这里抛出的异常都在AbstractAuthenticationProcessingFilter#doFilter方法中捕获,捕获之后会进入登录失败的逻辑。 if (postOnly && !request.getMethod().equals(“POST”)) { throw new AuthenticationServiceException( “Authentication method not supported: " + request.getMethod()); } // 2、从request中拿用户名跟密码 String username = obtainUsername(request); String password = obtainPassword(request); // 3、非空处理,防止NPE异常 if (username == null) { username = “”; } if (password == null) { password = “”; } // 4、除去空格 username = username.trim(); // 5、根据username跟password构造出一个UsernamePasswordAuthenticationToken对象 从上文分析可知道,此时的token是未认证的。 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // 6、配置一下其他信息 ip 等等 setDetails(request, authRequest); // 7、调用ProviderManger的authenticate的方法进行具体认证逻辑 return this.getAuthenticationManager().authenticate(authRequest); }ProviderManager维护一个AuthenticationProvider列表,进行认证逻辑验证1、authenticatepublic Authentication authenticate(Authentication authentication) throws AuthenticationException { // 1、拿到token的类型。 Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; // 2、遍历AuthenticationProvider列表 for (AuthenticationProvider provider : getProviders()) { // 3、AuthenticationProvider不支持当前token类型,则直接跳过 if (!provider.supports(toTest)) { continue; } try { // 4、如果Provider支持当前token,则交给Provider完成认证。 result = provider.authenticate(authentication); } catch (AccountStatusException e) { throw e; } catch (InternalAuthenticationServiceException e) { throw e; } catch (AuthenticationException e) { lastException = e; } } // 5、登录成功 返回登录成功的token if (result != null) { eventPublisher.publishAuthenticationSuccess(result); return result; } }AbstractUserDetailsAuthenticationProvider1、authenticateAbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider接口,并且实现了部分方法,DaoAuthenticationProvider继承于AbstractUserDetailsAuthenticationProvider类,所以我们先来看看AbstractUserDetailsAuthenticationProvider的实现。public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { // 国际化处理 protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); /* * 对token一些检查,具体检查逻辑交给子类实现,抽象方法 / protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; /* * 认证逻辑的实现,调用抽象方法retrieveUser根据username获取UserDetails对象 */ public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 1、获取usernmae String username = (authentication.getPrincipal() == null) ? “NONE_PROVIDED” : authentication.getName(); // 2、尝试去缓存中获取UserDetails对象 UserDetails user = this.userCache.getUserFromCache(username); // 3、如果为空,则代表当前对象没有缓存。 if (user == null) { cacheWasUsed = false; try { //4、调用retrieveUser去获取UserDetail对象,为什么这个方法是抽象方法大家很容易知道,如果UserDetail信息存在关系数据库 则可以重写该方法并且去关系数据库获取用户信息,如果UserDetail信息存在其他地方,可以重写该方法用其他的方法去获取用户信息,这样丝毫不影响整个认证流程,方便扩展。 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { // 捕获异常 日志处理 并且往上抛出,登录失败。 if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } else { throw notFound; } } } try { // 5、前置检查 判断当前用户是否锁定,禁用等等 preAuthenticationChecks.check(user); // 6、其他的检查,在DaoAuthenticationProvider是检查密码是否一致 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { } // 7、后置检查,判断密码是否过期 postAuthenticationChecks.check(user); // 8、登录成功通过UserDetail对象重新构造一个认证通过的Token对象 return createSuccessAuthentication(principalToReturn, authentication, user); } protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // 调用第二个构造方法,构造一个认证通过的Token对象 UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; }}接下来我们具体看看retrieveUser的实现,没看源码大家应该也可以知道,retrieveUser方法应该是调用UserDetailsService去数据库查询是否有该用户,以及用户的密码是否一致。DaoAuthenticationProviderDaoAuthenticationProvider 主要是通过UserDetailService来获取UserDetail对象。1、retrieveUserprotected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { try { // 1、调用UserDetailsService接口的loadUserByUsername方法获取UserDeail对象 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); // 2、如果loadedUser为null 代表当前用户不存在,抛出异常 登录失败。 if (loadedUser == null) { throw new InternalAuthenticationServiceException( “UserDetailsService returned null, which is an interface contract violation”); } // 3、返回查询的结果 return loadedUser; } }2、additionalAuthenticationChecksprotected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // 1、如果密码为空,则抛出异常、 if (authentication.getCredentials() == null) { throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } // 2、获取用户输入的密码 String presentedPassword = authentication.getCredentials().toString(); // 3、调用passwordEncoder的matche方法 判断密码是否一致 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug(“Authentication failed: password does not match stored value”); // 4、如果不一致 则抛出异常。 throw new BadCredentialsException(messages.getMessage( “AbstractUserDetailsAuthenticationProvider.badCredentials”, “Bad credentials”)); } }总结至此,整认证流程已经分析完毕,大家如果有什么不懂可以关注我的公众号一起讨论。学习是一个漫长的过程,学习源码可能会很困难但是只要努力一定就会有获取,大家一致共勉。 ...

April 16, 2019 · 4 min · jiezi

SpringBoot 2.X Kotlin系列之AOP统一打印日志

在开发项目中,我们经常会需要打印日志,这样方便开发人员了解接口调用情况及定位错误问题,很多时候对于Controller或者是Service的入参和出参需要打印日志,但是我们又不想重复的在每个方法里去使用logger打印,这个时候希望有一个管理者统一来打印,这时Spring AOP就派上用场了,利用切面的思想,我们在进入、出入Controller或Service时给它切一刀实现统一日志打印。SpringAOP不仅可以实现在不产生新类的情况下打印日志,还可以管理事务、缓存等。具体可以了解官方文档。https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-api基础概念在使用SpringAOP,这里还是先简单讲解一些基本的知识吧,如果说的不对请及时指正,这里主要是根据官方文档来总结的。本章内容主要涉及的知识点。Pointcut: 切入点,这里用于定义规则,进行方法的切入(形象的比喻就是一把刀)。JoinPoint: 连接点,用于连接定义的切面。Before: 在之前,在切入点方法执行之前。AfterReturning: 在切入点方法结束并返回时执行。这里除了SpringAOP相关的知识,还涉及到了线程相关的知识点,因为我们需要考虑多线程中它们各自需要保存自己的变量,所以就用到了ThreadLocal。依赖引入这里主要是用到aop和mongodb,在pom.xml文件中加入以下依赖即可:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId></dependency>相关实体类/** * 请求日志实体,用于保存请求日志 /@Documentclass WebLog { var id: String = "" var request: String? = null var response: String? = null var time: Long? = null var requestUrl: String? = null var requestIp: String? = null var startTime: Long? = null var endTime: Long? = null var method: String? = null override fun toString(): String { return ObjectMapper().writeValueAsString(this) }}/* * 业务对象,上一章讲JPA中有定义 /@Documentclass Student { @Id var id :String? = null var name :String? = null var age :Int? = 0 var gender :String? = null var sclass :String ?= null override fun toString(): String { return ObjectMapper().writeValueAsString(this) }}定义切面定义切入点/* * 定义一个切入,只要是为io.intodream..web下public修饰的方法都要切入 /@Pointcut(value = “execution(public * io.intodream..web..(..))")fun webLog() {}定义切入点的表达式还可以使用within、如:/** * 表示在io.intodream.web包下的方法都会被切入 /@Pointcut(value = “within(io.intodream.web..")定义一个连接点/** * 切面的连接点,并声明在该连接点进入之前需要做的一些事情 /@Before(value = “webLog()”)@Throws(Throwable::class)fun doBefore(joinPoint: JoinPoint) { val webLog = WebLog() webLog.startTime = System.currentTimeMillis() val attributes = RequestContextHolder.getRequestAttributes() as ServletRequestAttributes? val request = attributes!!.request val args = joinPoint.args val paramNames = (joinPoint.signature as CodeSignature).parameterNames val params = HashMap<String, Any>(args.size) for (i in args.indices) { if (args[i] !is BindingResult) { params[paramNames[i]] = args[i] } } webLog.id = UUID.randomUUID().toString() webLog.request = params.toString() webLog.requestUrl = request.requestURI.toString() webLog.requestIp = request.remoteAddr webLog.method = request.method webRequestLog.set(webLog) logger.info(“REQUEST={} {}; SOURCE IP={}; ARGS={}”, request.method, request.requestURL.toString(), request.remoteAddr, params)}方法结束后执行@AfterReturning(returning = “ret”, pointcut = “webLog()”)@Throws(Throwable::class)fun doAfterReturning(ret: Any) { val webLog = webRequestLog.get() webLog.response = ret.toString() webLog.endTime = System.currentTimeMillis() webLog.time = webLog.endTime!! - webLog.startTime!! logger.info(“RESPONSE={}; SPEND TIME={}MS”, ObjectMapper().writeValueAsString(ret), webLog.time) logger.info(“webLog:{}”, webLog) webLogRepository.save(webLog) webRequestLog.remove()}这里的主要思路是,在方法执行前,先记录详情的请求参数,请求方法,请求ip, 请求方式及进入时间,然后将对象放入到ThreadLocal中,在方法结束后并取到对应的返回对象且计算出请求耗时,然后将请求日志保存到mongodb中。完成的代码package io.intodream.kotlin07.aspectimport com.fasterxml.jackson.databind.ObjectMapperimport io.intodream.kotlin07.dao.WebLogRepositoryimport io.intodream.kotlin07.entity.WebLogimport org.aspectj.lang.JoinPointimport org.aspectj.lang.annotation.AfterReturningimport org.aspectj.lang.annotation.Aspectimport org.aspectj.lang.annotation.Beforeimport org.aspectj.lang.annotation.Pointcutimport org.aspectj.lang.reflect.CodeSignatureimport org.slf4j.Loggerimport org.slf4j.LoggerFactoryimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.core.annotation.Orderimport org.springframework.stereotype.Componentimport org.springframework.validation.BindingResultimport org.springframework.web.context.request.RequestContextHolderimport org.springframework.web.context.request.ServletRequestAttributesimport java.util./** * {描述} * * @author yangxianxi@gogpay.cn * @date 2019/4/10 19:06 * /@Aspect@Order(5)@Componentclass WebLogAspect { private val logger:Logger = LoggerFactory.getLogger(WebLogAspect::class.java) private val webRequestLog: ThreadLocal<WebLog> = ThreadLocal() @Autowired lateinit var webLogRepository: WebLogRepository /* * 定义一个切入,只要是为io.intodream..web下public修饰的方法都要切入 / @Pointcut(value = “execution(public * io.intodream..web..(..))”) fun webLog() {} /** * 切面的连接点,并声明在该连接点进入之前需要做的一些事情 / @Before(value = “webLog()”) @Throws(Throwable::class) fun doBefore(joinPoint: JoinPoint) { val webLog = WebLog() webLog.startTime = System.currentTimeMillis() val attributes = RequestContextHolder.getRequestAttributes() as ServletRequestAttributes? val request = attributes!!.request val args = joinPoint.args val paramNames = (joinPoint.signature as CodeSignature).parameterNames val params = HashMap<String, Any>(args.size) for (i in args.indices) { if (args[i] !is BindingResult) { params[paramNames[i]] = args[i] } } webLog.id = UUID.randomUUID().toString() webLog.request = params.toString() webLog.requestUrl = request.requestURI.toString() webLog.requestIp = request.remoteAddr webLog.method = request.method webRequestLog.set(webLog) logger.info(“REQUEST={} {}; SOURCE IP={}; ARGS={}”, request.method, request.requestURL.toString(), request.remoteAddr, params) } @AfterReturning(returning = “ret”, pointcut = “webLog()”) @Throws(Throwable::class) fun doAfterReturning(ret: Any) { val webLog = webRequestLog.get() webLog.response = ret.toString() webLog.endTime = System.currentTimeMillis() webLog.time = webLog.endTime!! - webLog.startTime!! logger.info(“RESPONSE={}; SPEND TIME={}MS”, ObjectMapper().writeValueAsString(ret), webLog.time) logger.info(“webLog:{}”, webLog) webLogRepository.save(webLog) webRequestLog.remove() }}这里定义的是Web层的切面,对于Service层我也可以定义一个切面,但是对于Service层的进入和返回的日志我们可以把级别稍等调低一点,这里改debug,具体实现如下:package io.intodream.kotlin07.aspectimport com.fasterxml.jackson.databind.ObjectMapperimport org.aspectj.lang.JoinPointimport org.aspectj.lang.annotation.AfterReturningimport org.aspectj.lang.annotation.Aspectimport org.aspectj.lang.annotation.Beforeimport org.aspectj.lang.annotation.Pointcutimport org.aspectj.lang.reflect.CodeSignatureimport org.slf4j.Loggerimport org.slf4j.LoggerFactoryimport org.springframework.core.annotation.Orderimport org.springframework.stereotype.Componentimport org.springframework.validation.BindingResult/* * service层所有public修饰的方法调用返回日志 * * @author yangxianxi@gogpay.cn * @date 2019/4/10 17:33 * /@Aspect@Order(2)@Componentclass ServiceLogAspect { private val logger: Logger = LoggerFactory.getLogger(ServiceLogAspect::class.java) /* * / @Pointcut(value = “execution(public * io.intodream..service..*(..))”) private fun serviceLog(){} @Before(value = “serviceLog()”) fun deBefore(joinPoint: JoinPoint) { val args = joinPoint.args val codeSignature = joinPoint.signature as CodeSignature val paramNames = codeSignature.parameterNames val params = HashMap<String, Any>(args.size).toMutableMap() for (i in args.indices) { if (args[i] !is BindingResult) { params[paramNames[i]] = args[i] } } logger.debug(“CALL={}; ARGS={}”, joinPoint.signature.name, params) } @AfterReturning(returning = “ret”, pointcut = “serviceLog()”) @Throws(Throwable::class) fun doAfterReturning(ret: Any) { logger.debug(“RESPONSE={}”, ObjectMapper().writeValueAsString(ret)) }}接口测试这里就不在贴出Service层和web的代码实现了,因为我是拷贝之前将JPA那一章的代码,唯一不同的就是加入了切面,切面的加入并不影响原来的业务流程。执行如下请求:我们会在控制台看到如下日志2019-04-14 19:32:27.208 INFO 4914 — [nio-9000-exec-1] i.i.kotlin07.aspect.WebLogAspect : REQUEST=POST http://localhost:9000/api/student/; SOURCE IP=0:0:0:0:0:0:0:1; ARGS={student={“id”:“5”,“name”:“Rose”,“age”:17,“gender”:“Girl”,“sclass”:“Second class”}}2019-04-14 19:32:27.415 INFO 4914 — [nio-9000-exec-1] org.mongodb.driver.connection : Opened connection [connectionId{localValue:2, serverValue:4}] to localhost:270172019-04-14 19:32:27.431 INFO 4914 — [nio-9000-exec-1] i.i.kotlin07.aspect.WebLogAspect : RESPONSE={“id”:“5”,“name”:“Rose”,“age”:17,“gender”:“Girl”,“sclass”:“Second class”}; SPEND TIME=239MS2019-04-14 19:32:27.431 INFO 4914 — [nio-9000-exec-1] i.i.kotlin07.aspect.WebLogAspect : webLog:{“id”:“e7b0ca1b-0a71-4fa0-9f5f-95a29d4d54a1”,“request”:"{student={"id":"5","name":"Rose","age":17,"gender":"Girl","sclass":"Second class"}}”,“response”:”{"id":"5","name":"Rose","age":17,"gender":"Girl","sclass":"Second class"}",“time”:239,“requestUrl”:"/api/student/",“requestIp”:“0:0:0:0:0:0:0:1”,“startTime”:1555241547191,“endTime”:1555241547430,“method”:“POST”}查看数据库会看到我们的请求日志已经写入了:这里有一个地方需要注意,在Service层的实现,具体如下:return studentRepository.findById(id).get()这里的findById会返回一个Optional<T>对象,如果没有查到数据,我们使用get获取数据会出现异常java.util.NoSuchElementException: No value present,可以改为返回对象可以为空只要在返回类型后面加一个?即可,同时调用Optional的ifPresent进行安全操作。 ...

April 16, 2019 · 3 min · jiezi

SpringBoot 2.X Kotlin系列之JavaMailSender发送邮件

在很多服务中我经常需要用到发送邮件功能,所幸的是SpringBoot可以快速使用的框架spring-boot-starter-mail,只要引入改框架我们可以快速的完成发送邮件功能。引入mailJar<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId></dependency>获取邮件发送服务器配置在国内用的最多的就是QQ邮件和网易163邮件,这里会简单讲解获取两家服务商的发送邮件配置。QQ邮箱等录QQ邮箱,点击设置然后选择账户在下方可以看到POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务,然后我们需要把smtp服务开启,开启成功后会得到一个秘钥。如图所示:开启成功需要在application.properties配置文件中加入相应的配置,以下信息部分需要替换为自己的信息,教程结束下面的账号就会被停用spring.mail.host=smtp.qq.comspring.mail.username=6928700@qq.com # 替换为自己的QQ邮箱号spring.mail.password=owqpkjmqiasnbigc # 替换为自己的秘钥或授权码spring.mail.port=465spring.mail.properties.mail.smtp.auth=truespring.mail.properties.mail.smtp.starttls.enable=truespring.mail.properties.mail.smtp.starttls.required=true# sender email.sender=6928700@qq.com # 替换为自己的QQ邮箱号163邮箱登录账户然后在设置找到POP3/SMTP/IMAP选项,然后开启smtp服务,具体操作如下图所示,然后修改对应的配置文件spring.mail.host=smtp.163.comspring.mail.username=xmsjgzs@163.com # 替换为自己的163邮箱号spring.mail.password=owqpkj163MC # 替换为自己的授权码spring.mail.port=465spring.mail.properties.mail.smtp.auth=truespring.mail.properties.mail.smtp.starttls.enable=truespring.mail.properties.mail.smtp.starttls.required=true# sender email.sender=xmsjgzs@163.com # 替换为自己的163邮箱号实现简单发送邮件这里发送邮件我们主要用到的是JavaMailSender对象,发送简单邮件主要是发送字符串内容,复杂的邮件我们可能会添加附件或者是发送HTML格式的邮件,我们先测试简单的发送,代码如下:override fun sendSimple(receiver: String, title: String, content: String) { logger.info(“发送简单邮件服务”) val message = mailSender.createMimeMessage() val helper = MimeMessageHelper(message, true) helper.setFrom(sender) helper.setTo(receiver) helper.setSubject(title) helper.setText(content) mailSender.send(message)}测试代码@RunWith(SpringJUnit4ClassRunner::class)@SpringBootTestclass MailServiceImplTest { @Autowired lateinit var mailService: MailService @Test fun sendSimple() { mailService.sendSimple(“xmsjgzs@163.com”, “Hello Kotlin Mail”, “SpringBoot Kotlin 专栏学习之JavaMailSender发送邮件”) }}检查邮件是否收到发送的内容发送模板邮件我们这里用的HTML模板引擎是thymeleaf,大家需要引入一下spring-boot-starter-thymeleaf<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>有个地方需要注意,SpringBoot项目默认静态资源都是放在resources/templates目录下,所以我们编写的HTML模板就需要放在该目录下,具体内容如下:<!DOCTYPE html><html lang=“en” xmlns=“http://www.w3.org/1999/xhtml" xmlns:th=“http://www.thymeleaf.org”><head> <meta charset=“UTF-8”> <title th:text="${title}">Title</title></head><body> <h1 th:text="${name}">Demo</h1> <h1 th:text="${phone}">xxx</h1></body></html>发送模板邮件主要实现代码override fun sendMail(receiver: String, title: String, o: Any, templateName: String) { logger.info(“开始发送邮件服务,To:{}”, receiver) val message = mailSender.createMimeMessage() val helper = MimeMessageHelper(message, true) helper.setFrom(sender) helper.setTo(receiver) helper.setSubject(title) val context = Context() context.setVariable(“title”, title) /* * 设置动态数据,这里不建议强转,具体业务需求传入具体的对象 / context.setVariables(o as MutableMap<String, Any>?) / * 读取取模板html代码并赋值 / val content = templateEngine.process(templateName, context) helper.setText(content, true) mailSender.send(message) logger.info(“邮件发送结束”)}测试代码@Testfun sendMail() { val model = HashMap<String, Any>() model[“name”] = “Tom” model[“phone”] = “69288888” mailService.sendMail(“xmsjgzs@163.com”, “Kotlin Template Mail”, model, “mail”)}查看邮件我们可以看到如下内容:邮件添加附件附件的添加也是非常容易的,我需要先把发送的附件放在resources/templates目录下,然后在MimeMessageHelper对象中设置相应的属性即可,如下所示:helper.addAttachment(“test.txt”, FileSystemResource(File(“test.txt”)))完整的代码package io.intodream.kotlin06.service.implimport io.intodream.kotlin06.service.MailServiceimport org.slf4j.Loggerimport org.slf4j.LoggerFactoryimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.beans.factory.annotation.Valueimport org.springframework.core.io.FileSystemResourceimport org.springframework.mail.javamail.JavaMailSenderimport org.springframework.mail.javamail.MimeMessageHelperimport org.springframework.stereotype.Serviceimport org.thymeleaf.TemplateEngineimport org.thymeleaf.context.Contextimport java.io.File/* * {描述} * * @author yangxianxi@gogpay.cn * @date 2019/4/8 19:19 * /@Serviceclass MailServiceImpl @Autowired constructor(private var mailSender: JavaMailSender, private var templateEngine: TemplateEngine) : MailService{ val logger : Logger = LoggerFactory.getLogger(MailServiceImpl::class.java) @Value(”${email.sender}") val sender: String = “6928700@qq.com” override fun sendSimple(receiver: String, title: String, content: String) { logger.info(“发送简单邮件服务”) val message = mailSender.createMimeMessage() val helper = MimeMessageHelper(message, true) helper.setFrom(sender) helper.setTo(receiver) helper.setSubject(title) helper.setText(content) mailSender.send(message) } override fun sendMail(receiver: String, title: String, o: Any, templateName: String) { logger.info(“开始发送邮件服务,To:{}”, receiver) val message = mailSender.createMimeMessage() val helper = MimeMessageHelper(message, true) helper.setFrom(sender) helper.setTo(receiver) helper.setSubject(title) val context = Context() context.setVariable(“title”, title) / * 设置动态数据,这里不建议强转,具体业务需求传入具体的对象 / context.setVariables(o as MutableMap<String, Any>?) / * 添加附件 / helper.addAttachment(“test.txt”, FileSystemResource(File(“test.txt”))) / * 读取取模板html代码并赋值 / val content = templateEngine.process(templateName, context) helper.setText(content, true) mailSender.send(message) logger.info(“邮件发送结束”) }}测试代码package io.intodream.kotlin06.service.implimport io.intodream.kotlin06.service.MailServiceimport org.junit.Testimport org.junit.runner.RunWithimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.boot.test.context.SpringBootTestimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner/* * {描述} * * @author yangxianxi@gogpay.cn * @date 2019/4/9 18:38 */@RunWith(SpringJUnit4ClassRunner::class)@SpringBootTestclass MailServiceImplTest { @Autowired lateinit var mailService: MailService @Test fun sendSimple() { mailService.sendSimple(“xmsjgzs@163.com”, “Hello Kotlin Mail”, “SpringBoot Kotlin 专栏学习之JavaMailSender发送邮件”) } @Test fun sendMail() { val model = HashMap<String, Any>() model[“name”] = “Tom” model[“phone”] = “69288888” mailService.sendMail(“xmsjgzs@163.com”, “Kotlin Template Mail”, model, “mail”) }}关于Kotlin使用JavaMailSender发送邮件的介绍就到此结束了,如果大家觉得教程有用麻烦点一下赞,如果有错误的地方欢迎指出。 ...

April 16, 2019 · 2 min · jiezi

谷歌助力,快速实现 Java 应用容器化

原文地址:梁桂钊的博客博客地址:http://blog.720ui.com欢迎关注公众号:「服务端思维」。一群同频者,一起成长,一起精进,打破认知的局限性。Google 在 2018 年下旬开源了一款新的 Java 工具 Jib,可以轻松地将 Java 应用程序容器化。通过 Jib,我们不需要编写 Dockerfile 或安装 Docker,通过集成到 Maven 或 Gradle 插件,就可以立即将 Java 应用程序容器化。开源地址:https://github.com/GoogleContainerTools/jib一、什么是 JibJib 是一个快速而简单的容器镜像构建工具,它作为 Maven 或 Gradle 的一部分运行,不需要编写 Dockerfile 或运行 Docker 守护进程。它从 Maven 或 Gradle 中构建我们的 Docker 镜像, 并只将发生变更的层(而不是整个应用程序)推送到注册表来节省宝贵的构建时间。现在,我们对 Docker 构建流程和 Jib 构建流程进行对比。Docker 构建流程,如下所示。Jib 构建流程,则是这样的。二、实战出真知1. 构建一个简单的 Java 工程我们编写一个简单的 Java 类。public class HelloWorld { public static void main(String[] args) { System.out.println(“Hello World!”); System.out.println(“http://blog.720ui.com”); }}紧接着,我们再创建一个 pom.xml 文件。<project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.lianggzone.sample.lib</groupId> <artifactId>helloworld-samples</artifactId> <version>0.1</version> <packaging>jar</packaging> <name>helloworld-samples</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <jib-maven-plugin.version>1.0.2</jib-maven-plugin.version> <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version> </properties> <dependencies> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-compiler-plugin.version}</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <!– Jib –> <plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>${jib-maven-plugin.version}</version> <configuration> <from> <image>registry.cn-hangzhou.aliyuncs.com/lianggzone/oracle_java8</image> </from> <to> <image>registry.cn-hangzhou.aliyuncs.com/lianggzone/jib-helloworld:v1</image> </to> <container> <jvmFlags> <jvmFlag>-Xms512m</jvmFlag> <jvmFlag>-Xdebug</jvmFlag> </jvmFlags> <mainClass>com.lianggzone.HelloWorld</mainClass> </container> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> </plugin> </plugins> </build></project>由于默认访问谷歌的 gcr.io 仓库,而国内访问 gcr.io 不稳定会经常导致网络超时,所以笔者使用了国内的阿里云镜像服务,那么就不需要访问谷歌的仓库了。现在,我们执行 mvn compile jib:build 命令进行自动化构建,它会从 <from> 拉取镜像,并把生成的镜像上传到 <to> 设置的地址。这里,笔者还通过 <jvmFlags>` 设置了一些 JVM 参数。mvn compile jib:build此外,如果"登录失败,未授权”,需要通过 docker login 登录鉴权一下。此外,更好的做法是,你可以考虑在Maven 中放置凭据。<settings> … <servers> … <server> <id>registry.cn-hangzhou.aliyuncs.com</id> <username>你的阿里云账号</username> <password>你的阿里云密码</password> </server> </servers></settings>最后,执行完成后,我们可以在阿里云镜像仓库获取镜像。大功告成,现在,我们来验证一把。我们通过 docker pull 拉取镜像,并运行。docker pull registry.cn-hangzhou.aliyuncs.com/lianggzone/jib-helloworld:v1docker run –name jib-helloworld -it registry.cn-hangzhou.aliyuncs.com/lianggzone/jib-helloworld:v1 /bin/bash执行结果,如下所示。2. 构建一个 SpringBoot 的可运行 Jar我们来一个复杂一些的项目,构建一个 SpringBoot 的项目。关于 SpringBoot 的使用,可以阅读笔者之前的文章:http://blog.720ui.com/columns/springboot_all/。现在,我们首先需要搭建一个工程,并创建一个启动类。@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}同时,需要一个 Web 的接口。@RestControllerpublic class WebController { @RequestMapping("/blog”) public String index() { return “http://blog.720ui.com”; }}紧接着,我们再创建一个 pom.xml 文件。<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.2.RELEASE</version> </parent> <groupId>com.lianggzone.sample.lib</groupId> <artifactId>springboot-samples</artifactId> <version>0.1</version> <packaging>jar</packaging> <name>springboot-samples</name> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <jib-maven-plugin.version>1.0.2</jib-maven-plugin.version> <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-compiler-plugin.version}</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <!– Jib –> <plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>${jib-maven-plugin.version}</version> <configuration> <from> <image>registry.cn-hangzhou.aliyuncs.com/lianggzone/oracle_java8</image> </from> <to> <image>registry.cn-hangzhou.aliyuncs.com/lianggzone/jib-springboot:v1</image> </to> <container> <jvmFlags> <jvmFlag>-Xms512m</jvmFlag> <jvmFlag>-Xdebug</jvmFlag> </jvmFlags> </container> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> </plugin> </plugins> </build></project>现在,我们执行 mvn compile jib:build 命令进行自动化构建。执行完成后,我们可以在阿里云镜像仓库获取镜像。现在,我们再来验证一把。我们通过 docker pull 拉取镜像,并运行。docker pull registry.cn-hangzhou.aliyuncs.com/lianggzone/jib-springboot:v1docker run -p 8080:8080 –name jib-springboot -it registry.cn-hangzhou.aliyuncs.com/lianggzone/jib-springboot:v1 /bin/bash执行结果,如下所示。现在,我们访问 http://localhost:8080/blog ,我们可以正常调用 API 接口了。3. 构建一个 WAR 工程Jib 还支持 WAR 项目。如果 Maven 项目使用 war-packaging 类型,Jib 将默认使用 distroless Jetty 作为基础镜像来部署项目。要使用不同的基础镜像,我们可以自定义 <container><appRoot> , <container> <entrypoint> 和 <container> <args> 。以下是使用 Tomcat 镜像的案例。<configuration> <from> <image>tomcat:8.5-jre8-alpine</image> </from> <container> <appRoot>/usr/local/tomcat/webapps/ROOT</appRoot> </container></configuration>三、源码地址源码地址:https://github.com/lianggzone/jib-samples附:参考资料https://github.com/GoogleContainerTools/jibhttps://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin(完,转载请注明作者及出处。)写在末尾【服务端思维】:我们一起聊聊服务端核心技术,探讨一线互联网的项目架构与实战经验。同时,拥有众多技术大牛的「后端圈」大家庭,期待你的加入,一群同频者,一起成长,一起精进,打破认知的局限性。更多精彩文章,尽在「服务端思维」! ...

April 16, 2019 · 2 min · jiezi

Spring Boot 2.x基础教程:工程结构推荐

Spring Boot框架本身并没有对工程结构有特别的要求,但是按照最佳实践的工程结构可以帮助我们减少可能会遇见的坑,尤其是Spring包扫描机制的存在,如果您使用最佳实践的工程结构,可以免去不少特殊的配置工作。典型示例以下结构是比较推荐的package组织方式:com +- example +- myproject +- Application.java | +- domain | +- Customer.java | +- CustomerRepository.java | +- service | +- CustomerService.java | +- web | +- CustomerController.java |root package:com.example.myproject,所有的类和其他package都在root package之下。应用主类:Application.java,该类直接位于root package下。通常我们会在应用主类中做一些框架配置扫描等配置,我们放在root package下可以帮助程序减少手工配置来加载到我们希望被Spring加载的内容com.example.myproject.domain包:用于定义实体映射关系与数据访问相关的接口和实现com.example.myproject.service包:用于编写业务逻辑相关的接口与实现com.example.myproject.web:用于编写Web层相关的实现,比如:Spring MVC的Controller等上面的结构中,root package与应用主类的位置是整个结构的关键。由于应用主类在root package中,所以按照上面的规则定义的所有其他类都处于root package下的其他子包之后。默认情况下,Spring Boot的应用主类会自动扫描root package以及所有子包下的所有类来进行初始化。什么意思呢?举个例子,假设我们将com.example.myproject.web包与上面所述的root package:com.example.myproject放在同一级,像下面这样:com +- example +- myproject +- Application.java | +- domain | +- Customer.java | +- CustomerRepository.java | +- service | +- CustomerService.java | +- web | +- CustomerController.java |这个时候,应用主类Application.java在默认情况下就无法扫描到com.example.myproject.web中的Controller定义,就无法初始化Controller中定义的接口。非典型结构下的初始化那么如果,我们一定要加载非root package下的内容怎么办呢?方法一:使用@ComponentScan注解指定具体的加载包,比如:@SpringBootApplication@ComponentScan(basePackages=“com.example”)public class Bootstrap { public static void main(String[] args) { SpringApplication.run(Bootstrap.class, args); }}这种方法通过注解直接指定要扫描的包,比较直观。如果有这样的需求也是可以用的,但是原则上还是推荐以上面的典型结构来定义,这样也可以少写一些注解,代码更加简洁。方法二:使用@Bean注解来初始化,比如:@SpringBootApplicationpublic class Bootstrap { public static void main(String[] args) { SpringApplication.run(Bootstrap.class, args); } @Bean public CustomerController customerController() { return new CustomerController(); }}这种方法在业务开发的时候并不是特别推荐,更适合用于框架封装等场景,关于更多封装上的技巧,后面我们在进阶教程中详细讲解。如果读者觉得自己团队使用的工程结构不错,欢迎留言分享~代码示例本教程配套仓库:Github:https://github.com/dyc87112/SpringBoot-Learning/tree/2.xGitee:https://gitee.com/didispace/SpringBoot-Learning/tree/2.x如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!专栏推荐Spring Boot从入门到精通Spring Cloud从入门到精通关联内容Spring Boot工程结构推荐 - 1.x版本 ...

April 15, 2019 · 1 min · jiezi

分布式并发场景下SpringSession(Redis) 的数据脏读问题

问题现象问题来源于一个临时订单重复提交管控场景,通过在Session中写入本次提交的临时订单ID防止同个表单的重复提交。但在用户使用某些浏览器(如QQ浏览器、微信内置浏览器)时,仍有偶发性的重复提交现象。相关核心代码如下:原因分析该问题主要原因是因为当有A、B两个一样的请求时,如果在A还没响应完毕的时候SpringMvc又接收了B请求,B请求在获取Session中的值时,会获取到A请求改写之前的数据。其根本原因在于SpringSession在写入或删除Session属性时,会根据配置中的FlushMode决定在什么时候序列化到Redis,而默认的FlushMode为ON_SAVE,API原文是这样的:也就是说,在默认情况下只有在Response被提交时Session内容才会序列化到Redis。所以导致了并发场景下的Session数据脏读问题解决方案目前我们采取将RedisFlushMode改为IMMEDIATE,修改方法为在@EnableRedisHttpSession注解中指定flushMode:如此修改后,在每次调用removeAttribure后,都能正确的观察到Redis中相应的属性被置为空,问题也就基本得到了解决。

April 15, 2019 · 1 min · jiezi

Spring Boot 定义系统启动任务,你会几种方式?

在 Servlet/Jsp 项目中,如果涉及到系统任务,例如在项目启动阶段要做一些数据初始化操作,这些操作有一个共同的特点,只在项目启动时进行,以后都不再执行,这里,容易想到web基础中的三大组件( Servlet、Filter、Listener )之一 Listener ,这种情况下,一般定义一个 ServletContextListener,然后就可以监听到项目启动和销毁,进而做出相应的数据初始化和销毁操作,例如下面这样:public class MyListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { //在这里做数据初始化操作 } @Override public void contextDestroyed(ServletContextEvent sce) { //在这里做数据备份操作 }}当然,这是基础 web 项目的解决方案,如果使用了 Spring Boot,那么我们可以使用更为简便的方式。Spring Boot 中针对系统启动任务提供了两种解决方案,分别是 CommandLineRunner 和 ApplicationRunner,分别来看。CommandLineRunner使用 CommandLineRunner 时,首先自定义 MyCommandLineRunner1 并且实现 CommandLineRunner 接口:@Component@Order(100)public class MyCommandLineRunner1 implements CommandLineRunner { @Override public void run(String… args) throws Exception { }}关于这段代码,我做如下解释:首先通过 @Compoent 注解将 MyCommandLineRunner1 注册为Spring容器中的一个 Bean。添加 @Order注解,表示这个启动任务的执行优先级,因为在一个项目中,启动任务可能有多个,所以需要有一个排序。@Order 注解中,数字越小,优先级越大,默认情况下,优先级的值为 Integer.MAX_VALUE,表示优先级最低。在 run 方法中,写启动任务的核心逻辑,当项目启动时,run方法会被自动执行。run 方法的参数,来自于项目的启动参数,即项目入口类中,main方法的参数会被传到这里。此时启动项目,run方法就会被执行,至于参数,可以通过两种方式来传递,如果是在 IDEA 中,可以通过如下方式来配置参数: 另一种方式,则是将项目打包,在命令行中启动项目,然后启动时在命令行传入参数,如下:java -jar devtools-0.0.1-SNAPSHOT.jar 三国演义 西游记注意,这里参数传递时没有key,直接写value即可,执行结果如下: ApplicationRunnerApplicationRunner 和 CommandLineRunner 功能一致,用法也基本一致,唯一的区别主要体现在对参数的处理上,ApplicationRunner 可以接收更多类型的参数(ApplicationRunner 除了可以接收 CommandLineRunner 的参数之外,还可以接收 key/value形式的参数)。 使用 ApplicationRunner ,自定义类实现 ApplicationRunner 接口即可,组件注册以及组件优先级的配置都和 CommandLineRunner 一致,如下:@Component@Order(98)public class MyApplicationRunner1 implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { List<String> nonOptionArgs = args.getNonOptionArgs(); System.out.println(“MyApplicationRunner1>>>"+nonOptionArgs); Set<String> optionNames = args.getOptionNames(); for (String key : optionNames) { System.out.println(“MyApplicationRunner1>>>"+key + “:” + args.getOptionValues(key)); } String[] sourceArgs = args.getSourceArgs(); System.out.println(“MyApplicationRunner1>>>"+Arrays.toString(sourceArgs)); }}当项目启动时,这里的 run 方法就会被自动执行,关于 run 方法的参数 ApplicationArguments ,我说如下几点:args.getNonOptionArgs();可以用来获取命令行中的无key参数(和CommandLineRunner一样)。args.getOptionNames();可以用来获取所有key/value形式的参数的key。args.getOptionValues(key));可以根据key获取key/value 形式的参数的value。args.getSourceArgs(); 则表示获取命令行中的所有参数。ApplicationRunner 定义完成后,传启动参数也是两种方式,参数类型也有两种,第一种和 CommandLineRunner 一致,第二种则是 –key=value 的形式,在 IDEA 中定义方式如下: 或者使用 如下启动命令:java -jar devtools-0.0.1-SNAPSHOT.jar 三国演义 西游记 –age=99运行结果如下:总结整体来说 ,这两种的用法的差异不大 ,主要体现在对参数的处理上,小伙伴可以根据项目中的实际情况选择合适的解决方案。 关注公众号牧码小子,专注于 Spring Boot+微服务,定期视频教程分享,关注后回复 Java ,领取松哥为你精心准备的 Java 干货! ...

April 15, 2019 · 1 min · jiezi

SpringBoot + Swagger + ReDoc 笔记

前言在开始使用 Swagger 之前,我们先来了解下几个概念。名词释义SwaggerSwagger 是一个 RESTful 接口规范,现在流行的版本有 2.0 和 3.0 。OpenAPIOpenAPI 规范就是之前的 Swagger 规范,只是换了个名字。swagger.json/swagger.yamlswagger.json 或 swagger.yaml 是符合 Swagger 规范的接口描述文件。文件名称随意,这里只是举例。SpringfoxSpringfox 套件可以为 Spring 系列项目自动生成 swagger.json,还可以集成 Swagger UI。Swagger UISwagger UI 通过解析 swagger.[json/yaml],来生成在线接口文档。ReDocReDoc 是另一个 Swagger UI 工具。SpringfoxSpringfox 当前有两个主要版本:正式版 2.9.2 和 快照版 3.0.0-SNAPSHOT。建议读者先试用正式版。Maven 依赖(2.9.2)<properties> <springfox.version>2.9.2</springfox.version></properties><dependencies> <!– 生成 Swagger 文档 –> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${springfox.version}</version> </dependency> <!– 添加 Springfox Swagger UI 支持 –> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${springfox.version}</version> </dependency></dependencies>Maven 依赖(3.0.0-SNAPSHOT)编写 Swagger 配置类在 SpringBoot 启动类同级目录下添加该配置类。配置类 SwaggerConfig 上添加 @Configuration 注解,是为了让 Spring 识别到这是一个配置类。package org.qadoc.wiremock.web;import com.google.common.base.Predicates;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import springfox.documentation.builders.PathSelectors;import springfox.documentation.builders.RequestHandlerSelectors;import springfox.documentation.service.ApiInfo;import springfox.documentation.service.Contact;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.Docket;import java.util.ArrayList;/** * Swagger 配置类 <br> * 创建时间:2019/4/10 15:35<br> * @author 李云 /@Configurationpublic class SwaggerConfig { @Bean public Docket mocklabAPI(){ return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.any()) //.apis(not(withClassAnnotation(CustomIgnore.class))) .paths(Predicates.not(PathSelectors.regex("/error."))) //不展示 Spring 自带的 error Controller .build() //.pathMapping("/") //.directModelSubstitute(LocalDate.class,String.class) //.genericModelSubstitutes(ResponseEntity.class) .useDefaultResponseMessages(false) //.tags(new Tag(“tagName”,“description”)) .apiInfo(apiInfo()) ; } private ApiInfo apiInfo(){ Contact contact = new Contact("","",""); return new ApiInfo( “MockLab API 文档”, “MockLab API 文档(Web端)”, “1.0.0”, “”, contact, “”, “”, new ArrayList<>() ); }}启用 Swagger在 SpringBoot 的启动类上添加 @EnableSwagger2 注解。package org.qadoc.wiremock.web;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import springfox.documentation.swagger2.annotations.EnableSwagger2;@SpringBootApplication@EnableSwagger2public class MockLabWebApplication { public static void main(String[] args) { SpringApplication.run(MockLabWebApplication.class, args); }}Swagger 文档注解Swagger 的文档注解有三类:resource(Controller 类) 注解operation(API 方法)注解API models(实体类)注解注解概览注解描述@Api标记一个类为 Swagger 资源。@ApiImplicitParam表示 API Operation 中的单个参数。@ApiImplicitParams包装注解,包含多个 @ApiImplicitParam 注解@ApiModel提供 Swagger models 的附加信息@ApiModelProperty添加和操作 model 属性的数据。@ApiOperation描述一个特定路径的 operation(通常是 HTTP 方法)@ApiParam为 operation 参数添加额外的 meta-data。@ApiResponse描述 operation 可能的响应。@ApiResponses包装注解,包含多个 @ApiResponse 注解。@ResponseHeader表示响应头。Resource API 声明@Api声明该 API 接口类需要生成文档。@Api(tags = {“应用健康检查”})@RestController@RequestMapping(value = “/healthcheck”)public class HealthCheckController { …}属性列表属性描述tags属性用来对接口进行分组管理。当然你可以添加多个 tag,那么该类下的接口会在这两个分组里出现。注意事项:如果没有指定响应的 Content-Type ,springfox 的默认值是 /。有两种指定方式。在 Controller 类或方法上的 @RequestMapping 注解中增加 produces 属性值。@RequestMapping(value = “/healthcheck”,produces = MediaType.APPLICATION_JSON_VALUE)在 Swagger 配置类中指定默认值。docket… .produces(Sets.newHashSet(MediaType.APPLICATION_JSON_VALUE)) .consumes(Sets.newHashSet(MediaType.APPLICATION_JSON_VALUE))…@ApiIgnore声明该 API 接口类不需要生成文档。@ApiIgnore(“过时的API”)@ConditionalOnExpression(“false”)@RestController@RequestMapping(value = “/record/xianbank”)public class RecordXianBankController { …}Operation 声明@ApiOperation用于接口方法上,描述该接口相关信息。@ApiOperation( nickname = “healthCheckUsingGet”, value = “应用健康检查”, notes = “用于检查应用是否可以正常访问。”, produces = MediaType.APPLICATION_JSON_VALUE, response = HealthCheckRes.class)@GetMapping()public BaseResult<HealthCheckRes.AppStatus> healthCheck() { …}属性列表属性描述nicknameoperationID,接口唯一标识value接口名称notes接口描述信息produces响应 Content-Type,示例:“application/json, application/xml"consumes请求 Content-Type,示例:“application/json, application/xml"responseresponse body Model,响应体结构及单个字段示例参考资料:@ApiModelProperty throwing NumberFormatException if example value is not set ...

April 14, 2019 · 2 min · jiezi

如何在生产环境中重启Spring Boot应用?

通过HTTP重启Spring Boot应用程序需求背景在一个很奇葩的需求下,要求在客户端动态修改Spring Boot配置文件中的属性,例如端口号、应用名称、数据库连接信息等,然后通过一个Http请求重启Spring Boot程序。这个需求类似于操作系统更新配置后需要进行重启系统才能生效的应用场景。动态配置系统并更新生效是应用的一种通用性需求,实现的方式也有很多种。例如监听配置文件变化、使用配置中心等等。网络上也有很多类似的教程存在,但大多数都是在开发阶段,借助Spring Boot DevTools插件实现应用程序的重启,或者是使用spring-boot-starter-actuator和spring-cloud-starter-config来提供端点(Endpoint)的刷新。第一种方式无法在生产环境中使用(不考虑),第二种方式需要引入Spring Cloud相关内容,这无疑是杀鸡用了宰牛刀。接下来,我将尝试采用另外一种方式实现HTTP请求重启Spring Boot应用程序这个怪异的需求。尝试思路重启Spring Boot应用程序的关键步骤是对主类中SpringApplication.run(Application.class,args);方法返回值的处理。SpringApplication#run()方法将会返回一个ConfigurableApplicationContext类型对象,通过查看官方文档可以看到,ConfigurableApplicationContext接口类中定义了一个close()方法,可以用来关闭当前应用的上下文:package org.springframework.context;import java.io.Closeable;import org.springframework.beans.BeansException;import org.springframework.beans.factory.config.BeanFactoryPostProcessor;import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;import org.springframework.core.env.ConfigurableEnvironment;import org.springframework.core.io.ProtocolResolver;import org.springframework.lang.Nullable;public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {void close(); } 继续看官方源码,AbstractApplicationContext类中实现close()方法,下面是实现类中的方法摘要:public void close() { Object var1 = this.startupShutdownMonitor; synchronized(this.startupShutdownMonitor) { this.doClose(); if (this.shutdownHook != null) { try { Runtime.getRuntime().removeShutdownHook(this.shutdownHook); } catch (IllegalStateException var4) { ; } } } }#close()方法将会调用#doClose()方法,我们再来看看#doClose()方法做了哪些操作,下面是doClose()方法的摘要:protected void doClose() { if (this.active.get() && this.closed.compareAndSet(false, true)) { … LiveBeansView.unregisterApplicationContext(this); … this.destroyBeans(); this.closeBeanFactory(); this.onClose(); if (this.earlyApplicationListeners != null) { this.applicationListeners.clear(); this.applicationListeners.addAll(this.earlyApplicationListeners); } this.active.set(false); } }在#doClose()方法中,首先将应用上下文从注册表中清除掉,然后是销毁Bean工厂中的Beans,紧接着关闭Bean工厂。官方文档看到这里,就产生了解决一个结局重启应用应用程序的大胆猜想。在应用程序的main()方法中,我们可以使用一个临时变量来存放SpringApplication.run()返回的ConfigurableApplicationContext对象,当我们完成对Spring Boot应用程序中属性的设置后,调用ConfigurableApplicationContext的#close()方法,最后再调用SpringApplication.run()方法重新给ConfigurableApplicationContext对象进行赋值已达到重启的效果。现在,我们再来看一下SpringApplication.run()方法中是如何重新创建ConfigurableApplicationContext对象的。在SpringApplication类中,run()方法会调用createApplicationContext()方法来创建一个ApplicationContext对象:protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; if (contextClass == null) { try { switch(this.webApplicationType) { case SERVLET: contextClass = Class.forName(“org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext”); break; case REACTIVE: contextClass = Class.forName(“org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext”); break; default: contextClass = Class.forName(“org.springframework.context.annotation.AnnotationConfigApplicationContext”); } } catch (ClassNotFoundException var3) { throw new IllegalStateException(“Unable create a default ApplicationContext, please specify an ApplicationContextClass”, var3); } } return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass); }createApplicationContext()方法会根据WebApplicationType类型来创建ApplicationContext对象。在WebApplicationType中定义了三种种类型:NONE、SERVLET和REACTIVE。通常情况下,将会创建servlet类型的ApplicationContext对象。接下来,我将以一个简单的Spring Boot工程来验证上述的猜想是否能够达到重启Spring Boot应用程序的需求。编码实现首先,在application.properties文件中加入如下的配置信息,为动态修改配置信息提供数据:spring.application.name= SPRING-BOOT-APPLICATION接下来,在Spring Boot主类中定义两个私有变量,用于存放main()方法的参数和SpringApplication.run()方法返回的值。下面的代码给出了主类的示例:public class ExampleRestartApplication { @Value ( “${spring.application.name}” ) String appName; private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class ); private static String[] args; private static ConfigurableApplicationContext context; public static void main(String[] args) { ExampleRestartApplication.args = args; ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args); }}最后,直接在主类中定义用于刷新并重启Spring Boot应用程序的端点(Endpoint),并使用@RestController注解对主类进行注释。@GetMapping("/refresh")public String restart(){ logger.info ( “spring.application.name:"+appName); try { PropUtil.init ().write ( “spring.application.name”,“SPRING-DYNAMIC-SERVER” ); } catch (IOException e) { e.printStackTrace ( ); } ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue<> ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ()); threadPool.execute (()->{ context.close (); context = SpringApplication.run ( ExampleRestartApplication.class,args ); } ); threadPool.shutdown (); return “spring.application.name:"+appName;}说明:为了能够重新启动Spring Boot应用程序,需要将close()和run()方法放在一个独立的线程中执行。为了验证Spring Boot应用程序在被修改重启有相关的属性有没有生效,再添加一个获取属性信息的端点,返回配置属性的信息。@GetMapping("/info”)public String info(){ logger.info ( “spring.application.name:"+appName); return appName;}完整的代码下面给出了主类的全部代码:package com.ramostear.application;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.io.IOException;import java.util.concurrent.*;/** * @author ramostear */@SpringBootApplication@RestControllerpublic class ExampleRestartApplication { @Value ( “${spring.application.name}” ) String appName; private static Logger logger = LoggerFactory.getLogger ( ExampleRestartApplication.class ); private static String[] args; private static ConfigurableApplicationContext context; public static void main(String[] args) { ExampleRestartApplication.args = args; ExampleRestartApplication.context = SpringApplication.run(ExampleRestartApplication.class, args); } @GetMapping("/refresh”) public String restart(){ logger.info ( “spring.application.name:"+appName); try { PropUtil.init ().write ( “spring.application.name”,“SPRING-DYNAMIC-SERVER” ); } catch (IOException e) { e.printStackTrace ( ); } ExecutorService threadPool = new ThreadPoolExecutor (1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue<> ( 1 ),new ThreadPoolExecutor.DiscardOldestPolicy ()); threadPool.execute (()->{ context.close (); context = SpringApplication.run ( ExampleRestartApplication.class,args ); } ); threadPool.shutdown (); return “spring.application.name:"+appName; } @GetMapping("/info”) public String info(){ logger.info ( “spring.application.name:"+appName); return appName; }}接下来,运行Spring Boot程序,下面是应用程序启动成功后控制台输出的日志信息:[2019-03-12T19:05:53.053z][org.springframework.scheduling.concurrent.ExecutorConfigurationSupport][main][171][INFO ] Initializing ExecutorService ‘applicationTaskExecutor’[2019-03-12T19:05:53.053z][org.apache.juli.logging.DirectJDKLog][main][173][INFO ] Starting ProtocolHandler [“http-nio-8080”][2019-03-12T19:05:53.053z][org.springframework.boot.web.embedded.tomcat.TomcatWebServer][main][204][INFO ] Tomcat started on port(s): 8080 (http) with context path ‘’[2019-03-12T19:05:53.053z][org.springframework.boot.StartupInfoLogger][main][59][INFO ] Started ExampleRestartApplication in 1.587 seconds (JVM running for 2.058)在测试修改系统配置并重启之前,使用Postman测试工具访问:http://localhost:8080/info ,查看一下返回的信息:成功返回SPRING-BOOT-APPLICATION提示信息。然后,访问:http://localhost:8080/refresh ,设置应用应用程序spring.application.name的值为SPRING-DYNAMIC-SERVER,观察控制台输出的日志信息:可以看到,Spring Boot应用程序已经重新启动成功,最后,在此访问:http://localhost:8080/info ,验证之前的修改是否生效:请求成功返回了SPRING-DYNAMIC-SERVER信息,最后在看一眼application.properties文件中的配置信息是否真的被修改了:配置文件的属性也被成功的修改,证明之前的猜想验证成功了。本次内容所描述的方法不适用于以JAR文件启动的Spring Boot应用程序,以WAR包的方式启动应用程序亲测可用。┏ (^^)=☞目前该药方副作用未知,如有大牛路过,还望留步指点迷津,不胜感激。结束语本次内容记录了自己验证HTTP请求重启Spring Boot应用程序试验的一次经历,文章中所涉及到的内容仅代表个人的一些观点和不成熟的想法,并未将此方法应用到实际的项目中去,如因引用本次内容中的方法应用到实际生产开发工作中所带来的风险,需引用者自行承担因风险带来的后遗症(→←)——此药方还有待商榷(O_o)(o_O)。原文作者:谭朝红原文标题:如何在生产环境中重启Spring Boot应用? ...

April 13, 2019 · 2 min · jiezi

spring boot 默认异常处理

本周在看陈杰写的自定义异常的微信异常时,使用的是自定义异常状态码和信息,在出错时将他抛出,并用@ExceptionHandler注解定义一个全局异常处理器,根据异常的内容向前台发送状态码和信息,处理异常的代码如下图://处理微信登录的异常 @ExceptionHandler(value = WechatLoginException.class) public String WechatLoginExceptionHandler(HttpServletRequest request, HttpServletResponse response, WechatLoginException e){ logger.error(“微信登录异常:—Host {} invokes url {} ERROR: {}”, request.getRemoteHost(), request.getRequestURL(), e.getMessage()); response.setStatus(e.getCode()); return e.getMessage(); }在这里我看的时候有点疑惑,将状态码写入响应,而信息却直接返回了,询问陈杰,前台果然没有接受到e.getMessage()的信息,我上网搜索了一下,推荐他使用response.sendError(code, message)这个方法来返回异常的信息,但是这么一试之后却遭到了奇怪的问题.莫名的拦截器项目配置了一个拦截器,专门用来对用户进行验证是否登录的,这个是前提.在使用response.setStatus()方法时,前台能正确的接受到传入的状态码,而使用response.sendError()时,前台却接受到的一直是401用户未登录的状态码,打了断点进行调试,分别在拦截器,跑出异常的方法,处理异常的方法打上断点,测试使用response.setStatus()和response.sendError()方法来查看执行顺序,结果让我感到惊奇:使用response.setStatus()执行顺序:使用response.sendError()执行顺序:出现了令人惊奇两点:1.setStatus()请求时没有经过拦截器2.sendError()在异常处理完毕后经过了一次拦截器查看注册拦截器配置,解决了第一个问题的疑惑:public void addInterceptors(InterceptorRegistry registry) { // 添加拦截器,去除对登录的拦截 registry.addInterceptor(authInterceptor) .excludePathPatterns("/user/login") .excludePathPatterns("/user/wechatLogin"); }这个异常是用户登录时抛出的,在注册时将登录路径给忽略了,因为我们只是拦截未登录的请求,而请求登录的请求不应该拦截,这是正确的,但第二点却怎么也不明白,本应忽略拦截的请求,为什么换了sendError()方法后,却在异常处理完毕后经过了异常拦截器?springboot的默认异常处理对比两个方法的不同:setStatus()只是改了一下状态吗,而sendError()还有请求错误的意味,于是猜想是不是请求错误才会出现这种情况,将方法直接改为throw new RuntimeException()(没有处理异常),发现拦截器拦截的请求的url居然是一个/error的url.这个/error的url并未在项目中定义过任何的控制器中,也从未发起这样的请求,上网一查询,原来这是Spring Boot提供了一个默认的映射:/error,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容.但是我们的拦截器把这个请求拦截了(并且这个请求没有携带正确的cookie),所以直接就返回了401错误,response中也没有我们定义的状态码和信息了.json还是html一切真相大白了,但忽然想到如果是浏览器发起的请求,服务器错误后springboot默认异常处理返回的是html,但是如果像我们前后台分离的请求,返回就不应该是html而是json的错误信息了,这个要怎么区分呢?使用google插件发送请求,返回的body是这样的:而用浏览器发起的请求返回的却是一个html页面:<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id=‘created’>Sat Apr 13 21:34:34 CST 2019</div><div>There was an unexpected error (type=Internal Server Error, status=500).</div><div>No message available</div></body></html>仔细查看两者发起的请求不同,在浏览器发起请求信息requestheader上发现了Accept字段:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8顿时就明白了,在发送请求时spring-boot根据Accept字段来给你返回响应的内容,例如application/json返回json,text/html返回html,真是感叹spring-boot真是太周全了.总结spring-boot好心帮你默认请求异常,但是却给你带来了麻烦,感觉还是自己理解的不够多,学习路还远着呢。 ...

April 13, 2019 · 1 min · jiezi

你真的理解 Spring Boot 项目中的 parent 吗?

前面和大伙聊了 Spring Boot 项目的三种创建方式,这三种创建方式,无论是哪一种,创建成功后,pom.xml 坐标文件中都有如下一段引用:<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath/> <!– lookup parent from repository –></parent>对于这个 parent 的作用,你是否完全理解?有小伙伴说,不就是依赖的版本号定义在 parent 里边吗?是的,没错,但是 parent 的作用可不仅仅这么简单哦!本文松哥就来和大伙聊一聊这个 parent 到底有什么作用。基本功能当我们创建一个 Spring Boot 工程时,可以继承自一个 spring-boot-starter-parent ,也可以不继承自它,我们先来看第一种情况。先来看 parent 的基本功能有哪些?定义了 Java 编译版本为 1.8 。使用 UTF-8 格式编码。继承自 spring-boot-dependencies,这个里边定义了依赖的版本,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本号。执行打包操作的配置。自动化的资源过滤。自动化的插件配置。针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的配置文件,例如 application-dev.properties 和 application-dev.yml。请注意,由于application.properties和application.yml文件接受Spring样式占位符 $ {…} ,因此 Maven 过滤更改为使用 @ .. @ 占位符,当然开发者可以通过设置名为 resource.delimiter 的Maven 属性来覆盖 @ .. @ 占位符。源码分析当我们创建一个 Spring Boot 项目后,我们可以在本地 Maven 仓库中看到看到这个具体的 parent 文件,以 2.1.4 这个版本为例,松哥 这里的路径是 C:\Users\sang.m2\repository\org\springframework\boot\spring-boot-starter-parent\2.1.4.RELEASE\spring-boot-starter-parent-2.1.4.RELEASE.pom ,打开这个文件,快速阅读文件源码,基本上就可以证实我们前面说的功能,如下图: 我们可以看到,它继承自 spring-boot-dependencies ,这里保存了基本的依赖信息,另外我们也可以看到项目的编码格式,JDK 的版本等信息,当然也有我们前面提到的数据过滤信息。最后,我们再根据它的 parent 中指定的 spring-boot-dependencies 位置,来看看 spring-boot-dependencies 中的定义: 在这里,我们看到了版本的定义以及 dependencyManagement 节点,明白了为啥 Spring Boot 项目中部分依赖不需要写版本号了。不用 parent但是并非所有的公司都需要这个 parent ,有的时候,公司里边会有自己定义的 parent ,我们的 Spring Boot 项目要继承自公司内部的 parent ,这个时候该怎么办呢? 一个简单的办法就是我们自行定义 dependencyManagement 节点,然后在里边定义好版本号,再接下来在引用依赖时也就不用写版本号了,像下面这样:<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.4.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>这样写之后,依赖的版本号问题虽然解决了,但是关于打包的插件、编译的 JDK 版本、文件的编码格式等等这些配置,在没有 parent 的时候,这些统统要自己去配置。总结好了,一篇简单的文章,向大伙展示一下 Spring Boot 项目中 parent 的作用,有问题欢迎留言讨论。 关注松哥,公众号内回复 牧码小子,获取松哥私藏多年的Java干货哦! ...

April 13, 2019 · 1 min · jiezi

3分钟干货之Spring Boot注解

1、 @SpringBootApplication这是 Spring Boot 最最最核心的注解,用在 Spring Boot 主类上,标识这是一个 Spring Boot 应用,用来开启 Spring Boot 的各项能力。其实这个注解就是 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 这三个注解的组合,也可以用这三个注解来代替 @SpringBootApplication 注解。2、@EnableAutoConfiguration允许 Spring Boot 自动配置注解,开启这个注解之后,Spring Boot 就能根据当前类路径下的包或者类来配置 Spring Bean。如:当前类路径下有 Mybatis 这个 JAR 包,MybatisAutoConfiguration 注解就能根据相关参数来配置 Mybatis 的各个 Spring Bean。3、@Configuration这是 Spring 3.0 添加的一个注解,用来代替 applicationContext.xml 配置文件,所有这个配置文件里面能做到的事情都可以通过这个注解所在类来进行注册。

April 12, 2019 · 1 min · jiezi

spring-boot 自定义异常

需求:小程序登录时,后台需要将异常抛出,并将错误信息交给前台。一、后台异常处理1.自定义微信登录异常 public class WechatLoginException extends RuntimeException { /** * @Description 自定义状态码 * 510: 未找到该学号或该学号的状态已冻结 * 509: 该学号已被其他微信帐号绑定,请联系老师或者管理员解除绑定 * 508: 该微信帐号已绑定学号,请解除绑定后重新登录 **/ private HttpStatus status = null; public WechatLoginException(String message, HttpStatus httpStatus) { super(message); status = httpStatus; } public HttpStatus getStatus() { return status; }}2.userService抛出异常 if (student.getWechat() != null && !student.getWechat().getOpenid().equals(openId)) { throw new WechatLoginException(“该学号已被其他微信帐号绑定,请联系老师或者管理员解除绑定”, HttpStatus.valueOf(509)); }3. 全局异常处理// 处理微信登录的异常@ExceptionHandler(WechatLoginException.class)public ResponseEntity<JsonErrorResult> wechatLoginHandler(HttpServletRequest request, WechatLoginException e){ logger.error(“微信登录异常:—Host {} invokes url {} ERROR: {}”, request.getRemoteHost(), request.getRequestURL(), e.getMessage()); return new ResponseEntity<>(new JsonErrorResult(request, e), e.getStatus());}存在的问题后来,潘老师和张喜硕学长评论说这样写不好,因为开放异常信息,异常的状态码只靠注释约束,当返回未知状态码前台不知道如何处理。二、改进根据潘老师的建议,做出如下修改:1. 添加异常状态枚举public enum HttpStatusEnum { StudentNotFound(“未找到该学生”, 601), StudentIsForzen(“该学生的状态已冻结”, 602), StudentISBinded(“该学号已被其他微信帐号绑定,请联系老师或者管理员解除绑定”, 603), WecahtIsBinded(“该微信帐号已绑定学号,请解除绑定后重新登录”, 604); private String description; private int code; private HttpStatusEnum (String description, int code) { this.description = description; this.code = code; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public int getCode() { return code; } public void setCode(int code) { this.code = code; }}2. 异常类修改,构造时传入枚举体public class WechatLoginException extends RuntimeException { private int code; // 异常状态码 public WechatLoginException(HttpStatusEnum httpStatusEnum) { super(httpStatusEnum.getDescription()); code = httpStatusEnum.getCode(); } public int getCode() { return code; }}3. service抛出异常throw new WechatLoginException(HttpStatusEnum.StudentISBinded);4. 全局异常处理//处理微信登录的异常@ExceptionHandler(WechatLoginException.class)public ResponseEntity<JsonErrorResult> wechatLoginHandler(HttpServletRequest request, WechatLoginException e){ logger.error(“微信登录异常:—Host {} invokes url {} ERROR: {}”, request.getRemoteHost(), request.getRequestURL(), e.getMessage()); return new ResponseEntity<>(new JsonErrorResult(request, e), e.getStatus());}然后这一步就出现了问题,使用ResponseEntity返回异常信息时,第二个参数需要传入HttpStatus,如图:而张喜硕学长指出,spring-boot的HttpStatus枚举是不可扩展的,即我无法通过继承来增加自定义的http状态码所以,此时陷入一个僵局,要么放弃使用自定义状态码,要么放弃使用ResponseEntity,这时,张喜硕学长说了一下他的解决方案,即不返回ResponseEntity了,而是注入HttpServletResponse,调取setStatus()直接改返回的状态码:// 处理微信登录的异常@ExceptionHandler(value = WechatLoginException.class)public String WechatLoginExceptionHandler(HttpServletRequest request, HttpServletResponse response, WechatLoginException e) throws IOException { logger.error(“微信登录异常:—Host {} invokes url {} ERROR: {}”, request.getRemoteHost(), request.getRequestURL(), e.getMessage()); response.setStatus(e.getCode()); return e.getMessage();}然后在小程序端测试,发现果然状态码可以变成了自定义的了接着,就是在小程序端添加异常处理: public static handleHttpException(response: RequestSuccessCallbackResult) { let result = ‘’; switch (response.statusCode) { case 601: result = ‘未找到该学生’; break; case 602: result = ‘该学生的状态已冻结’; break; case 603: result = ‘该学号已被其他微信帐号绑定,请联系老师或者管理员解除绑定’; break; case 604: result = ‘该微信帐号已绑定学号,请解除绑定后重新登录’; } // 如果异常信息不为空,则显示提示信息,并且抛出异常 if (result != ‘’) { // 显示异常 wx.showToast({ title: result, icon: “none”, duration: Request.duration }); throw response; } }最后的效果:总结总得来说,这次收获还是挺多的,遇到了不少问题,也对一些知识的盲区进行了探索和挖掘。虽然实现了需求,但觉得小程序的异常处理还是有些生硬,还有改进的余地。 ...

April 12, 2019 · 2 min · jiezi

几个我喜欢的博主

1、纯洁的微笑 ,某理财公司的技术主管,出色分享有《SpringBoot42讲》,很详细。有一些技术人文的东西,真的有用心在写。他的领土 2、邢森|电信架构师 有意思的思考与分享,超喜欢《跟我开发区块链商业应用:超级账本》的,可惜不太热衷网络分享呀,很少看到他发布的其他文章。3、

April 12, 2019 · 1 min · jiezi

创建一个 Spring Boot 项目,你会几种方法?

我最早是 2016 年底开始写 Spring Boot 相关的博客,当时使用的版本还是 1.4.x ,文章发表在 CSDN 上,阅读量最大的一篇有 42W+,如下图: 2017 年由于种种原因,就没有再继续更新 Spring Boot 相关的博客了,2018年又去写书了,也没更新,现在 Spring Boot 最新稳定版是 2.1.4 ,松哥想针对此写一个系列教程,专门讲 Spring Boot2 中相关的知识点。这个系列,就从本篇开始吧。Spring Boot 介绍我们刚开始学习 JavaWeb 的时候,使用 Servlet/JSP 做开发,一个接口搞一个 Servlet ,很头大,后来我们通过隐藏域或者反射等方式,可以减少 Servlet 的创建,但是依然不方便,再后来,我们引入 Struts2/SpringMVC 这一类的框架,来简化我们的开发 ,和 Servlet/JSP 相比,引入框架之后,生产力确实提高了不少,但是用久了,又发现了新的问题,即配置繁琐易出错,要做一个新项目,先搭建环境,环境搭建来搭建去,就是那几行配置,不同的项目,可能就是包不同,其他大部分的配置都是一样的,Java 总是被人诟病配置繁琐代码量巨大,这就是其中一个表现。那么怎么办?Spring Boot 应运而生,Spring Boot 主要提供了如下功能:为所有基于 Spring 的 Java 开发提供方便快捷的入门体验。开箱即用,有自己自定义的配置就是用自己的,没有就使用官方提供的默认的。提供了一系列通用的非功能性的功能,例如嵌入式服务器、安全管理、健康检测等。绝对没有代码生成,也不需要XML配置。Spring Boot 的出现让 Java 开发又回归简单,因为确确实实解决了开发中的痛点,因此这个技术得到了非常广泛的使用,松哥很多朋友出去面试 Java 工程师,从2017年年初开始,Spring Boot基本就是必问,现在流行的 Spring Cloud 微服务也是基于 Spring Boot,因此,所有的 Java 工程师都有必要掌握好 Spring Boot。系统要求截至本文写作(2019.04.11),Spring Boot 目前最新版本是 2.1.4,要求至少 JDK8,集成的 Spring 版本是 5.1.6 ,构建工具版本要求如下:Build ToolVersionMaven3.3+Gradle4.4+内置的容器版本分别如下:NameVersionTomcat 9.04.0Jetty 9.43.1Undertow 2.04.0三种创建方式初学者看到 Spring Boot 工程创建成功后有那么多文件就会有点懵圈,其实 Spring Boot 工程本质上就是一个 Maven 工程,从这个角度出发,松哥在这里向大家介绍三种项目创建方式。在线创建这是官方提供的一个创建方式,实际上,如果我们使用开发工具去创建 Spring Boot 项目的话(即第二种方案),也是从这个网站上创建的,只不过这个过程开发工具帮助我们完成了,我们只需要在开发工具中进行简单的配置即可。 首先打开 https://start.spring.io 这个网站,如下: 这里要配置的按顺序分别如下:项目构建工具是 Maven 还是 Gradle ?松哥见到有人用 Gradle 做 Java 后端项目,但是整体感觉 Gradle 在 Java 后端中使用的还是比较少,Gradle 在 Android 中使用较多,Java 后端,目前来看还是 Maven 为主,因此这里选择第一项。开发语言,这个当然是选择 Java 了。Spring Boot 版本,可以看到,目前最新的稳定版是 2.1.4 ,这里我们就是用最新稳定版。既然是 Maven 工程,当然要有项目坐标,项目描述等信息了,另外这里还让输入了包名,因为创建成功后会自动创建启动类。Packing 表示项目要打包成 jar 包还是 war 包,Spring Boot 的一大优势就是内嵌了 Servlet 容器,打成 jar 包后可以直接运行,所以这里建议打包成 jar 包,当然,开发者根据实际情况也可以选择 war 包。然后选选择构建的 JDK 版本。最后是选择所需要的依赖,输入关键字如 web ,会有相关的提示,这里我就先加入 web 依赖。所有的事情全部完成后,点击最下面的 Generate Project 按钮,或者点击 Alt+Enter 按键,此时会自动下载项目,将下载下来的项目解压,然后用 IntelliJ IDEA 或者 Eclipse 打开即可进行开发。使用开发工具创建有人觉得上面的步骤太过于繁琐,那么也可以使用 IDE 来创建,松哥这里以 IntelliJ IDEA 和 STS 为例,需要注意的是,IntelliJ IDEA 只有 ultimate 版才有直接创建 Spring Boot 项目的功能,社区版是没有此项功能的。IntelliJ IDEA首先在创建项目时选择 Spring Initializr,如下图: 然后点击 Next ,填入 Maven 项目的基本信息,如下: 再接下来选择需要添加的依赖,如下图: 勾选完成后,点击 Next 完成项目的创建。STS这里我再介绍下 Eclipse 派系的 STS 给大家参考, STS 创建 Spring Boot 项目,实际上也是从上一小节的那个网站上来的,步骤如下: 首先右键单击,选择 New -> Spring Starter Project ,如下图: 然后在打开的页面中填入项目的相关信息,如下图: 这里的信息和前面提到的都一样,不再赘述。最后一路点击 Next ,完成项目的创建。Maven 创建上面提到的几种方式,实际上都借助了 https://start.spring.io/ 这个网站,松哥记得在 2017 年的时候,这个网站还不是很稳定,经常发生项目创建失败的情况,从2018年开始,项目创建失败就很少遇到了,不过有一些读者偶尔还是会遇到这个问题,他们会在微信上问松哥这个问题腰怎么处理?我一般给的建议就是直接使用 Maven 来创建项目。步骤如下: 首先创建一个普通的 Maven 项目,以 IntelliJ IDEA 为例,创建步骤如下: 注意这里不用选择项目骨架(如果大伙是做练习的话,也可以去尝试选择一下,这里大概有十来个 Spring Boot 相关的项目骨架),直接点击 Next ,下一步中填入一个 Maven 项目的基本信息,如下图: 然后点击 Next 完成项目的创建。 创建完成后,在 pom.xml 文件中,添加如下依赖:<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies>添加成功后,再在 java 目录下创建包,包中创建一个名为 App 的启动类,如下:@EnableAutoConfiguration@RestControllerpublic class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } @GetMapping("/hello") public String hello() { return “hello”; }}@EnableAutoConfiguration 注解表示开启自动化配置。 然后执行这里的 main 方法就可以启动一个 Spring Boot 工程了。项目结构使用工具创建出来的项目结构大致如下图: 对于我们来说,src 是最熟悉的, Java 代码和配置文件写在这里,test 目录用来做测试,pom.xml 是 Maven 的坐标文件,就这几个。总结本文主要向大家介绍了三种创建 Spring Boot 工程的方式,大家有更6的方法欢迎来讨论。 关注松哥,公众号内回复 牧码小子,获取松哥私藏多年的Java干货哦! ...

April 12, 2019 · 2 min · jiezi

Java异常处理12条军规

摘要: 简单实用的建议。原文:Java异常处理12条军规公众号:Spring源码解析Fundebug经授权转载,版权归原作者所有。在Java语言中,异常从使用方式上可以分为两大类:CheckedExceptionUncheckedException在Java中类的异常结构图如下:可检查异常需要在方法上声明,一般要求调用者必须感知异常可能发生,并且对可能发生的异常进行处理。可以理解成系统正常状态下很可能发生的情况,通常发生在通过网络调用外部系统或者使用文件系统时,在这种情况下,错误是可能恢复的,调用者可以根据异常做出必要的处理,例如重试或者资源清理等。非检查异常是不需要在throws子句中声明的异常。JVM根本不会强制您处理它们,因为它们主要是由于程序错误而在运行时生成的。它们扩展了RuntimeException。最常见的例子是NullPointerException 可能不应该重试未经检查的异常,并且正确的操作通常应该是什么都不做,并让它从您的方法和执行堆栈中出来。在高执行级别,应记录此类异常。Error是最为严重的运行时错误,几乎是不可能恢复和处理,一些示例是OutOfMemoryError,LinkageError和StackOverflowError。它们通常会使程序或程序的一部分崩溃。只有良好的日志记录练习才能帮助您确定错误的确切原因.在异常处理时的几点建议:1. 永远不要catch中吞掉异常,否则在系统发生错误时,你永远不知道到底发生了什么catch (SomeException e) { return null;}2. 尽量使用特定的异常而不是一律使用Exception这样太泛泛的异常public void foo() throws Exception { //错误的做法}public void foo() throws MyBusinessException1, MyBusinessException2 { //正确的做法}一味的使用Exception,这样就违背了可检查异常的设计初衷,因为调用都不知道Exception到底是什么,也不知道该如何处理。捕获异常时,也不要捕获范围太大,例如捕获Exception,相反,只捕获你能处理的异常,应该处理的异常。即然方法的声明者在方法上声明了不同类型的可检查异常,他是希望调用者区别对待不同异常的。3. Never catch Throwable class永远不要捕获Throwable,因为Error也是继承自它,Error是Jvm都处理不了的错误,你能处理?所以基于有些Jvm在Error时就不会让你catch住。4. 正确的封装和传递异常**不要丢失异常栈,因为异常栈对于定位原始错误很关键catch (SomeException e) {throw new MyServiceException(“Some information: " + e.getMessage()); //错误的做法}一定要保留原始的异常:catch (SomeException e) { throw new MyServiceException(“Some information: " , e); //正确的打开方式}5. 要打印异常,就不要抛出,不要两者都做catch (SomeException e) { LOGGER.error(“Some information”, e); throw e;}这样的log没有任何意义,只会打印出一连串的error log,对于定位问题无济于事。6. 不要在finally块中抛出异常如果在finally中抛出异常,将会覆盖原始的异常,如果finally中真的可能会发生异常,那一定要处理并记录它,不要向上抛。7. 不要使用printStackTrace要给异常添加上有用的上下文信息,单纯的异常栈,没有太大意义8. Throw early catch late异常界著名的原则,错误发生时及早抛出,然后在获得所以全部信息时再捕获处理.也可以理解为在低层次抛出的异常,在足够高的抽象层面才能更好的理解异常,然后捕获处理。9. 对于使用一些重量级资源的操作,发生异常时,一定记得清理如网络连接,数据库操作等,可以用try finally来做clean up的工作。10. 不要使用异常来控制程序逻辑流程我们总是不经意间这么做了,这样使得代码变更丑陋,使得正常业务逻辑和错误处理混淆不清;而且也可能会带来性能问题,因为异常是个比较重的操作。11. 及早校验用户的输入在最边缘的入口校验用户的输入,这样使得我们不用再更底层逻辑中处处校验参数的合法性,能大大简化业务逻辑中不必要的异常处理逻辑;相反,在业务中不如果担心参数的合法性,则应该使用卫语句抛出运行时异常,一步步把对参数错误的处理推到系统的边缘,保持系统内部的清洁。12. 在打印错误的log中尽量在一行中包含尽可能多的上下文LOGGER.debug(“enter A”);LOGGER.debug(“enter B”); //错误的方式LOGGER.debug(“enter A, enter B”);//正确的方式 ...

April 12, 2019 · 1 min · jiezi

Mybatis-Plus 真好用(乡村爱情加持)

写在前面MyBatis的增强方案确实有不少,甚至有种感觉是现在如果只用 “裸MyBatis”,不来点增强插件都不好意思了。这不,在上一篇文章《Spring Boot项目利用MyBatis Generator进行数据层代码自动生成》 中尝试了一下 MyBatis Generator。这次来点更加先进的 Mybatis-Plus,SQL语句都不用写了,分页也是自动完成,嗯,真香!数据库准备CREATE TABLE tbl_user( user_id BIGINT(20) NOT NULL COMMENT ‘主键ID’, user_name VARCHAR(30) NULL DEFAULT NULL COMMENT ‘姓名’, user_age INT(11) NULL DEFAULT NULL COMMENT ‘年龄’, PRIMARY KEY (user_id)) charset = utf8;MyBatis-Plus加持工程搭建 (不赘述了)依赖引入<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version></dependency><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>8.0.12</version></dependency>主要是 Mybatis Plus、Lombok(不知道Lombok干嘛的?可以看这里)、Druid连接池 等依赖。MyBatis Plus配置项目配置mybatis-plus: mapper-locations: classpath:/mapper/*Mapper.xml新增 MyBatis Plus配置类@Configuration@MapperScan(“cn.codesheep.springbtmybatisplus.mapper”)public class MyBatisConfig {}看到没,几乎零配置啊,下面就可以写业务逻辑了业务编写实体类@Data@TableName(“tbl_user”)public class User { @TableId(value = “user_id”) private Long userId; private String userName; private Integer userAge;}Mapper类public interface UserMapper extends BaseMapper<User> {}这里啥接口方法也不用写,就可以实现增删改查了!Service类Service接口:public interface UserService extends IService<User> { int insertUser( User user ); int updateUser( User user ); int deleteUser( User user ); User findUserByName( String userName ); IPage getUserPage( Page page, User user );}Service实现:@Service@AllArgsConstructorpublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { // 增 @Override public int insertUser(User user) { return baseMapper.insert( user ); } // 改 @Override public int updateUser(User user) { return baseMapper.updateById( user ); } // 删 @Override public int deleteUser(User user) { return baseMapper.deleteById( user.getUserId() ); } // 查 @Override public User findUserByName( String userName ) { return baseMapper.getUserByName( userName ); }}Controller类@RestController@RequestMapping("/user")public class UserContorller { @Autowired private UserService userService; // 增 @PostMapping( value = “/insert”) public Object insert( @RequestBody User user ) { return userService.insertUser( user ); } // 改 @PostMapping( value = “/update”) public Object update( @RequestBody User user ) { return userService.updateUser( user ); } // 删 @PostMapping( value = “/delete”) public Object delete( @RequestBody User user ) { return userService.deleteUser( user ); } // 查 @GetMapping( value = “/getUserByName”) public Object getUserByName( @RequestParam String userName ) { return userService.findUserByName( userName ); }}通过以上几个简单的步骤,我们就实现了 tbl_user表的增删改查,传统 MyBatis的 XML文件一个都不需要写!实际实验【《乡爱》加持】启动项目很牛批的 logo就会出现接下来通过 Postman来发送增删改查的请求插入记录通过 Postman随便插入几条记录 POST localhost:8089/user/insert{“userId”:3,“userName”:“刘能”,“userAge”:“58”}{“userId”:4,“userName”:“赵四”,“userAge”:“58”}{“userId”:5,“userName”:“谢广坤”,“userAge”:“58”}{“userId”:6,“userName”:“刘大脑袋”,“userAge”:“58”}修改记录修改记录时需要带用户ID,比如我们修改 赵四 那条记录的名字为 赵四(Zhao Four)删除记录修改记录时同样需要带用户ID,比如删除ID=6 那条 刘大脑袋的记录查询记录(普通查询,下文讲分页查询)比如,按照名字来查询:GET localhost:8089/user/getUserByName?userName=刘能最关心的分页问题首先装配分页插件@Beanpublic PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor();}Mapper类public interface UserMapper extends BaseMapper<User> { // 普通查询 User getUserByName( String userName ); // 分页查询 IPage<List<User>> getUsersPage( Page page, @Param(“query”) User user );}Service类@Service@AllArgsConstructorpublic class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { // 查:普通查 @Override public User findUserByName( String userName ) { return baseMapper.getUserByName( userName ); } // 分页查 @Override public IPage getUserPage(Page page, User user) { return baseMapper.getUsersPage( page, user ); }}Controller类@GetMapping( value = “/page”)public Object getUserPage( Page page, User user ) { return userService.getUserPage( page, user );}实际实验一下,我们分页查询 年龄 = 58 的多条记录:可以看到结果数据中,除了给到当前页数据,还把总记录条数,总页数等一并返回了,很是优雅呢 !写在最后由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

April 12, 2019 · 2 min · jiezi

SpringBoot 仿抖音短视频小程序开发(三)

SpringBoot 仿抖音短视频小程序开发(一):项目的简介(https://segmentfault.com/a/11...SpringBoot 仿抖音短视频小程序开发(二):项目功能分析与具体实现(https://segmentfault.com/a/11…源代码: SpringBoot 仿抖音短视频小程序开发 全栈式实战项目(https://gitee.com/scau_zns/sh…)短视频后台管理系统:(https://gitee.com/scau_zns/sh…小程序的后台管理系统涉及的技术栈:Bootstrap + jQuery + jGrid + SSM框架 + zookeeper一、用户列表的获取与分页前端代码: <div class=“usersList_wrapper”> <!– 用户列表展示的表格 –> <table id=“usersList”></table> <!– 底部的分页条 –> <div id=“usersListPager”></div> </div> jGrid发送请求获取数据封装好展示到页面// 用户列表 var handleList = function() { // 上下文对象路径 var hdnContextPath = $("#hdnContextPath").val(); var apiServer = $("#apiServer").val(); var jqGrid = $("#usersList"); jqGrid.jqGrid({ caption: “短视频用户列表”, url: hdnContextPath + “/users/list.action”, mtype: “post”, styleUI: ‘Bootstrap’,//设置jqgrid的全局样式为bootstrap样式 datatype: “json”, colNames: [‘ID’, ‘头像’, ‘用户名’, ‘昵称’, ‘粉丝数’, ‘关注数’, ‘获赞数’], colModel: [ { name: ‘id’, index: ‘id’, width: 30, sortable: false, hidden: false }, { name: ‘faceImage’, index: ‘username’, width: 50, sortable: false, formatter:function(cellvalue, options, rowObject) { <!– 配置的虚拟目录apiServer = http://192.168.199.150:8080 –> var src = apiServer + cellvalue; var img = “<img src=’” + src + “’ width=‘120’></img>” return img; } }, { name: ‘username’, index: ‘password’, width: 30, sortable: false }, { name: ’nickname’, index: ’nickname’, width: 30, sortable: false }, { name: ‘fansCounts’, index: ‘age’, width: 20, sortable: false }, { name: ‘followCounts’, index: ‘sexValue’, width: 20, sortable: false }, { name: ‘receiveLikeCounts’, index: ‘province’, width: 20, sortable: false, hidden: false } ], viewrecords: true, // 定义是否要显示总记录数 rowNum: 10, // 在grid上显示记录条数,这个参数是要被传递到后台 rownumbers: true, // 如果为ture则会在表格左边新增一列,显示行顺序号,从1开始递增。此列名为’rn’ autowidth: true, // 如果为ture时,则当表格在首次被创建时会根据父元素比例重新调整表格宽度。如果父元素宽度改变,为了使表格宽度能够自动调整则需要实现函数:setGridWidth height: 500, // 表格高度,可以是数字,像素值或者百分比 rownumWidth: 36, // 如果rownumbers为true,则可以设置行号 的宽度 pager: “#usersListPager”, // 分页控件的id subGrid: false // 是否启用子表格 }).navGrid(’#usersListPager’, { edit: false, add: false, del: false, search: false }); // 随着窗口的变化,设置jqgrid的宽度 $(window).bind(‘resize’, function () { var width = $(’.usersList_wrapper’).width()0.99; jqGrid.setGridWidth(width); }); // 不显示水平滚动条 jqGrid.closest(".ui-jqgrid-bdiv").css({ “overflow-x” : “hidden” }); // 条件查询所有用户列表 $("#searchUserListButton").click(function(){ var searchUsersListForm = $("#searchUserListForm"); jqGrid.jqGrid().setGridParam({ page: 1, url: hdnContextPath + “/users/list.action?” + searchUsersListForm.serialize(), }).trigger(“reloadGrid”); }); }后端获取用户列表分页数据的接口: @PostMapping("/list") @ResponseBody public PagedResult list(Users user , Integer page) { PagedResult result = usersService.queryUsers(user, page == null ? 1 : page, 10); return result; }搜索功能的实现:<!– 搜索内容 –> <div class=“col-md-12”> <br/> <form id=“searchUserListForm” class=“form-inline” method=“post” role=“form”> <div class=“form-group”> <label class=“sr-only” for=“username”>用户名:</label> <input id=“username” name=“username” type=“text” class=“form-control” placeholder=“用户名” /> </div> <div class=“form-group”> <label class=“sr-only” for=“nickname”>昵称:</label> <input id=“nickname” name=“nickname” type=“text” class=“form-control” placeholder=“昵称” /> </div> <button id=“searchUserListButton” class=“btn yellow-casablanca” type=“button”>搜 索</button> </form> </div>使用jGrid发送请求给后台 // 条件查询所有用户列表 $("#searchUserListButton").click(function(){ var searchUsersListForm = $("#searchUserListForm"); jqGrid.jqGrid().setGridParam({ page: 1, url: hdnContextPath + “/users/list.action?” + searchUsersListForm.serialize(), }).trigger(“reloadGrid”); });二、背景音乐BGM的上传、查询和删除上传 $("#file").fileupload({ pasteZone: “#bgmContent”, dataType: “json”, done: function(e, data) { console.log(data); if (data.result.status != ‘200’) { alert(“长传失败…”); } else { var bgmServer = $("#bgmServer").val(); var url = bgmServer + data.result.data; $("#bgmContent").html("<a href=’" + url + “’ target=’_blank’>点我播放</a>”); $("#path").attr(“value”, data.result.data); } } });后台接口保存BGM的方法参考上传头像的方法分页查询参考用户列表信息的分页查询多少删除BGM var deleteBgm = function(bgmId) { var flag = window.confirm(“是否确认删除???”); if (!flag) { return; } $.ajax({ url: $("#hdnContextPath").val() + ‘/video/delBgm.action?bgmId=’ + bgmId, type: “POST”, success: function(data) { if (data.status == 200 && data.msg == ‘OK’) { alert(‘删除成功~~’); var jqGrid = $("#bgmList"); jqGrid.jqGrid().trigger(“reloadGrid”); } } })}三、举报管理禁止播放var forbidVideo = function(videoId) { var flag = window.confirm(“是否禁播”); if (!flag) { return; } $.ajax({ url: $("#hdnContextPath").val() + “/video/forbidVideo.action?videoId=” + videoId, type: “POST”, async: false, success: function(data) { if(data.status == 200 && data.msg == “OK”) { alert(“操作成功”); var jqGrid = $("#usersReportsList"); //reloadGrid是重新加载表格 jqGrid.jqGrid().trigger(“reloadGrid”); } else { console.log(JSON.stringify(data)); } } })}四、后台管理系统增加或删除BGM,向zookeeper-server创建子节点,让小程序后端监听【重点】1、首先安装Zookeeper到Linux上,启动服务器2、编写zk客户端代码:import org.apache.curator.framework.CuratorFramework;import org.apache.zookeeper.CreateMode;import org.apache.zookeeper.ZooDefs.Ids;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class ZKCurator { //zk客户端 private CuratorFramework client = null; final static Logger log = LoggerFactory.getLogger(ZKCurator.class); public ZKCurator(CuratorFramework client) { this.client = client; } public void init() { client = client.usingNamespace(“admin”); try { //判断在admin命名空间下是否有bgm节点 /admin/bgm if( client.checkExists().forPath("/bgm") == null ) { //对于zk来讲,有两种类型的节点,一种是持久节点(永久存在,除非手动删除),另一种是临时节点(会话断开,自动删除) client.create().creatingParentContainersIfNeeded() .withMode(CreateMode.PERSISTENT) //持久节点 .withACL(Ids.OPEN_ACL_UNSAFE) //匿名权限 .forPath("/bgm"); log.info(“zookeeper客户端连接初始化成功”); log.info(“zookeeper服务端状态:{}",client.isStarted()); } } catch (Exception e) { log.error(“zookeeper客户端连接初始化失败”); e.printStackTrace(); } } /* * 增加或者删除Bgm,向zk-server创建子节点,供小程序后端监听 * @param bgmId * @param operType */ public void sendBgmOperator(String bgmId, String operObject) { try { client.create().creatingParentContainersIfNeeded() .withMode(CreateMode.PERSISTENT) //持久节点 .withACL(Ids.OPEN_ACL_UNSAFE) //匿名权限 .forPath("/bgm/” + bgmId, operObject.getBytes()); } catch (Exception e) { e.printStackTrace(); } }}3、在applicationContext-zookeeper.xml配置zookeeper: <!– 创建重连策列 –> <bean id=“retryPolicy” class=“org.apache.curator.retry.ExponentialBackoffRetry”> <!– 每次重试连接的等待时间 –> <constructor-arg index=“0” value=“1000”></constructor-arg> <!– 设置最大的重连次数 –> <constructor-arg index=“1” value=“5”></constructor-arg> </bean> <!– 创建zookeeper客户端 –> <bean id=“client” class=“org.apache.curator.framework.CuratorFrameworkFactory” factory-method=“newClient” init-method=“start”> <constructor-arg index=“0” value=“120.79.18.35:2181”></constructor-arg> <constructor-arg index=“1” value=“10000”></constructor-arg> <constructor-arg index=“2” value=“10000”></constructor-arg> <constructor-arg index=“3” ref=“retryPolicy”></constructor-arg> </bean> <!– 调用init方法启动 –> <bean id=“ZKCurator” class=“com.imooc.web.util.ZKCurator” init-method=“init”> <constructor-arg index=“0” ref=“client”></constructor-arg> </bean>4、上传或者删除BGM时调用VideoServiceImpl.java的方法 @Autowired private ZKCurator zKCurator; @Override public void addBgm(Bgm bgm) { String id = sid.nextShort(); bgm.setId(id); bgmMapper.insert(bgm); Map<String, String> map = new HashMap<>(); map.put(“operType”, BGMOperatorTypeEnum.ADD.type); map.put(“path”, bgm.getPath()); zKCurator.sendBgmOperator(id, JSONUtils.toJSONString(map)); } @Override public void deleteBgm(String id) { Bgm bgm = bgmMapper.selectByPrimaryKey(id); bgmMapper.deleteByPrimaryKey(id); Map<String, String> map = new HashMap<>(); map.put(“operType”, BGMOperatorTypeEnum.DELETE.type); map.put(“path”, bgm.getPath()); zKCurator.sendBgmOperator(id, JSONUtils.toJSONString(map)); }5、小程序编写代码监听zookeeper的节点,并对其做出相应的删除和上传操作【重点】初始化zookeeper客户端 private CuratorFramework client = null; final static Logger log = LoggerFactory.getLogger(ZKCuratorClient.class);// public static final String ZOOKEEPER_SERVER = “120.79.18.36:2181”; public void init() { if(client != null) { return; } //重试策略 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 5); //创建zk客户端 120.79.18.36:2181 client = CuratorFrameworkFactory.builder().connectString(resourceConfig.getZookeeperServer()).sessionTimeoutMs(10000) .retryPolicy(retryPolicy).namespace(“admin”).build(); //启动客户端 client.start(); try { addChildWatch("/bgm"); } catch (Exception e) { e.printStackTrace(); } } 监听zk-server的节点,当短视频后台管理系统上传或者删除某个BGM的时候,小程序后台服务器通过Zookeeper监听自动下载背景音乐public void addChildWatch(String nodePath) throws Exception { final PathChildrenCache cache = new PathChildrenCache(client, nodePath, true); cache.start(); cache.getListenable().addListener(new PathChildrenCacheListener() { @Override public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception { if(event.getType().equals(PathChildrenCacheEvent.Type.CHILD_ADDED)){ log.info(“监听到事件CHILD_ADDED”); //1. 从数据库查询bgm对象,获取路径Path String path = event.getData().getPath(); String operatorObjStr = new String(event.getData().getData()); Map<String, String> map = JsonUtils.jsonToPojo(operatorObjStr, Map.class); String operatorType = map.get(“operType”); String songPath = map.get(“path”);// String[] arr = path.split("/");// String bgmId = arr[arr.length-1];// Bgm bgm = bgmService.queryBgmById(bgmId);// if(bgm == null){// return;// } //1.1 bgm所在的相对路径// String songPath = bgm.getPath(); //2. 定义保存到本地的bgm路径// String filePath = “E:\imooc_videos_dev” + songPath; String filePath = resourceConfig.getFileSpace() + songPath; //3. 定义下载的路径(播放url) String[] arrPath = songPath.split("\\"); //windows// String[] arrPath = songPath.split("/"); //linux String finalPath = “”; //3.1 处理url的斜杠以及编码 for(int i=0; i<arrPath.length;i++){ if(StringUtils.isNotBlank(arrPath[i])) { finalPath += “/”; finalPath += URLEncoder.encode(arrPath[i], “UTF-8”); } }// String bgmUrl = “http://192.168.199.150:8080/mvc” + finalPath; String bgmUrl = resourceConfig.getBgmServer() + finalPath; if(operatorType.equals(“1”)){ //下载bgm到springboot服务器 URL url = new URL(bgmUrl); File file = new File(filePath); FileUtils.copyURLToFile(url, file); client.delete().forPath(path); }else if(operatorType.equals(“2”)){ File file = new File(filePath); FileUtils.forceDelete(file); client.delete().forPath(path); } } } }); } ...

April 11, 2019 · 5 min · jiezi

SpringCloud之zuul

简介Zuul是所有从设备和web站点到Netflix流媒体应用程序后端的请求的前门。作为一个边缘服务应用程序,Zuul的构建是为了支持动态路由、监视、弹性和安全性。它还可以根据需要将请求路由到多个Amazon自动伸缩组。Zuul使用了一系列不同类型的过滤器,使我们能够快速灵活地将功能应用到edge服务中。这些过滤器帮助我们执行以下功能:身份验证和安全性——识别每个资源的身份验证需求并拒绝不满足这些需求的请求。洞察和监控——在边缘跟踪有意义的数据和统计数据,以便为我们提供准确的生产视图。动态路由——根据需要动态地将请求路由到不同的后端集群。压力测试——逐步增加集群的流量,以评估性能。减少负载——为每种类型的请求分配容量,并删除超过限制的请求。静态响应处理——直接在边缘构建一些响应,而不是将它们转发到内部集群多区域弹性——跨AWS区域路由请求,以使我们的ELB使用多样化,并使我们的优势更接近我们的成员工作原理在高级视图中,Zuul 2.0是一个Netty服务器,它运行预过滤器(入站过滤器),然后使用Netty客户机代理请求,然后在运行后过滤器(出站过滤器)后返回响应。过滤器是Zuul业务逻辑的核心所在。它们能够执行非常大范围的操作,并且可以在请求-响应生命周期的不同部分运行,如上图所示。Inbound Filters在路由到源之前执行,可以用于身份验证、路由和装饰请求。Endpoint Filters 可用于返回静态响应,否则内置的ProxyEndpoint过滤器将把请求路由到源。Outbound Filters 在从源获取响应后执行,可用于度量、装饰用户响应或添加自定义头。还有两种类型的过滤器:同步和异步。因为我们是在一个事件循环上运行的,所以千万不要阻塞过滤器。如果要阻塞,可以在一个异步过滤器中阻塞,在一个单独的threadpool上阻塞——否则可以使用同步过滤器。实用过滤器DebugRequest——查找一个查询参数来为请求添加额外的调试日志Healthcheck -简单的静态端点过滤器,返回200,如果一切引导正确ZuulResponseFilter -添加信息头部提供额外的细节路由,请求执行,状态和错误原因GZipResponseFilter -可以启用gzip出站响应SurgicalDebugFilter ——可以将特定的请求路由到不同的主机进行调试使用技巧依赖: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>MyFilter 过滤器@Componentpublic class MyFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(MyFilter.class); /** * pre:路由之前 * routing:路由之时 * post: 路由之后 * error:发送错误调用 * @return / @Override public String filterType() { return “pre”; } /* * 过滤的顺序 * @return / @Override public int filterOrder() { return 0; } /* * 这里可以写逻辑判断,是否要过滤,本文true,永远过滤 * @return / @Override public boolean shouldFilter() { return true; } /* * 过滤器的具体逻辑。 * 可用很复杂,包括查sql,nosql去判断该请求到底有没有权限访问。 * @return * @throws ZuulException / @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); log.info(String.format("%s >>> %s", request.getMethod(), request.getRequestURL().toString())); Object accessToken = request.getParameter(“token”); if(accessToken == null) { log.warn(“token is empty”); ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); try { ctx.getResponse().getWriter().write(“token is empty”); }catch (Exception e){} return null; } log.info(“ok”); return null; }}application.yml配置路由转发eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/server: port: 8769spring: application: name: cloud-service-zuulzuul: routes: api-a: path: /api-a/* serviceId: cloud-service-ribbon api-b: path: /api-b/** serviceId: cloud-service-feign启用zuul@SpringBootApplication@EnableZuulProxy@EnableEurekaClient@EnableDiscoveryClientpublic class CloudServiceZuulApplication { public static void main(String[] args) { SpringApplication.run(CloudServiceZuulApplication.class, args); }}路由熔断/** * 路由熔断 */@Componentpublic class ProducerFallback implements FallbackProvider { private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class); //指定要处理的 service。 @Override public String getRoute() { return “spring-cloud-producer”; } @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { if (cause != null && cause.getCause() != null) { String reason = cause.getCause().getMessage(); logger.info(“Excption {}",reason); } return fallbackResponse(); } public ClientHttpResponse fallbackResponse() { return new ClientHttpResponse() { @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } @Override public int getRawStatusCode() throws IOException { return 200; } @Override public String getStatusText() throws IOException { return “OK”; } @Override public void close() { } @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream(“The service is unavailable.".getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; }}总结Zuul网关有自动转发机制,但其实Zuul还有更多的应用场景,比如:鉴权、流量转发、请求统计等等,这些功能都可以使用Zuul来实现。’更多资源:zuul重试参考博文:http://www.ityouknow.com/spri…最后如果对 Java、大数据感兴趣请长按二维码关注一波,我会努力带给你们价值。觉得对你哪怕有一丁点帮助的请帮忙点个赞或者转发哦。 ...

April 10, 2019 · 2 min · jiezi

开发人员常用框架文档整理及中文翻译

开发人员常用的框架文档及中文翻译,包含 Spring 系列文档(Spring, Spring Boot, Spring Cloud, Spring Security, Spring Session),日志(Apache Flume, Log4j2),Http Server(NGINX,Apache),Python,数据库(OpenTSDB,MySQL,PostgreSQL)等最新官方文档以及对应的中文翻译。Docs4devhttps://www.docs4dev.comFeatures丰富的文档资源支持多语言英文中文(beta)…同步更新官方最新版本的文档支持多版本的文档基于 Elasticsearch 的文档全文检索文档列表Spring名称文档版本语言Spring Boot Reference1.5.9.RELEASEEnglishSpring Boot 中文文档1.5.9.RELEASE中文Spring Boot Reference2.1.1.RELEASEEnglishSpring Boot 中文文档2.1.1.RELEASE中文Spring Cloud ReferenceEdgware.SR5EnglishSpring Cloud 中文文档Edgware.SR5中文Spring Cloud ReferenceFinchley.SR2EnglishSpring Cloud 中文文档Finchley.SR2中文Spring Cloud ReferenceGreenwich.RELEASEEnglishSpring Cloud 中文文档Greenwich.RELEASE中文Spring Batch Reference3.0.xEnglishSpring Batch 中文文档3.0.x中文Spring Batch Reference4.1.xEnglishSpring Batch 中文文档4.1.x中文Spring Session Reference1.3.4.RELEASEEnglishSpring Session 中文文档1.3.4.RELEASE中文Spring Session Reference2.1.2.RELEASEEnglishSpring Session 中文文档2.1.2.RELEASE中文Spring Security Reference4.2.10.RELEASEEnglishSpring Security 中文文档4.2.10.RELEASE中文Spring Security Reference5.1.2.RELEASEEnglishSpring Security 中文文档5.1.2.RELEASE中文Spring AMQP Reference1.7.11.RELEASEEnglishSpring AMQP 中文文档1.7.11.RELEASE中文Spring AMQP Reference2.1.2.RELEASEEnglishSpring AMQP 中文文档2.1.2.RELEASE中文Spring Framework Reference4.3.21.RELEASEEnglishSpring Framework 中文文档4.3.21.RELEASE中文Spring Framework Reference5.1.3.RELEASEEnglishSpring Framework 中文文档5.1.3.RELEASE中文Spring Data JDBC1.0.5.RELEASEEnglishSpring Data JDBC1.0.5.RELEASE中文Spring Data JPA1.11.18.RELEASEEnglishSpring Data JPA1.11.18.RELEASE中文Spring Data JPA2.0.13.RELEASEEnglishSpring Data JPA2.0.13.RELEASE中文Spring Data JPA2.1.5.RELEASEEnglishSpring Data JPA2.1.5.RELEASE中文Spring Data Redis1.8.18.RELEASEEnglishSpring Data Redis1.8.18.RELEASE中文Spring Data Redis2.1.5.RELEASEEnglishSpring Data Redis2.1.5.RELEASE中文Http Server名称文档版本语言NginxcurrentEnglishNginx 中文文档current中文Apache2.4EnglishApache 中文文档2.4中文Python名称文档版本语言Python2.7.15EnglishPython 中文文档2.7.15中文Python3.7.2rc1EnglishPython 中文文档3.7.2rc1中文Database名称文档版本语言MySql5.7EnglishMySql 中文文档5.7中文PostgreSQL Documentation10.7EnglishPostgreSQL 中文文档10.7中文PostgreSQL Documentation11.2EnglishPostgreSQL 中文文档11.2中文OpenTSDB2.3EnglishOpenTSDB 中文文档2.3中文Logging名称文档版本语言Log4j2 Manual2.xEnglishLog4j2 中文文档2.x中文Apache Flume User Guide1.9.0EnglishApache Flume 用户指南1.9.0中文Contributing文档翻译如果你希望翻译相关的文档,请 clone 项目到本地,翻译完成后提交 PR。目前文档的是按照以下结构进行分类的:docs4dev/文档名称/版本号/语言代码/具体文档目前只支持以下语言:zh: 中文en: 英文如果你希望翻译其它语言,请提 issue.文档纠错如果你发现某个文档和官方文档有出入,请提 issue 或是在网站中提交 Feedback。其它如果你有针对此网站好的建议或意见,也欢迎提 issue.Roadmap更多的文档和更多的文档版本支持…Screenshot ...

April 10, 2019 · 1 min · jiezi

SpringBoot 2.X Kotlin 系列之Reactive Mongodb 与 JPA

一、本节目标前两章主要讲了SpringBoot Kotlin的基本操作,这一章我们将学习使用Kotlin访问MongoDB,并通过JPA完成(Create,Read,Update,Delete)简单操作。这里有一个问题什么不选用MySQL数据库呢?答案是 Spring Data Reactive Repositories 目前支持 Mongo、Cassandra、Redis、Couchbase。不支持 MySQL,那究竟为啥呢?那就说明下 JDBC 和 Spring Data 的关系。Spring Data Reactive Repositories 突出点是 Reactive,即非阻塞的。区别如下:基于 JDBC 实现的 Spring Data,比如 Spring Data JPA 是阻塞的。原理是基于阻塞 IO 模型 消耗每个调用数据库的线程(Connection)。事务只能在一个 java.sql.Connection 使用,即一个事务一个操作。二、构建项目及配置本章不在讲解如何构建项目了,大家可以参考第一章。这里我们主要引入了mongodb-reactive框架,在pom文件加入下列内容即可。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId></dependency>如何jar包后我们需要配置一下MongoDB数据库,在application.properties文件中加入一下配置即可,密码和用户名需要替换自己的,不然会报错的。spring.data.mongodb.host=localhostspring.data.mongodb.port=27017spring.data.mongodb.password=student2018.Docker_spring.data.mongodb.database=studentspring.data.mongodb.username=student三、创建实体及具体实现3.1实体创建package io.intodream.kotlin03.entityimport org.springframework.data.annotation.Idimport org.springframework.data.mongodb.core.mapping.Document/** * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-25,18:05 /@Documentclass Student { @Id var id :String? = null var name :String? = null var age :Int? = 0 var gender :String? = null var sClass :String ?= null override fun toString(): String { return ObjectMapper().writeValueAsString(this) }}3.2Repositorypackage io.intodream.kotlin03.daoimport io.intodream.kotlin03.entity.Studentimport org.springframework.data.mongodb.repository.ReactiveMongoRepository/* * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-25,18:04 /interface StudentRepository : ReactiveMongoRepository<Student, String>{}3.3Servicepackage io.intodream.kotlin03.serviceimport io.intodream.kotlin03.entity.Studentimport reactor.core.publisher.Fluximport reactor.core.publisher.Mono/* * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-25,18:04 /interface StudentService { /* * 通过学生编号获取学生信息 / fun find(id : String): Mono<Student> /* * 查找所有学生信息 / fun list(): Flux<Student> /* * 创建一个学生信息 / fun create(student: Student): Mono<Student> /* * 通过学生编号删除学生信息 / fun delete(id: String): Mono<Void>}// 接口实现类package io.intodream.kotlin03.service.implimport io.intodream.kotlin03.dao.StudentRepositoryimport io.intodream.kotlin03.entity.Studentimport io.intodream.kotlin03.service.StudentServiceimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.stereotype.Serviceimport reactor.core.publisher.Fluximport reactor.core.publisher.Mono/* * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-25,18:23 /@Serviceclass StudentServiceImpl : StudentService{ @Autowired lateinit var studentRepository: StudentRepository override fun find(id: String): Mono<Student> { return studentRepository.findById(id) } override fun list(): Flux<Student> { return studentRepository.findAll() } override fun create(student: Student): Mono<Student> { return studentRepository.save(student) } override fun delete(id: String): Mono<Void> { return studentRepository.deleteById(id) }}3.4 Controller实现package io.intodream.kotlin03.webimport io.intodream.kotlin03.entity.Studentimport io.intodream.kotlin03.service.StudentServiceimport org.slf4j.LoggerFactoryimport org.springframework.beans.factory.annotation.Autowiredimport org.springframework.web.bind.annotation.import reactor.core.publisher.Fluximport reactor.core.publisher.Mono/ * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-25,18:03 /@RestController@RequestMapping("/api/student")class StudentController { @Autowired lateinit var studentService: StudentService val logger = LoggerFactory.getLogger(this.javaClass) /* * 保存或新增学生信息 / @PostMapping("/") fun create(@RequestBody student: Student): Mono<Student> { logger.info("【保存学生信息】请求参数:{}", student) return studentService.create(student) } /* * 更新学生信息 / @PutMapping("/") fun update(@RequestBody student: Student): Mono<Student> { logger.info("【更新学生信息】请求参数:{}", student) return studentService.create(student) } /* * 查找所有学生信息 / @GetMapping("/list") fun listStudent(): Flux<Student> { return studentService.list() } /* * 通过学生编号查找学生信息 / @GetMapping("/id") fun student(@RequestParam id : String): Mono<Student> { logger.info(“查询学生编号:{}”, id) return studentService.find(id) } /* * 通过学生编号删除学生信息 */ @DeleteMapping("/") fun delete(@RequestParam id: String): Mono<Void> { logger.info(“删除学生编号:{}”, id) return studentService.delete(id) }}四、接口测试这里我们使用Postman来对接口进行测试,关于Postman这里接不用做过多的介绍了,不懂可以自行百度。控制台打印如下:2019-03-25 18:57:04.333 INFO 2705 — [ctor-http-nio-3] i.i.kotlin03.web.StudentController : 【保存学生信息】请求参数:{“id”:“1”,“name”:“Tom”,“age”:18,“gender”:“Boy”,“sclass”:“First class”}我们看一下数据库是否存储了其他接口测试情况如果大家觉得文章有用麻烦点一下赞,有问题的地方欢迎大家指出来。 ...

April 10, 2019 · 2 min · jiezi

SpringBoot Kotlin 系列之HTML与WebFlux

上一章我们提到过Mono 与 Flux,对于具体的介绍没说到,这一章我在这里简单介绍一下,既然提到Mono和Flux,那肯定得提到什么是响应式编程,什么是WebFlux。一、什么是响应式编程对于关于什么是响应编程,网上的说也很多,这里简单一句话介绍:响应式编程是基于异步和事件驱动的非阻塞程序,只是垂直通过在 JVM 内启动少量线程扩展,而不是水平通过集群扩展。二、Mono 与 FluxMono 和 Flux Reactor 是提供的两种响应式APIMono:实现发布者,并返回 0 或 1 个元素Flux:实现发布者,并返回 N 个元素三、什么是Spring WebfluxSpring Boot Webflux 就是基于 Reactor 实现的。Spring Boot 2.0 包括一个新的 spring-webflux 模块。该模块包含对响应式 HTTP 和 WebSocket 客户端的支持,以及对 REST,HTML 和 WebSocket 交互等程序的支持。一般来说,Spring MVC 用于同步处理,Spring Webflux 用于异步处理。Spring Boot Webflux 有两种编程模型实现,一种类似 Spring MVC 注解方式,另一种是使用其功能性端点方式。注解的会在第二篇文章讲到,下面快速入门用 Spring Webflux 功能性方式实现。在Spring官方有介绍,如图所示:四、Thymeleaf渲染HTML这里就不演示如何创建项目了,大家参考第一章,我们需要引入Thymeleaf框架,在pom文件中添加如下内容即可:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>引入Thymeleaf后我们需要做一些简单的配置,在application.properties文件中直接粘贴即可。主要是包括常用的编码、是否开启缓存等等。spring.thymeleaf.cache=truespring.thymeleaf.check-template=truespring.thymeleaf.check-template-location=truespring.thymeleaf.enabled=truespring.thymeleaf.encoding=UTF-8spring.thymeleaf.mode=HTML5spring.thymeleaf.prefix=classpath:/templates/spring.thymeleaf.servlet.content-type=text/htmlspring.thymeleaf.suffix=.html编写HTML,把文件放在resources/templates下<!DOCTYPE html><html lang=“zh-CN” xmlns:th=“http://www.w3.org/1999/xhtml"><head> <meta charset=“UTF-8”> <title>Title</title></head><body><h1>Hello <span th:text="${name}"></span></h1><h1>Now time <span th:text="${time}"></span></h1></body></html>编写Controllerpackage io.intodream.kotlin02.webimport org.springframework.stereotype.Controllerimport org.springframework.ui.Modelimport org.springframework.web.bind.annotation.GetMappingimport org.springframework.web.bind.annotation.RequestMappingimport reactor.core.publisher.Monoimport java.time.LocalDateTime/** * @description * * @author Jwenk * @copyright intoDream.io 筑梦科技 * @email xmsjgzs@163.com * @date 2019-03-24,18:24 */@RequestMapping("/webflux”)@Controllerclass IndexController { @GetMapping("/index") fun index(model : Model): Mono<String> { model.addAttribute(“name”, “Tom”) model.addAttribute(“time”, LocalDateTime.now()) return Mono.create{ monoSink -> monoSink.success(“index”)} }}启动项目,访问路径http://localhost:8080/webflux/index看到图片里面的内容则说明编写成功了,在Controller里面可以直接返回String,而不是Mono<String> ,但是 Mono 代表着我这个返回 View 也是回调的。 ...

April 10, 2019 · 1 min · jiezi

SpringBoot+MySQL+MyBatis的入门教程

本博客 猫叔的博客,转载请申明出处本系列教程为HMStrange项目附带。历史文章如何在VMware12安装Centos7.6最新版Centos7.6安装Java8Centos7.6安装MySQL+Redis(最新版)教程内容备注:本系列开发工具均为IDEA1、构建项目,选择Lombok、Web、MySQL、MyBatis四个基本的Maven依赖。大家可以看看pom文件<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.myself.mybatis</groupId> <artifactId>datademo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>datademo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>2、准备MySQL,这里可以参考历史文章的安装MySQL环节,我新建了一个数据库,针对这个项目,构建了一张简单的表。DDLCREATE TABLE t_msg ( id int(11) NOT NULL, message varchar(255) DEFAULT NULL COMMENT ‘信息’, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;3、构建项目目录,我构建了一个经典的web项目目录结构,entity实体类、mapper映射、service接口、impl接口实现、controller业务访问、resources/mapper包用于存放xml4、填写application.yml,默认生成不是yml,不过我觉得yml视觉效果好一些,就改了一下,我们需要填写数据库信息,还有mybatis的数据库映射地址,实体类地址spring: datasource: url: jdbc:mysql://192.168.192.133:3306/datademo?characterEncoding=utf-8&useSSL=false username: root password: password driver-class-name: com.mysql.cj.jdbc.Drivermybatis: mapper-locations: classpath*:mapper/Mapper.xml type-aliases-package: com.myself.mybatis.entity5、构建数据库对应的实体类TMsg,这个类放在entitypackage com.myself.mybatis.entity;import lombok.Data;import java.io.Serializable;/* * Created by MySelf on 2019/4/9. /@Datapublic class TMsg implements Serializable { private Integer id; private String message;}6、构建对应的Mapper接口(其实就类似dao层),这里与TMsgMapper.xml文件对应关系package com.myself.mybatis.mapper;import com.myself.mybatis.entity.TMsg;import org.apache.ibatis.annotations.Mapper;/* * Created by MySelf on 2019/4/9. /@Mapperpublic interface TMsgMapper { public TMsg findById(Integer id);}<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace=“com.myself.mybatis.mapper.TMsgMapper”> <select id=“findById” resultType=“com.myself.mybatis.entity.TMsg”> SELECT id,message from t_msg WHERE id = #{id} </select></mapper>我这边就单纯一个方法,大家可以扩展自己的方法。7、service层与其实现,这个比较简单,一般做过web项目的都了解package com.myself.mybatis.service;import com.myself.mybatis.entity.TMsg;/* * Created by MySelf on 2019/4/9. /public interface TMsgService { public TMsg findById(Integer id);}package com.myself.mybatis.service.impl;import com.myself.mybatis.entity.TMsg;import com.myself.mybatis.mapper.TMsgMapper;import com.myself.mybatis.service.TMsgService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;/* * Created by MySelf on 2019/4/9. /@Servicepublic class TMsgServiceImpl implements TMsgService { @Autowired private TMsgMapper tMsgMapper; @Override public TMsg findById(Integer id) { return tMsgMapper.findById(id); }}8、controller层,我这边构建了一个get方法,通过id获取信息。package com.myself.mybatis.controller;import com.myself.mybatis.entity.TMsg;import com.myself.mybatis.service.TMsgService;import org.apache.ibatis.annotations.Param;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/* * Created by MySelf on 2019/4/9. */@RestController@RequestMapping("/msg”)public class TMsgController { @Autowired private TMsgService tMsgService; @GetMapping("/getMsg”) public String getMsg(@Param(“id”) Integer id){ TMsg tMsg = tMsgService.findById(id); return tMsg.getMessage(); }}9、启动项目,并使用Postman测试10、项目下载地址欢迎到HMStrange项目进行下载:https://github.com/UncleCatMy…公众号:Java猫说学习交流群:728698035现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。 ...

April 10, 2019 · 2 min · jiezi

SpringBoot中异步请求和异步调用(看这一篇就够了)

<font color=gray> 原创不易,如需转载,请注明出处https://www.cnblogs.com/baixianlong/p/10661591.html,否则将追究法律责任!!! </font>一、SpringBoot中异步请求的使用1、异步请求与同步请求特点:可以先释放容器分配给请求的线程与相关资源,减轻系统负担,释放了容器所分配线程的请求,其响应将被延后,可以在耗时处理完成(例如长时间的运算)时再对客户端进行响应。<font color=red>一句话:增加了服务器对客户端请求的吞吐量</font>(实际生产上我们用的比较少,如果并发请求量很大的情况下,我们会通过nginx把请求负载到集群服务的各个节点上来分摊请求压力,当然还可以通过消息队列来做请求的缓冲)。2、异步请求的实现方式一:Servlet方式实现异步请求 @RequestMapping(value = “/email/servletReq”, method = GET) public void servletReq (HttpServletRequest request, HttpServletResponse response) { AsyncContext asyncContext = request.startAsync(); //设置监听器:可设置其开始、完成、异常、超时等事件的回调处理 asyncContext.addListener(new AsyncListener() { @Override public void onTimeout(AsyncEvent event) throws IOException { System.out.println(“超时了…”); //做一些超时后的相关操作… } @Override public void onStartAsync(AsyncEvent event) throws IOException { System.out.println(“线程开始”); } @Override public void onError(AsyncEvent event) throws IOException { System.out.println(“发生错误:"+event.getThrowable()); } @Override public void onComplete(AsyncEvent event) throws IOException { System.out.println(“执行完成”); //这里可以做一些清理资源的操作… } }); //设置超时时间 asyncContext.setTimeout(20000); asyncContext.start(new Runnable() { @Override public void run() { try { Thread.sleep(10000); System.out.println(“内部线程:” + Thread.currentThread().getName()); asyncContext.getResponse().setCharacterEncoding(“utf-8”); asyncContext.getResponse().setContentType(“text/html;charset=UTF-8”); asyncContext.getResponse().getWriter().println(“这是异步的请求返回”); } catch (Exception e) { System.out.println(“异常:"+e); } //异步请求完成通知 //此时整个请求才完成 asyncContext.complete(); } }); //此时之类 request的线程连接已经释放了 System.out.println(“主线程:” + Thread.currentThread().getName()); }方式二:使用很简单,直接返回的参数包裹一层callable即可,可以继承WebMvcConfigurerAdapter类来设置默认线程池和超时处理 @RequestMapping(value = “/email/callableReq”, method = GET) @ResponseBody public Callable<String> callableReq () { System.out.println(“外部线程:” + Thread.currentThread().getName()); return new Callable<String>() { @Override public String call() throws Exception { Thread.sleep(10000); System.out.println(“内部线程:” + Thread.currentThread().getName()); return “callable!”; } }; } @Configuration public class RequestAsyncPoolConfig extends WebMvcConfigurerAdapter { @Resource private ThreadPoolTaskExecutor myThreadPoolTaskExecutor; @Override public void configureAsyncSupport(final AsyncSupportConfigurer configurer) { //处理 callable超时 configurer.setDefaultTimeout(601000); configurer.setTaskExecutor(myThreadPoolTaskExecutor); configurer.registerCallableInterceptors(timeoutCallableProcessingInterceptor()); } @Bean public TimeoutCallableProcessingInterceptor timeoutCallableProcessingInterceptor() { return new TimeoutCallableProcessingInterceptor(); }}方式三:和方式二差不多,在Callable外包一层,给WebAsyncTask设置一个超时回调,即可实现超时处理 @RequestMapping(value = “/email/webAsyncReq”, method = GET) @ResponseBody public WebAsyncTask<String> webAsyncReq () { System.out.println(“外部线程:” + Thread.currentThread().getName()); Callable<String> result = () -> { System.out.println(“内部线程开始:” + Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(4); } catch (Exception e) { // TODO: handle exception } logger.info(“副线程返回”); System.out.println(“内部线程返回:” + Thread.currentThread().getName()); return “success”; }; WebAsyncTask<String> wat = new WebAsyncTask<String>(3000L, result); wat.onTimeout(new Callable<String>() { @Override public String call() throws Exception { // TODO Auto-generated method stub return “超时”; } }); return wat; }方式四:DeferredResult可以处理一些相对复杂一些的业务逻辑,最主要还是可以在另一个线程里面进行业务处理及返回,即可在两个完全不相干的线程间的通信。@RequestMapping(value = “/email/deferredResultReq”, method = GET) @ResponseBody public DeferredResult<String> deferredResultReq () { System.out.println(“外部线程:” + Thread.currentThread().getName()); //设置超时时间 DeferredResult<String> result = new DeferredResult<String>(601000L); //处理超时事件 采用委托机制 result.onTimeout(new Runnable() { @Override public void run() { System.out.println(“DeferredResult超时”); result.setResult(“超时了!”); } }); result.onCompletion(new Runnable() { @Override public void run() { //完成后 System.out.println(“调用完成”); } }); myThreadPoolTaskExecutor.execute(new Runnable() { @Override public void run() { //处理业务逻辑 System.out.println(“内部线程:” + Thread.currentThread().getName()); //返回结果 result.setResult(“DeferredResult!!”); } }); return result; }二、SpringBoot中异步调用的使用1、介绍异步请求的处理。除了异步请求,一般上我们用的比较多的应该是异步调用。通常在开发过程中,会遇到一个方法是和实际业务无关的,没有紧密性的。比如记录日志信息等业务。这个时候正常就是启一个新线程去做一些业务处理,让主线程异步的执行其他业务。2、使用方式(基于spring下)需要在启动类加入@EnableAsync使异步调用@Async注解生效在需要异步执行的方法上加入此注解即可@Async(“threadPool”),threadPool为自定义线程池代码略。。。就俩标签,自己试一把就可以了3、注意事项在默认情况下,未设置TaskExecutor时,默认是使用SimpleAsyncTaskExecutor这个线程池,但此线程不是真正意义上的线程池,因为线程不重用,每次调用都会创建一个新的线程。可通过控制台日志输出可以看出,每次输出线程名都是递增的。所以最好我们来自定义一个线程池。调用的异步方法,不能为同一个类的方法(包括同一个类的内部类),简单来说,因为Spring在启动扫描时会为其创建一个代理类,而同类调用时,还是调用本身的代理类的,所以和平常调用是一样的。其他的注解如@Cache等也是一样的道理,说白了,就是Spring的代理机制造成的。所以在开发中,最好把异步服务单独抽出一个类来管理。下面会重点讲述。。4、什么情况下会导致@Async异步方法会失效?<font color=red>调用同一个类下注有@Async异步方法</font>:在spring中像@Async和@Transactional、cache等注解本质使用的是动态代理,其实Spring容器在初始化的时候Spring容器会将含有AOP注解的类对象“替换”为代理对象(简单这么理解),那么注解失效的原因就很明显了,就是因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器,那么解决方法也会沿着这个思路来解决。<font color=red>调用的是静态(static )方法</font><font color=red>调用(private)私有化方法</font>5、解决4中问题1的方式(其它2,3两个问题自己注意下就可以了)<font color=red>将要异步执行的方法单独抽取成一个类</font>,原理就是当你把执行异步的方法单独抽取成一个类的时候,这个类肯定是被Spring管理的,其他Spring组件需要调用的时候肯定会注入进去,这时候实际上注入进去的就是代理类了。其实我们的注入对象都是从Spring容器中给当前Spring组件进行成员变量的赋值,由于某些类使用了AOP注解,那么实际上在Spring容器中实际存在的是它的代理对象。那么我们就可以<font color=red>通过上下文获取自己的代理对象调用异步方法</font>。@Controller@RequestMapping("/app”)public class EmailController { //获取ApplicationContext对象方式有多种,这种最简单,其它的大家自行了解一下 @Autowired private ApplicationContext applicationContext; @RequestMapping(value = “/email/asyncCall”, method = GET) @ResponseBody public Map<String, Object> asyncCall () { Map<String, Object> resMap = new HashMap<String, Object>(); try{ //这样调用同类下的异步方法是不起作用的 //this.testAsyncTask(); //通过上下文获取自己的代理对象调用异步方法 EmailController emailController = (EmailController)applicationContext.getBean(EmailController.class); emailController.testAsyncTask(); resMap.put(“code”,200); }catch (Exception e) { resMap.put(“code”,400); logger.error(“error!",e); } return resMap; } //注意一定是public,且是非static方法 @Async public void testAsyncTask() throws InterruptedException { Thread.sleep(10000); System.out.println(“异步任务执行完成!”); }}<font color=red>开启cglib代理,手动获取Spring代理类</font>,从而调用同类下的异步方法。首先,在启动类上加上<font color=red>@EnableAspectJAutoProxy(exposeProxy = true)</font>注解。代码实现,如下:@Service@Transactional(value = “transactionManager”, readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = Throwable.class)public class EmailService {@Autowiredprivate ApplicationContext applicationContext;@Asyncpublic void testSyncTask() throws InterruptedException { Thread.sleep(10000); System.out.println(“异步任务执行完成!”);}public void asyncCallTwo() throws InterruptedException { //this.testSyncTask();// EmailService emailService = (EmailService)applicationContext.getBean(EmailService.class);// emailService.testSyncTask(); boolean isAop = AopUtils.isAopProxy(EmailController.class);//是否是代理对象; boolean isCglib = AopUtils.isCglibProxy(EmailController.class); //是否是CGLIB方式的代理对象; boolean isJdk = AopUtils.isJdkDynamicProxy(EmailController.class); //是否是JDK动态代理方式的代理对象; //以下才是重点!!! EmailService emailService = (EmailService)applicationContext.getBean(EmailService.class); EmailService proxy = (EmailService) AopContext.currentProxy(); System.out.println(emailService == proxy ? true : false); proxy.testSyncTask(); System.out.println(“end!!!”);}}三、异步请求与异步调用的区别两者的使用场景不同,异步请求用来解决并发请求对服务器造成的压力,从而提高对请求的吞吐量;而异步调用是用来做一些非主线流程且不需要实时计算和响应的任务,比如同步日志到kafka中做日志分析等。异步请求是会一直等待response相应的,需要返回结果给客户端的;而异步调用我们往往会马上返回给客户端响应,完成这次整个的请求,至于异步调用的任务后台自己慢慢跑就行,客户端不会关心。四、总结异步请求和异步调用的使用到这里基本就差不多了,有问题还希望大家多多指出。这边文章提到了动态代理,而spring中Aop的实现原理就是动态代理,后续会对动态代理做详细解读,还望多多支持哈~个人博客地址:csdn:https://blog.csdn.net/tiantuo6513 cnblogs:https://www.cnblogs.com/baixianlongsegmentfault:https://segmentfault.com/u/baixianlong github:https://github.com/xianlongbai ...

April 8, 2019 · 3 min · jiezi

SpringBoot中并发定时任务的实现、动态定时任务的实现(看这一篇就够了)

原创不易,如需转载,请注明出处https://www.cnblogs.com/baixianlong/p/10659045.html,否则将追究法律责任!!!一、在JAVA开发领域,目前可以通过以下几种方式进行定时任务1、单机部署模式Timer:jdk中自带的一个定时调度类,可以简单的实现按某一频度进行任务执行。提供的功能比较单一,无法实现复杂的调度任务。ScheduledExecutorService:也是jdk自带的一个基于线程池设计的定时任务类。其每个调度任务都会分配到线程池中的一个线程执行,所以其任务是并发执行的,互不影响。Spring Task:Spring提供的一个任务调度工具,支持注解和配置文件形式,支持Cron表达式,使用简单但功能强大。Quartz:一款功能强大的任务调度器,可以实现较为复杂的调度功能,如每月一号执行、每天凌晨执行、每周五执行等等,还支持分布式调度,就是配置稍显复杂。2、分布式集群模式(不多介绍,简单提一下)问题:I、如何解决定时任务的多次执行?II、如何解决任务的单点问题,实现任务的故障转移?问题I的简单思考:1、固定执行定时任务的机器(可以有效避免多次执行的情况 ,缺点就是单点故障问题)。2、借助Redis的过期机制和分布式锁。3、借助mysql的锁机制等。成熟的解决方案:1、Quartz:可以去看看这篇文章Quartz分布式。2、elastic-job:(https://github.com/elasticjob/elastic-job-lite)当当开发的弹性分布式任务调度系统,采用zookeeper实现分布式协调,实现任务高可用以及分片。3、xxl-job:(https://github.com/xuxueli/xxl-job)是大众点评员发布的分布式任务调度平台,是一个轻量级分布式任务调度框架。4、saturn:(https://github.com/vipshop/Saturn) 是唯品会提供一个分布式、容错和高可用的作业调度服务框架。二、SpringTask实现定时任务(这里是基于springboot)1、简单的定时任务实现使用方式:使用@EnableScheduling注解开启对定时任务的支持。使用@Scheduled 注解即可,基于corn、fixedRate、fixedDelay等一些定时策略来实现定时任务。使用缺点:1、多个定时任务使用的是同一个调度线程,所以任务是阻塞执行的,执行效率不高。2、其次如果出现任务阻塞,导致一些场景的定时计算没有实际意义,比如每天12点的一个计算任务被阻塞到1点去执行,会导致结果并非我们想要的。使用优点:1、配置简单2、适用于单个后台线程执行周期任务,并且保证顺序一致执行的场景 源码分析://默认使用的调度器 if(this.taskScheduler == null) { this.localExecutor = Executors.newSingleThreadScheduledExecutor(); this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);}//可以看到SingleThreadScheduledExecutor指定的核心线程为1,说白了就是单线程执行public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1));}//利用了DelayedWorkQueue延时队列作为任务的存放队列,这样便可以实现任务延迟执行或者定时执行public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());} 2、实现并发的定时任务使用方式:方式一:由1中我们知道之所以定时任务是阻塞执行,是配置的线程池决定的,那就好办了,换一个不就行了!直接上代码:@Configurationpublic class ScheduledConfig implements SchedulingConfigurer { @Autowired private TaskScheduler myThreadPoolTaskScheduler; @Override public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) { //简单粗暴的方式直接指定 //scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); //也可以自定义的线程池,方便线程的使用与维护,这里不多说了 scheduledTaskRegistrar.setTaskScheduler(myThreadPoolTaskScheduler); }}@Bean(name = “myThreadPoolTaskScheduler”)public TaskScheduler getMyThreadPoolTaskScheduler() { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setPoolSize(10); taskScheduler.setThreadNamePrefix(“Haina-Scheduled-”); taskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //调度器shutdown被调用时等待当前被调度的任务完成 taskScheduler.setWaitForTasksToCompleteOnShutdown(true); //等待时长 taskScheduler.setAwaitTerminationSeconds(60); return taskScheduler;} 方式二:方式一的本质改变了任务调度器默认使用的线程池,接下来这种是不改变调度器的默认线程池,而是把当前任务交给一个异步线程池去执行首先使用@EnableAsync 启用异步任务然后在定时任务的方法加上@Async即可,默认使用的线程池为SimpleAsyncTaskExecutor(该线程池默认来一个任务创建一个线程,就会不断创建大量线程,极有可能压爆服务器内存。当然它有自己的限流机制,这里就不多说了,有兴趣的自己翻翻源码~)项目中为了更好的控制线程的使用,我们可以自定义我们自己的线程池,使用方式@Async(“myThreadPool”)废话太多,直接上代码: @Scheduled(fixedRate = 100010,initialDelay = 100020) @Async(“myThreadPoolTaskExecutor”) //@Async public void scheduledTest02(){ System.out.println(Thread.currentThread().getName()+"—>xxxxx—>"+Thread.currentThread().getId()); } //自定义线程池 @Bean(name = “myThreadPoolTaskExecutor”) public TaskExecutor getMyThreadPoolTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(20); taskExecutor.setMaxPoolSize(200); taskExecutor.setQueueCapacity(25); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix(“Haina-ThreadPool-”); // 线程池对拒绝任务(无线程可用)的处理策略,目前只支持AbortPolicy、CallerRunsPolicy;默认为后者 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //调度器shutdown被调用时等待当前被调度的任务完成 taskExecutor.setWaitForTasksToCompleteOnShutdown(true); //等待时长 taskExecutor.setAwaitTerminationSeconds(60); taskExecutor.initialize(); return taskExecutor; }线程池的使用心得(后续有专门文章来探讨)java中提供了ThreadPoolExecutor和ScheduledThreadPoolExecutor,对应与spring中的ThreadPoolTaskExecutor和ThreadPoolTaskScheduler,但是在原有的基础上增加了新的特性,在spring环境下更容易使用和控制。使用自定义的线程池能够避免一些默认线程池造成的内存溢出、阻塞等等问题,更贴合自己的服务特性使用自定义的线程池便于对项目中线程的管理、维护以及监控。即便在非spring环境下也不要使用java默认提供的那几种线程池,坑很多,阿里代码规约不说了吗,得相信大厂!!!三、动态定时任务的实现问题:使用@Scheduled注解来完成设置定时任务,但是有时候我们往往需要对周期性的时间的设置会做一些改变,或者要动态的启停一个定时任务,那么这个时候使用此注解就不太方便了,原因在于这个注解中配置的cron表达式必须是常量,那么当我们修改定时参数的时候,就需要停止服务,重新部署。解决办法:方式一:实现SchedulingConfigurer接口,重写configureTasks方法,重新制定Trigger,核心方法就是addTriggerTask(Runnable task, Trigger trigger) ,不过需要注意的是,此种方式修改了配置值后,需要在下一次调度结束后,才会更新调度器,并不会在修改配置值时实时更新,实时更新需要在修改配置值时额外增加相关逻辑处理。@Configurationpublic class ScheduledConfig implements SchedulingConfigurer {@Autowiredprivate TaskScheduler myThreadPoolTaskScheduler;@Overridepublic void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) { //scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); scheduledTaskRegistrar.setTaskScheduler(myThreadPoolTaskScheduler); //可以实现动态调整定时任务的执行频率 scheduledTaskRegistrar.addTriggerTask( //1.添加任务内容(Runnable) () -> System.out.println(“cccccccccccccccc—>” + Thread.currentThread().getId()), //2.设置执行周期(Trigger) triggerContext -> { //2.1 从数据库动态获取执行周期 String cron = “0/2 * * * * ? “; //2.2 合法性校验.// if (StringUtils.isEmpty(cron)) {// // Omitted Code ..// } //2.3 返回执行周期(Date) return new CronTrigger(cron).nextExecutionTime(triggerContext); } );}}方式二:使用threadPoolTaskScheduler类可实现动态添加删除功能,当然也可实现执行频率的调整首先,我们要认识下这个调度类,它其实是对java中ScheduledThreadPoolExecutor的一个封装改进后的产物,主要改进有以下几点: 1、提供默认配置,因为是ScheduledThreadPoolExecutor,所以只有poolSize这一个默认参数。 2、支持自定义任务,通过传入Trigger参数。 3、对任务出错处理进行优化,如果是重复性的任务,不抛出异常,通过日志记录下来,不影响下次运行,如果是只执行一次的任务,将异常往上抛。顺便说下ThreadPoolTaskExecutor相对于ThreadPoolExecutor的改进点: 1、提供默认配置,原生的ThreadPoolExecutor的除了ThreadFactory和RejectedExecutionHandler其他没有默认配置 2、实现AsyncListenableTaskExecutor接口,支持对FutureTask添加success和fail的回调,任务成功或失败的时候回执行对应回调方法。 3、因为是spring的工具类,所以抛出的RejectedExecutionException也会被转换为spring框架的TaskRejectedException异常(这个无所谓) 4、提供默认ThreadFactory实现,直接通过参数重载配置扯了这么多,还是直接上代码:@Componentpublic class DynamicTimedTask { private static final Logger logger = LoggerFactory.getLogger(DynamicTimedTask.class); //利用创建好的调度类统一管理 //@Autowired //@Qualifier(“myThreadPoolTaskScheduler”) //private ThreadPoolTaskScheduler myThreadPoolTaskScheduler; //接受任务的返回结果 private ScheduledFuture<?> future; @Autowired private ThreadPoolTaskScheduler threadPoolTaskScheduler; //实例化一个线程池任务调度类,可以使用自定义的ThreadPoolTaskScheduler @Bean public ThreadPoolTaskScheduler threadPoolTaskScheduler() { ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler(); return new ThreadPoolTaskScheduler(); } /** * 启动定时任务 * @return / public boolean startCron() { boolean flag = false; //从数据库动态获取执行周期 String cron = “0/2 * * * * ? “; future = threadPoolTaskScheduler.schedule(new CheckModelFile(),cron); if (future!=null){ flag = true; logger.info(“定时check训练模型文件,任务启动成功!!!”); }else { logger.info(“定时check训练模型文件,任务启动失败!!!”); } return flag; } /* * 停止定时任务 * @return */ public boolean stopCron() { boolean flag = false; if (future != null) { boolean cancel = future.cancel(true); if (cancel){ flag = true; logger.info(“定时check训练模型文件,任务停止成功!!!”); }else { logger.info(“定时check训练模型文件,任务停止失败!!!”); } }else { flag = true; logger.info(“定时check训练模型文件,任务已经停止!!!”); } return flag; } class CheckModelFile implements Runnable{ @Override public void run() { //编写你自己的业务逻辑 System.out.print(“模型文件检查完毕!!!”) } } }四、总结到此基于springtask下的定时任务的简单使用算是差不多了,其中不免有些错误的地方,或者理解有偏颇的地方欢迎大家提出来!基于分布式集群下的定时任务使用,后续有时间再继续!!!个人博客地址:csdn:https://blog.csdn.net/tiantuo6513 cnblogs:https://www.cnblogs.com/baixianlongsegmentfault:https://segmentfault.com/u/baixianlong github:https://github.com/xianlongbai ...

April 8, 2019 · 2 min · jiezi

干货|一个案例学会Spring Security 中使用 JWT

在前后端分离的项目中,登录策略也有不少,不过 JWT 算是目前比较流行的一种解决方案了,本文就和大家来分享一下如何将 Spring Security 和 JWT 结合在一起使用,进而实现前后端分离时的登录解决方案。1 无状态登录1.1 什么是有状态?有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如Tomcat中的Session。例如登录:用户登录后,我们把用户的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session,然后下次请求,用户携带cookie值来(这一步有浏览器自动完成),我们就能识别到对应session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:服务端保存大量数据,增加服务端压力服务端保存用户状态,不支持集群化部署1.2 什么是无状态微服务集群中的每个服务,对外提供的都使用RESTful风格的接口。而RESTful风格的一个最重要的规范就是:服务的无状态性,即:服务端不保存任何客户端请求者信息客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份那么这种无状态性有哪些好处呢?客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器服务端的集群和状态对客户端透明服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)减小服务端存储压力1.3.如何实现无状态无状态登录的流程:首先客户端发送账户名/密码到服务端进行认证认证通过后,服务端将用户信息加密并且编码成一个token,返回给客户端以后客户端每次发送请求,都需要携带认证的token服务端对客户端发送来的token进行解密,判断是否有效,并且获取用户登录信息1.4 JWT1.4.1 简介JWT,全称是Json Web Token, 是一种JSON风格的轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权: JWT 作为一种规范,并没有和某一种语言绑定在一起,常用的Java 实现是GitHub 上的开源项目 jjwt,地址如下:https://github.com/jwtk/jjwt1.4.2 JWT数据格式JWT包含三部分数据:Header:头部,通常头部有两部分信息:声明类型,这里是JWT加密算法,自定义我们会对头部进行Base64Url编码(可解码),得到第一部分数据。Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:iss (issuer):表示签发人exp (expiration time):表示token过期时间sub (subject):主题aud (audience):受众nbf (Not Before):生效时间iat (Issued At):签发时间jti (JWT ID):编号这部分也会采用Base64Url编码,得到第二部分数据。Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过Header中配置的加密算法生成。用于验证整个数据完整和可靠性。生成的数据格式如下图: 注意,这里的数据通过 . 隔开成了三部分,分别对应前面提到的三部分,另外,这里数据是不换行的,图片换行只是为了展示方便而已。1.4.3 JWT交互流程流程图:步骤翻译:应用程序或客户端向授权服务器请求授权获取到授权后,授权服务器会向应用程序返回访问令牌应用程序使用访问令牌来访问受保护资源(如API)因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了RESTful的无状态规范。1.5 JWT 存在的问题说了这么多,JWT 也不是天衣无缝,由客户端维护登录状态带来的一些问题在这里依然存在,举例如下:续签问题,这是被很多人诟病的问题之一,传统的cookie+session的方案天然的支持续签,但是jwt由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入redis,虽然可以解决问题,但是jwt也变得不伦不类了。注销问题,由于服务端不再保存用户信息,所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的未过期的token就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。密码重置,密码重置后,原本的token依然可以访问系统,这时候也需要强制修改secret。基于第2点和第3点,一般建议不同用户取不同secret。2 实战说了这么久,接下来我们就来看看这个东西到底要怎么用?2.1 环境搭建首先我们来创建一个Spring Boot项目,创建时需要添加Spring Security依赖,创建完成后,添加 jjwt 依赖,完整的pom.xml文件如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version></dependency>然后在项目中创建一个简单的 User 对象实现 UserDetails 接口,如下:public class User implements UserDetails { private String username; private String password; private List<GrantedAuthority> authorities; public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } //省略getter/setter}这个就是我们的用户对象,先放着备用,再创建一个HelloController,内容如下:@RestControllerpublic class HelloController { @GetMapping("/hello") public String hello() { return “hello jwt !”; } @GetMapping("/admin") public String admin() { return “hello admin !”; }}HelloController 很简单,这里有两个接口,设计是 /hello 接口可以被具有 user 角色的用户访问,而 /admin 接口则可以被具有 admin 角色的用户访问。2.2 JWT 过滤器配置接下来提供两个和 JWT 相关的过滤器配置:一个是用户登录的过滤器,在用户的登录的过滤器中校验用户是否登录成功,如果登录成功,则生成一个token返回给客户端,登录失败则给前端一个登录失败的提示。第二个过滤器则是当其他请求发送来,校验token的过滤器,如果校验成功,就让请求继续执行。这两个过滤器,我们分别来看,先看第一个:public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl)); setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException { User user = new ObjectMapper().readValue(req.getInputStream(), User.class); return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); } @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException { Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities(); StringBuffer as = new StringBuffer(); for (GrantedAuthority authority : authorities) { as.append(authority.getAuthority()) .append(","); } String jwt = Jwts.builder() .claim(“authorities”, as)//配置用户角色 .setSubject(authResult.getName()) .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) .signWith(SignatureAlgorithm.HS512,“sang@123”) .compact(); resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(jwt)); out.flush(); out.close(); } protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException { resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(“登录失败!”); out.flush(); out.close(); }}关于这个类,我说如下几点:自定义 JwtLoginFilter 继承自 AbstractAuthenticationProcessingFilter,并实现其中的三个默认方法。attemptAuthentication方法中,我们从登录参数中提取出用户名密码,然后调用AuthenticationManager.authenticate()方法去进行自动校验。第二步如果校验成功,就会来到successfulAuthentication回调中,在successfulAuthentication方法中,将用户角色遍历然后用一个 , 连接起来,然后再利用Jwts去生成token,按照代码的顺序,生成过程一共配置了四个参数,分别是用户角色、主题、过期时间以及加密算法和密钥,然后将生成的token写出到客户端。第二步如果校验失败就会来到unsuccessfulAuthentication方法中,在这个方法中返回一个错误提示给客户端即可。再来看第二个token校验的过滤器:public class JwtFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; String jwtToken = req.getHeader(“authorization”); System.out.println(jwtToken); Claims claims = Jwts.parser().setSigningKey(“sang@123”).parseClaimsJws(jwtToken.replace(“Bearer”,"")) .getBody(); String username = claims.getSubject();//获取当前登录用户名 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get(“authorities”)); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(token); filterChain.doFilter(req,servletResponse); }}关于这个过滤器,我说如下几点:首先从请求头中提取出 authorization 字段,这个字段对应的value就是用户的token。将提取出来的token字符串转换为一个Claims对象,再从Claims对象中提取出当前用户名和用户角色,创建一个UsernamePasswordAuthenticationToken放到当前的Context中,然后执行过滤链使请求继续执行下去。如此之后,两个和JWT相关的过滤器就算配置好了。2.3 Spring Security 配置接下来我们来配置 Spring Security,如下:@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser(“admin”) .password(“123”).roles(“admin”) .and() .withUser(“sang”) .password(“456”) .roles(“user”); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/hello").hasRole(“user”) .antMatchers("/admin").hasRole(“admin”) .antMatchers(HttpMethod.POST, “/login”).permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class) .csrf().disable(); }}简单起见,这里我并未对密码进行加密,因此配置了NoOpPasswordEncoder的实例。简单起见,这里并未连接数据库,我直接在内存中配置了两个用户,两个用户具备不同的角色。配置路径规则时, /hello 接口必须要具备 user 角色才能访问, /admin 接口必须要具备 admin 角色才能访问,POST 请求并且是 /login 接口则可以直接通过,其他接口必须认证后才能访问。最后配置上两个自定义的过滤器并且关闭掉csrf保护。2.4 测试做完这些之后,我们的环境就算完全搭建起来了,接下来启动项目然后在 POSTMAN 中进行测试,如下: 登录成功后返回的字符串就是经过 base64url 转码的token,一共有三部分,通过一个 . 隔开,我们可以对第一个 . 之前的字符串进行解码,即Header,如下: 再对两个 . 之间的字符解码,即 payload: 可以看到,我们设置信息,由于base64并不是加密方案,只是一种编码方案,因此,不建议将敏感的用户信息放到token中。 接下来再去访问 /hello 接口,注意认证方式选择 Bearer Token,Token值为刚刚获取到的值,如下: 可以看到,访问成功。总结这就是 JWT 结合 Spring Security 的一个简单用法,讲真,如果实例允许,类似的需求我还是推荐使用 OAuth2 中的 password 模式。 不知道大伙有没有看懂呢?如果没看懂,松哥还有一个关于这个知识点的视频教程,如下: 如何获取这个视频教程呢?很简单,将本文转发到一个超过100人的微信群中(QQ群不算,松哥是群主的微信群也不算,群要为Java方向),或者多个微信群中,只要累计人数达到100人即可,然后加松哥微信,截图发给松哥即可获取资料。 ...

April 8, 2019 · 2 min · jiezi

SpringBoot 仿抖音短视频小程序开发(二)

一、注册和登录功能这两个功能比较简单,就不详细记录了,需要注意的是【密码】出于安全考虑用了@JsonIgnore,这样返回的VO的json数据给前端的时候就忽略掉。public class UsersVO { private String id; private String username; private String userToken; @JsonIgnore private String password; }二、上传个人头像并展示1. 微信小程序前端只需要把图片的临时路径传给后端接口,后台再保存到本地和数据库前端:后台接口:@PostMapping("/uploadFace") public IMoocJSONResult uploadFace(String userId, @RequestParam(“file”) MultipartFile[] files) throws Exception { if(StringUtils.isBlank(userId)){ return IMoocJSONResult.errorMsg(“用户Id不能为空”); } //文件保存的命名空间 String fileSpace = FILE_SPACE; //保存到数据库的相对路径 String uploadPathDB = “/” + userId +"/face"; FileOutputStream fileOutputStream = null; InputStream inputStream = null; try { if(files !=null && files.length>0){ String fileName = files[0].getOriginalFilename(); if(StringUtils.isNoneBlank(fileName)){ //文件上传的最终保存路径 String finalFacePath = fileSpace + uploadPathDB + “/” +fileName; //设置数据库保存的路径 uploadPathDB += ("/" + fileName); File outFile = new File(finalFacePath); if(outFile.getParentFile() !=null || !outFile.getParentFile().isDirectory()){ //创建父文件夹 outFile.getParentFile().mkdirs(); } fileOutputStream = new FileOutputStream(outFile); inputStream = files[0].getInputStream(); IOUtils.copy(inputStream,fileOutputStream); } }else{ IMoocJSONResult.errorMsg(“上传出错”); } } catch (IOException e) { e.printStackTrace(); IMoocJSONResult.errorMsg(“上传出错”); }finally { if(fileOutputStream!=null){ fileOutputStream.flush(); fileOutputStream.close(); } } Users user = new Users(); user.setId(userId); user.setFaceImage(uploadPathDB); userService.updateUserInfo(user); return IMoocJSONResult.ok(uploadPathDB); }2. 静态资源配置,显示图片三、上传视频、选择BGM(可选)上传视频和上传头像的实现是差不多的,多了截图、与BGM合并的操作 //将视频和BGM合并 MergeVideoMp3 tool = new MergeVideoMp3(FFMPEG_EXE); String videoInputPath = finalVideoPath; String videoOutputName = UUID.randomUUID().toString() + “.mp4”; uploadPathDB = “/” + userId + “/video/” + videoOutputName; finalVideoPath = FILE_SPACE + uploadPathDB; tool.convertor(videoInputPath,mp3InputPath,videoSeconds,finalVideoPath); 。。。。。 //对视频进行截图 FetchVideoCover videoCover = new FetchVideoCover(FFMPEG_EXE); videoCover.getCover(finalVideoPath, FILE_SPACE + coverPathDB);三、首页展示视频列表,分页========================前端=============================== 后端分页查询 PageHelper.startPage(page, pageSize); List<VideosVO> list = videosMapper.queryAllVideos(desc, userId); PageInfo<VideosVO> pageList = new PageInfo<>(list); PagedResult pagedResult = new PagedResult(); pagedResult.setPage(page); pagedResult.setTotal(pageList.getPages()); pagedResult.setRows(list); pagedResult.setRecords(pageList.getTotal()); return pagedResult; 前端成功回调函数,对视频列表进行拼接 var videoList = res.data.data.rows; var newVideoList = me.data.videoList; me.setData({ videoList: newVideoList.concat(videoList), page: page, totalPage: res.data.data.total, serverUrl: serverUrl }); 上拉刷新,即下一页 onReachBottom: function(){ var me = this; var currentPage = me.data.page; var totalPage = me.data.totalPage; //判断当前页数和总页数是否相等,如果相等则无需查询 if(currentPage == totalPage){ wx.showToast({ title: ‘已经没有视频啦!!’, icon: “none” }); return; } var page = currentPage + 1; me.getAllVideoList(page,0); } 下拉刷新回到第一页 onPullDownRefresh: function(){ wx.showNavigationBarLoading(); this.getAllVideoList(1,0); },四、点击播放视频点击视频触发事件,跳转页面 showVideoInfo: function(e){ console.log(e); var me = this; var videoList = me.data.videoList; var arrIndex = e.target.dataset.arrindex; console.log(arrIndex); var videoInfo = JSON.stringify(videoList[arrIndex]); wx.redirectTo({ url: ‘../videoinfo/videoinfo?videoInfo=’ + videoInfo }) }五、点赞和关注视频点赞和取消 @Override @Transactional(propagation = Propagation.REQUIRED) public void userLikeVideo(String userId, String videoId, String videoCreaterId) { //1. 保存用户和视频的喜欢点赞关联关系表 String likeId = sid.nextShort(); UsersLikeVideos ulv = new UsersLikeVideos(); ulv.setId(likeId); ulv.setUserId(userId); ulv.setVideoId(videoId); usersLikeVideosMapper.insert(ulv); //2. 视频喜欢数量累加 videosMapper.addVideoLikeCount(videoId); //3. 用户受喜欢数量的累加 usersMapper.addReceiveLikeCount(userId); } @Override @Transactional(propagation = Propagation.REQUIRED) public void userUnLikeVideo(String userId, String videoId, String videoCreaterId) { //1. 删除用户和视频的喜欢点赞关联关系表 UsersLikeVideosExample example = new UsersLikeVideosExample(); example.createCriteria().andUserIdEqualTo(userId).andVideoIdEqualTo(videoId); usersLikeVideosMapper.deleteByExample(example); //2. 视频喜欢数量累减 videosMapper.reduceVideoLikeCount(videoId); //3. 用户受喜欢数量的累减 usersMapper.reduceReceiveLikeCount(userId); }关注用户和取消关注 @Override @Transactional(propagation = Propagation.REQUIRED) public void saveUserFanRelation(String userId, String fanId) { UsersFans userFan = new UsersFans(); String relId = sid.nextShort(); userFan.setId(relId); userFan.setUserId(userId); userFan.setFanId(fanId); usersFansMapper.insert(userFan); usersMapper.addFansCount(userId); usersMapper.addFollersCount(fanId); } @Override @Transactional(propagation = Propagation.REQUIRED) public void deleteUserFanRelation(String userId, String fanId) { UsersFansExample example = new UsersFansExample(); example.createCriteria().andFanIdEqualTo(fanId).andUserIdEqualTo(userId); usersFansMapper.deleteByExample(example); usersMapper.reduceFansCount(userId); usersMapper.reduceFollersCount(fanId); }六、视频热搜关键词使用了一个微信小程序搜索框组件:https://github.com/mindawei/w…前端页面加载事件:热搜词的查询语句: <select id=“getHotWords” resultType=“string”> SELECT content FROM search_records GROUP BY content ORDER BY COUNT(content) DESC </select>六、显示我点过赞的视频(即收藏)<!– 查询我喜欢的视频 –> <select id=“queryMyLikeVideos” resultMap=“BaseResultMap” parameterType=“String”> select v.,u.face_image as face_image,u.nickname as nickname from videos v left join vuser u on v.user_id = u.id where v.id in (select ulv.video_id from users_like_videos ulv where ulv.user_id = #{userId}) and v.status = 1 order by v.create_time desc </select>七、查询我关注的人发的视频 <!– 查询我关注的人发的视频 –> <select id=“queryMyFollowVideos” resultMap=“BaseResultMap” parameterType=“String”> select v.,u.face_image as face_image,u.nickname as nickname from videos v left join vuser u on v.user_id = u.id where v.user_id in (select uf.user_id from users_fans uf where uf.fan_id = #{userId}) and v.status = 1 order by v.create_time desc </select>八、举报 获取举报的内容然后发送请求:九、评论留言和回复 POJO设计:public class Comments { private String id; private String fatherCommentId; private String toUserId; private String videoId; private String fromUserId; //留言者,评论的用户id private Date createTime; private String comment; //评论内容}点击某条评论,回复: replyFocus:function(e){ var me = this; var fatherCommentId = e.currentTarget.dataset.fathercommentid; var toUserId = e.currentTarget.dataset.touserid; var toNickname = e.currentTarget.dataset.tonickname; me.setData({ placeholder: “回复 " + toNickname, replyToUserId: toUserId, replyFatherCommentId: fatherCommentId, commentFocus: true }); },前端确认留言,保存: ...

April 7, 2019 · 3 min · jiezi

第三课(spring-boot+mybatis+jqgrid)

课程目标完成与spring boot 与的mybatis的集成处理数据curd课程计划使用mybatis完成博客后台管理员列表的jqgird搜索课程分析想要完成列表的搜索,就必须对sql按提交搜索条件进行逻辑判断组织sql,也就是动态sql步骤1.加入依赖// build.gradledependencies { compile(“org.springframework.boot:spring-boot-starter-web”) compile(“org.springframework.boot:spring-boot-starter-thymeleaf”) compile(“org.springframework.boot:spring-boot-devtools”) // JPA Data (We are going to use Repositories, Entities, Hibernate, etc…) compile ‘org.springframework.boot:spring-boot-starter-data-jpa’ // Use MySQL Connector-J compile ‘mysql:mysql-connector-java’ // 使用mybatis compile(“org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.2”) developmentOnly(“org.springframework.boot:spring-boot-devtools”) testCompile(“junit:junit”)}2. 配置mybatisspring: jpa: hibernate: ddl-auto: update datasource: url: jdbc:mysql://localhost:3306/db_sptest?useSSL=false username: mysqluser password: mysqlpwd mvc: static-path-pattern: /static/**mybatis: type-aliases-package: hello.model3. 使用mybatis mapper// model/AdminMapper@Mapperpublic interface AdminMapper { //使用注解方式 @Select(“SELECT * FROM ADMIN WHERE name = #{name} LIMIT 1”) Admin findByName(String name); @Select(“SELECT * FROM ADMIN WHERE id = #{id}”) Admin findById(Integer id); //动态sql @SelectProvider(type = AdminService.class,method = “selectAdminLike”) List<Admin> findBySearch(Admin admin); //动态sql 返回行数 @SelectProvider(type = AdminService.class,method = “countAdminSearch”) @ResultType(Integer.class) int countBySearch(Admin admin);}4.编写动态sql语句// service/AdminServiceimport org.apache.ibatis.jdbc.SQL;public class AdminService { // With conditionals (note the final parameters, required for the anonymous inner class to access them) public String selectAdminLike(Admin admin) { return new SQL() {{ SELECT(“A.name,A.email,A.id”); FROM(“ADMIN A”); if (admin.getName() != null) { WHERE(“A.name = ‘” + admin.getName() + “’”); } if (admin.getEmail() != null) { WHERE(“A.email = " + admin.getEmail()); } }}.toString(); } public String countAdminSearch(Admin admin){ return new SQL() {{ SELECT(“count(*)”); FROM(“ADMIN A”); if (admin.getName() != null) { WHERE(“A.name = ‘” + admin.getName() + “’”); } if (admin.getEmail() != null) { WHERE(“A.email = " + admin.getEmail()); } }}.toString(); }}5.使用mybatis查询方法 // AdminController @GetMapping(path = “get_list”) @ResponseBody public DataResponse<Admin> getAdminList( Admin adminReq, @RequestParam(defaultValue = “1”, value = “page”) String page, @RequestParam(defaultValue = “10”, value = “rows”) String rows) { String total; //页数 List<Admin> admin_list; int records; records = adminMapper.countBySearch(adminReq); int pageSize = Integer.valueOf(rows); total = Integer.toString((records + pageSize - 1) / pageSize); DataResponse<Admin> response = new DataResponse<>(); admin_list = adminMapper.findBySearch(adminReq); response.setTotal(total); response.setRows(admin_list); response.setPage(page); response.setRecords(records); return response; }课程成果完成使用jqgrid+spring boot+mybatis 的数据列表搜索遗留page 和 pageSize 的传参控制,下节课对代码进行稍微的改动就可以支持目前使用了jpa的Hibernate entity 这么用合理么? ...

April 7, 2019 · 2 min · jiezi

SpringCloud之Hystrix

简介在分布式环境中,许多服务依赖关系中的一些必然会失败。Hystrix是一个库,它通过添加延迟容忍和容错逻辑来帮助您控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点、停止跨服务的级联故障并提供回退选项来实现这一点,所有这些选项都提高了系统的总体弹性。目标Hystrix的设计目的如下:为通过第三方客户端库访问的依赖项(通常通过网络)提供保护和控制延迟和故障。停止复杂分布式系统中的级联故障。故障快速恢复。在可能的情况下,后退并优雅地降级。启用近实时监视、警报和操作控制。背景为了解决什么问题?复杂分布式体系结构中的应用程序有几十个依赖项,每个依赖项在某个时候都不可避免地会失败。如果主机应用程序没有从这些外部故障中隔离出来,那么它就有可能与这些外部故障一起宕机。例如,对于一个依赖于30个服务的应用程序,其中每个服务都有99.99%的正常运行时间,您可以这样期望:99.9930 = 99.7% uptime0.3% of 1 billion requests = 3,000,000 failures2+ hours downtime/month even if all dependencies have excellent uptime.现实通常更糟。即使当所有依赖项都运行良好时,即使0.01%的停机时间对几十个服务中的每个服务的总体影响也相当于一个月潜在的停机时间(如果您不为恢复而设计整个系统)。如下面的图演变:当一切正常时,请求流可以是这样的:当许多后端系统之一成为潜在,它可以阻止整个用户请求:对于高流量,一个后端依赖项成为潜在,可能会导致所有服务器上的所有资源在几秒钟内饱和。应用程序中通过网络或客户机库到达可能导致网络请求的每个点都是潜在故障的来源。比故障更糟的是,这些应用程序还可能导致服务之间的延迟增加,从而备份队列、线程和其他系统资源,从而导致系统中出现更多级联故障。工作原理工作流程图:1. 构造一个HystrixCommand或HystrixObservableCommand对象第一步是构造一个HystrixCommand或HystrixObservableCommand对象来表示对依赖项的请求。将请求发出时需要的任何参数传递给构造函数。如果期望依赖项返回单个响应,则构造一个HystrixCommand对象。例如:HystrixCommand command = new HystrixCommand(arg1, arg2);如果期望依赖项返回发出响应的可观察对象,则构造一个HystrixObservableCommand对象。例如:HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);2.执行命令有四种方法可以执行命令,使用以下四种方法之一的Hystrix命令对象(前两种方法只适用于简单的HystrixCommand对象,不适用于HystrixObservableCommand):execute()) — blocks, then returns the single response received from the dependency (or throws an exception in case of an error)queue()) — returns a Future with which you can obtain the single response from the dependencyobserve()) — subscribes to the Observable that represents the response(s) from the dependency and returns an Observable that replicates that source ObservabletoObservable()) — returns an Observable that, when you subscribe to it, will execute the Hystrix command and emit its responses3.是否缓存了响应如果为该命令启用了请求缓存,并且在缓存中可用对请求的响应,则此缓存的响应将立即以可观察到的形式返回。4. 电路打开了吗?当您执行该命令时,Hystrix将与断路器一起检查电路是否打开。如果电路打开(或“跳闸”),那么Hystrix将不执行命令,而是将流路由到(8)获取回退。如果电路被关闭,则流继续到(5),检查是否有可用的容量来运行命令。5.线程池/队列/信号量是否已满?如果与该命令关联的线程池和队列(或信号量,如果不在线程中运行)已满,那么Hystrix将不执行该命令,而是立即将流路由到(8)获取回退。6.HystrixObservableCommand.construct()或HystrixCommand.run ()这里,Hystrix通过为此目的编写的方法调用对依赖项的请求,方法如下:HystrixCommand.run()) — returns a single response or throws an exceptionHystrixObservableCommand.construct()) — returns an Observable that emits the response(s) or sends an onError notification如果run()或construct()方法超过了命令的超时值,线程将抛出一个TimeoutException(如果命令本身不在自己的线程中运行,则单独的计时器线程将抛出一个TimeoutException)。在这种情况下,Hystrix将响应路由到8。获取回退,如果最终返回值run()或construct()方法没有取消/中断,那么它将丢弃该方法。请注意,没有办法强制潜在线程停止工作——Hystrix在JVM上能做的最好的事情就是抛出InterruptedException。如果由Hystrix包装的工作不尊重interruptedexception,那么Hystrix线程池中的线程将继续它的工作,尽管客户机已经收到了TimeoutException。这种行为可能会使Hystrix线程池饱和,尽管负载“正确释放”。大多数Java HTTP客户端库不解释interruptedexception。因此,请确保正确配置HTTP客户机上的连接和读/写超时。如果该命令没有抛出任何异常并返回一个响应,那么Hystrix将在执行一些日志记录和度量报告之后返回此响应。在run()的情况下,Hystrix返回一个可观察的对象,该对象发出单个响应,然后发出一个onCompleted通知;在construct()的情况下,Hystrix返回由construct()返回的相同的可观察值。7.计算电路健康Hystrix向断路器报告成功、失败、拒绝和超时,断路器维护一组滚动计数器,用于计算统计数据。它使用这些统计数据来确定电路应该在什么时候“跳闸”,在这一点上,它会短路任何后续的请求,直到恢复期结束,在此期间,它会在第一次检查某些健康检查之后再次关闭电路。8.回退Hystrix试图恢复你的回滚命令执行失败时:当一个异常的构造()或()运行(6),当命令电路短路,因为打开(4),当命令的线程池和队列或信号能力(5),或者当命令已超过其超时长度。详情参考官网:https://github.com/Netflix/Hy…9. 返回成功的响应如果Hystrix命令成功,它将以可观察到的形式返回响应或响应给调用者。根据您如何调用上面步骤2中的命令,这个可观察对象可能在返回给您之前进行转换:execute() — 以与.queue()相同的方式获取一个Future,然后在这个Future上调用get()来获取可观察对象发出的单个值.queue() — 将可观察对象转换为BlockingObservable,以便将其转换为未来,然后返回此未来observe() — 立即订阅可观察对象,并开始执行命令的流;返回一个可观察对象,当您订阅该对象时,将重播排放和通知toObservable() — 返回可观察值不变;您必须订阅它,才能真正开始执行命令的流程更多原理可以移步官网https://github.com/Netflix/Hy…使用加入依赖<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>在ribbon中使用使用@EnableHystrix开启@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClient@EnableHystrixpublic class CloudServiceRibbonApplication { public static void main(String[] args) { SpringApplication.run(CloudServiceRibbonApplication.class, args); } @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); }}该注解对该方法创建了熔断器的功能,并指定了fallbackMethod熔断方法,熔断方法直接返回了一个字符串,字符串为"hi,"+name+",sorry,error!"@Servicepublic class TestService { @Autowired RestTemplate restTemplate; @HystrixCommand(fallbackMethod = “hiError”) public String hiService(String name) { return restTemplate.getForObject(“http://CLOUD-EUREKA-CLIENT/hi?name="+name,String.class); } public String hiError(String name) { return “hi,"+name+",sorry,error!”; }}在Feign中使用feign.hystrix.enabled: true 开启hystrixeureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/server: port: 8765spring: application: name: cloud-service-feignfeign.hystrix.enabled: true@EnableFeignClients启动@SpringBootApplication@EnableEurekaClient@EnableDiscoveryClient@EnableFeignClientspublic class CloudServiceFeginApplication { public static void main(String[] args) { SpringApplication.run(CloudServiceFeginApplication.class, args); }}fallback:配置连接失败等错误的返回类@FeignClient(value = “cloud-eureka-client”,fallback = TestServiceHystric.class)public interface TestService { @RequestMapping(value = “/hi”,method = RequestMethod.GET) String sayHiFromClientOne(@RequestParam(value = “name”) String name);}当访问接口有问题时,直接调用此接口返回。@Componentpublic class TestServiceHystric implements TestService{ @Override public String sayHiFromClientOne(String name) { return “sorry “+name; }}更多使用技巧可参考官网:https://github.com/Netflix/Hy…总结在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。熔断器的原理很简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。更多优质文章:http://www.ityouknow.com/spri…https://www.fangzhipeng.com/s...http://blog.didispace.com/tag…最后如果对 Java、大数据感兴趣请长按二维码关注一波,我会努力带给你们价值。觉得对你哪怕有一丁点帮助的请帮忙点个赞或者转发哦。 ...

April 7, 2019 · 2 min · jiezi

springboot 集成 shiro 导致事务无效

问题描述前两天测试一个写事务,发现这个事务出现异常不会回滚了,一直在事务上找问题,一直没有找到,结果发现是shiro的bean先于Spring事务将userService实例化了,结果导致spring事务初始化时好无法扫描到该bean,导致这个bean上没有绑定事务,导致事务无效寻找问题在哪一、在事务本身找问题通过百度发现,大家都有以下几个原因导致事务失效数据库的引擎是否是innoDB 启动类上是否加入@EnableTransactionManagement注解 方法是否为public是否是因为抛出了Exception等checked异常经过排查,发现以上原因都通过了,那么应该不是写的问题。二、在运行中找问题在上面4个原因检查时,发现将已有的service 类 copy下现在有两个除了名字其他都一模一样的类,这时运行下发现,在原来的类中@Transational失效,在新copy中的类中@Transational就起效了,这个问题好莫名奇妙,什么都没改就一个有效一个无效,现在的思路就是比较下这两个类在运行时有什么不同通过log发现打出了一下信息,说是jdbc的connection 不是Spring管的而正常回归的service类则是,调用了 JtaTransactionManager 类,而且 spring是管理jdbc的connection的通过这个分析,可以知道这spring对于这两个类的处理是不一样的,应该是spring代理或者初始化的问题,翻了下log 发现service 在ProxyTransactionManagementConfiguration 配置之前就被创建了,那应该是这里的问题了,这里就要分析下service为啥提前被创建了,发现在开始启动的是shiro ,而shiro中有个realm中引用了这些服务,所以这些服务在Transaction创建扫描之前创建了引发问题原因总结导致问题的真正原因是bean创建顺序问题,解决问题方法就是,在Transaction之后创建service。ps:呵呵,但是我还是不知道咋样才能解决创建顺序问题,继续百度之,关键词shiro 导致 事务不生效果然有解决方案解决方案经过百度找到了以下的解决方法,和以下解释shiro导致springboot事务不起效解决办法BeanPostProcessor加载次序及其对Bean造成的影响分析spring boot shiro 事务无效Shrio 多realms集成:No realms have been configured! One or more realms must be presentspring + shiro 配置中部分事务失效分析及解决方案(这个方案不管用)解决方法一:在realm引用的service服务上加@lazy注解,但是这个方法我测试了下并没有起效!!!解决方法二:把在 ShiroConfig里面初始化的Realm的bean和securityManager的bean方法移动到一个新建的ShiroComponent中,利用监听器中去初始化,主要配置如下,其中ShiroComponent中UserNamePassWordRealm和WeiXinRealm是我自定义的两个Realm,换成自己的就好,ShiroConfig.javaimport java.util.LinkedHashMap;import java.util.Map;import javax.servlet.DispatcherType;import javax.servlet.Filter;import org.apache.shiro.cache.ehcache.EhCacheManager;import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.spring.LifecycleBeanPostProcessor;import org.apache.shiro.spring.web.ShiroFilterFactoryBean;import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.filter.DelegatingFilterProxy;/** * 自定义继承shiro 没有使用shiro-spring-boot-web-starter 的shiro 套件 * * @author gaoxiuya * /@Configurationpublic class ShiroConfig { /* * FilterRegistrationBean * * @return / @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistration = new FilterRegistrationBean(); filterRegistration.setFilter(new DelegatingFilterProxy(“shiroFilter”)); filterRegistration.setEnabled(true); filterRegistration.addUrlPatterns("/"); filterRegistration.setDispatcherTypes(DispatcherType.REQUEST); return filterRegistration; } /** * @param securityManager * @see org.apache.shiro.spring.web.ShiroFilterFactorupload.visit.pathyBean * @return / @Bean(name = “shiroFilter”) public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); bean.setLoginUrl("/"); bean.setSuccessUrl("/index"); bean.setUnauthorizedUrl("/403"); Map<String, Filter> filters = new LinkedHashMap<>(); filters.put(“permsc”, new CustomPermissionsAuthorizationFilter()); bean.setFilters(filters); Map<String, String> chains = new LinkedHashMap<>(); chains.put("/favicon.ico", “anon”); bean.setFilterChainDefinitionMap(chains); return bean; } @Bean public EhCacheManager cacheManager() { EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile(“classpath:ehcache.xml”); return cacheManager; } /* * @see DefaultWebSessionManager * @return */ @Bean(name = “sessionManager”) public DefaultWebSessionManager defaultWebSessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setCacheManager(cacheManager()); sessionManager.setGlobalSessionTimeout(1800000); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionValidationSchedulerEnabled(true); sessionManager.setDeleteInvalidSessions(true); return sessionManager; } @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); }}ShiroComponent.javaimport java.util.ArrayList;import java.util.List;import org.apache.shiro.authc.Authenticator;import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;import org.apache.shiro.authc.pam.ModularRealmAuthenticator;import org.apache.shiro.cache.CacheManager;import org.apache.shiro.realm.Realm;import org.apache.shiro.session.mgt.SessionManager;import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.Bean;import org.springframework.context.event.ContextRefreshedEvent;import org.springframework.context.event.EventListener;import org.springframework.stereotype.Component;@Componentpublic class ShiroComponent { @Bean public Realm userNamePassWordRealm(CacheManager cacheManager) { UserNamePassWordRealm userNamePassWordRealm = new UserNamePassWordRealm(); userNamePassWordRealm.setCacheManager(cacheManager); return userNamePassWordRealm; } @Bean public Realm myWeiXinRealm(CacheManager cacheManager) { WeiXinRealm weiXinRealm = new WeiXinRealm(); weiXinRealm.setCacheManager(cacheManager); return weiXinRealm; } @Bean(name = “securityManager”) public DefaultWebSecurityManager securityManager(Authenticator modularRealmAuthenticator, CacheManager cacheManager, SessionManager defaultWebSessionManager) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setAuthenticator(modularRealmAuthenticator); manager.setCacheManager(cacheManager); manager.setSessionManager(defaultWebSessionManager); return manager; } @Bean public Authenticator modularRealmAuthenticator() { ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator(); modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy()); return modularRealmAuthenticator; } @EventListener public void handleContextRefresh(ContextRefreshedEvent event) { ApplicationContext context = event.getApplicationContext(); DefaultWebSecurityManager manager = (DefaultWebSecurityManager) context.getBean(“securityManager”); Realm userNamePassWordRealm = (Realm) context.getBean(“userNamePassWordRealm”); Realm myWeiXinRealm = (Realm) context.getBean(“myWeiXinRealm”); ModularRealmAuthenticator modularRealmAuthenticator = (ModularRealmAuthenticator) context .getBean(“modularRealmAuthenticator”); List<Realm> realms = new ArrayList<>(); realms.add(userNamePassWordRealm); realms.add(myWeiXinRealm); modularRealmAuthenticator.setRealms(realms); manager.setAuthenticator(modularRealmAuthenticator); manager.setRealms(realms); }}总结以后需要补课的地方spring bean 初始化顺序spring 事务原理spring bean 预加载 BeanPostProces 原理@Lazy 原理和为啥不起效 ...

April 7, 2019 · 2 min · jiezi

【极简版】SpringBoot+SpringData JPA 管理系统

前言只有光头才能变强。文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y在上一篇中已经讲解了如何从零搭建一个SpringBoot+SpringData JPA的环境,测试接口的时候也成功获取得到数据了。带你搭一个SpringBoot+SpringData JPA的Demo我的目的是做一个十分简易的管理系统,这就得有页面,下面我继续来讲讲我是怎么快速搭一个管理系统的。ps:由于是简易版,我的目的是能够快速搭建,而不在于代码的规范性。(所以在后面你可能会看到很多丑陋的代码)一、搭建管理系统1.1. 搭建页面在上一篇的最后,我们可以通过http://localhost:8887/user接口拿到我们User表所有的记录了。我们现在希望把记录塞到一个管理页面上(展示起来)。作为一个后端,我HTML+CSS实在是丑陋,于是我就去找了一份BootStrap的模板。首先,我进到bootStrap的官网,找到基本模板这一块:我们在里边可以看到挺多的模板的,这里选择一个控制台页面:于是,就把这份模板下载下来,在本地中运行起来试试看。官方给出的链接是下载整一份文档,我们找到想要的页面即可:于是我们将这两份文件单独粘贴在我们的项目中,发现这HTML文件需要bootstrap.css、bootstrap.js、jquery 的依赖(原来用的是相对路径,其实我们就是看看相对路径的文件在我们这有没有,如果没有,那就是我们需要的)。这里我们在CDN中找找,导入链接就行了。于是我们就将所缺的依赖替换成BootCDN的依赖,最重要的几个依赖如下:<link href=“https://cdn.bootcss.com/twitter-bootstrap/3.4.0/css/bootstrap.min.css" rel=“stylesheet”><script src=“https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script><script src=“https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>如无意外的话,我们也能在项目中正常打开页面。1.1.2 把数据塞到页面上把数据塞到页面上,有两种方案:要么就后端返回json给前端进行解析,要么就使用模板引擎。而我为了便捷,是不想写JS代码的。所以,我使用freemarker这个模板引擎。为什么这么多模板引擎,我选择这个?因为我只会这个!在SpringBoot下使用freemarker也是非常简单,首先,我们需要加入pom文件依赖:<!–freemarker–><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId></dependency>随后,在application.yml文件中,加入freemarker的配置: # freemarker配置 freemarker: suffix: .ftl request-context-attribute: request expose-session-attributes: true content-type: text/html check-template-location: true charset: UTF-8 cache: false template-loader-path: classpath:/templates这里我简单解释一下:freemarker的文件后缀名为.ftl,程序从/templates路径下加载我们的文件。于是乎,我将本来是.html的文件修改成.ftl文件,并放在templates目录下:接下来将我们Controller得到的数据,塞到Model对象中: /** * 得到所有用户 / @GetMapping(value = “/user”, produces = {“application/json;charset=UTF-8”}) public String getAllUser ( Model model) { List<User> allUser = userService.getAllUser(); model.addAttribute(“users”, allUser); return “/index”; }图片如下:在ftl文件中,我们只要判断数据是否存在,如果存在则在表格中遍历出数据就行了: <#if users?? && (users?size > 0)> <#list users as user> <tr> <td>${user.userId}</td> <td>${user.userNickname}</td> <td>${user.userEmail}</td> <td>${user.actiState}</td> <td><a href=“http://localhost:8887/deleteUser?id=${user.userId}">删除</a></td> </tr> </#list> <#else> <h3>还没有任何用户</h3> </#if>图片如下:删除的Controller代码如下:/* * 根据ID删除某个用户 */@GetMapping(value = “/deleteUser”, produces = {“application/json;charset=UTF-8”})public String deleteUserById (String id,Model model) { userService.deleteUserById(id); return getAllUser(model);}我们再找几张自己喜欢的图片,简单删除一些不必要模块,替换成我们想要的文字,就可以得到以下的效果了:至于图片上的评论管理、备忘录管理的做法都如上,我只是把文件再复制一次而已(期中没有写任何的JS代码,懒)。在编写的期中,要值得注意的是:静态的文件一般我们会放在static文件夹中。项目的目录结构如下:最后本文涉及到的链接(bootstrap & cdn):https://v3.bootcss.com/getting-started/#templatehttps://www.bootcdn.cn/all/乐于输出干货的Java技术公众号:Java3y。公众号内有200多篇原创技术文章、海量视频资源、精美脑图,不妨来关注一下!觉得我的文章写得不错,不妨点一下赞! ...

April 6, 2019 · 1 min · jiezi

Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器

标题文字 #### 概要之前的两篇文章,讲述了Spring Security 结合 OAuth2 、JWT 的使用,这一节要求对 OAuth2、JWT 有了解,若不清楚,先移步到下面两篇提前了解下。Spring Boot Security 整合 OAuth2 设计安全API接口服务Spring Boot Security 整合 JWT 实现 无状态的分布式API接口这一篇我们来实现 支持 JWT令牌 的授权服务器。优点使用 OAuth2 是向认证服务器申请令牌,客户端拿这令牌访问资源服务服务器,资源服务器校验了令牌无误后,如果资源的访问用到用户的相关信息,那么资源服务器还需要根据令牌关联查询用户的信息。使用 JWT 是客户端通过用户名、密码 请求服务器获取 JWT,服务器判断用户名和密码无误之后,可以将用户信息和权限信息经过加密成 JWT 的形式返回给客户端。在之后的请求中,客户端携带 JWT 请求需要访问的资源,如果资源的访问用到用户的相关信息,那么就直接从JWT中获取到。所以,如果我们在使用 OAuth2 时结合JWT ,就能节省集中式令牌校验开销,实现无状态授权认证。快速上手项目说明工程名端口作用jwt-authserver8080授权服务器jwt-resourceserver8081资源服务器授权服务器pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>WebSecurityConfig@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http. authorizeRequests().antMatchers("/").permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser(“user”).password(“123456”).roles(“USER”); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return Objects.equals(charSequence.toString(),s); } }; }}为了方便,使用内存模式,在内存中创建一个用户 user 密码 123456。OAuth2AuthorizationServer/ * 授权服务器 /@Configuration@EnableAuthorizationServerpublic class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter { /* * 注入AuthenticationManager ,密码模式用到 / @Autowired private AuthenticationManager authenticationManager; /* * 对Jwt签名时,增加一个密钥 * JwtAccessTokenConverter:对Jwt来进行编码以及解码的类 / @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(“test-secret”); return converter; } /* * 设置token 由Jwt产生,不使用默认的透明令牌 / @Bean public JwtTokenStore jwtTokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .tokenStore(jwtTokenStore()) .accessTokenConverter(accessTokenConverter()); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(“clientapp”) .secret(“123”) .scopes(“read”) //设置支持[密码模式、授权码模式、token刷新] .authorizedGrantTypes( “password”, “authorization_code”, “refresh_token”); }}资源服务器pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>HelloController@RestController("/api")public class HelloController { @PostMapping("/api/hi") public String say(String name) { return “hi , " + name; }}OAuth2ResourceServer/* * 资源服务器 */@Configuration@EnableResourceServerpublic class OAuth2ResourceServer extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated().and() .requestMatchers().antMatchers("/api/**”); }}application.ymlserver: port: 8081security: oauth2: resource: jwt: key-value: test-secret参数说明:security.oauth2.resource.jwt.key-value:设置签名key 保持和授权服务器一致。security.oauth2.resource.jwt:项目启动过程中,检查到配置文件中有security.oauth2.resource.jwt 的配置,就会生成 jwtTokenStore 的 bean,对令牌的校验就会使用 jwtTokenStore 。验证请求令牌curl -X POST –user ‘clientapp:123’ -d ‘grant_type=password&username=user&password=123456’ http://localhost:8080/oauth/token返回JWT令牌{ “access_token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ0MzExMDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiOGM0YWMyOTYtMDQwYS00Y2UzLTg5MTAtMWJmNjZkYTQwOTk3IiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsicmVhZCJdfQ.YAaSRN0iftmlR6Khz9UxNNEpHHn8zhZwlQrCUCPUmsU”, “token_type”: “bearer”, “refresh_token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsicmVhZCJdLCJhdGkiOiI4YzRhYzI5Ni0wNDBhLTRjZTMtODkxMC0xYmY2NmRhNDA5OTciLCJleHAiOjE1NTY5Nzk5MDgsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI0ZjA5M2ZjYS04NmM0LTQxZWUtODcxZS1kZTY2ZjFhOTI0NTAiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.vvAE2LcqggBv8pxuqU6RKPX65bl7Zl9dfcoIbIQBLf4”, “expires_in”: 43199, “scope”: “read”, “jti”: “8c4ac296-040a-4ce3-8910-1bf66da40997”}携带JWT令牌请求资源curl -X POST -H “authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ0MzExMDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiOGM0YWMyOTYtMDQwYS00Y2UzLTg5MTAtMWJmNjZkYTQwOTk3IiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsicmVhZCJdfQ.YAaSRN0iftmlR6Khz9UxNNEpHHn8zhZwlQrCUCPUmsU” -d ’name=zhangsan’ http://localhost:8081/api/hi返回hi , zhangsan源码https://github.com/gf-huanchu… ...

April 5, 2019 · 2 min · jiezi

ApiBoot - ApiBoot Alibaba Oss 使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。ApiBoot添加快速集成Aliyun的对象存储服务Oss,提供常用的文件操作方法,当然也提供自定义扩展,以致于满足绝大数业务场景,并且通过扩展可以实现上传文件进度条、下载文件进度条、存储空间操作、静态网站托管、访问日志、防盗链、分片上传、追加上传、断点续传等等。引入ApiBoot Alibaba Oss在pom.xml配置文件内添加依赖,如下所示:<!–ApiBoot Alibaba Oss–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-alibaba-oss</artifactId></dependency>ApiBoot所提供的依赖都不需要添加版本号,具体查看ApiBoot版本依赖配置参数列表配置参数参数介绍默认值是否必填api.boot.oss.regionoss所属地域空是api.boot.oss.bucket-nameoss存储空间名称空是api.boot.oss.access-key-id阿里云账户accessKeyId空是api.boot.oss.access-key-secret阿里云账户accessKeySecret空是api.boot.oss.domainoss存储空间所绑定的自定义域名,如果不配置,上传文件成功后返回默认格式化的文件访问路径空否上传文件在使用ApiBoot Oss时,只需要注入ApiBootOssService类就可以完成默认方法的使用,如下所示:@Autowiredprivate ApiBootOssService apiBootOssService;流上传/** * 流方式上传 /@Testpublic void uploadBytes() { ApiBootObjectStorageResponse response = apiBootOssService.upload(“admin.txt”, “admin”.getBytes()); logger.info(“文件名称:{}”, response.getObjectName()); logger.info(“文件访问路径:{}”, response.getObjectUrl());}本地文件上传/** 本地文件上传*/@Testpublic void uploadFile() { ApiBootObjectStorageResponse response = apiBootOssService.upload(“logo.png”, “/Users/yuqiyu/Downloads/logo.png”); logger.info(“文件名称:{}”, response.getObjectName()); logger.info(“文件访问路径:{}”, response.getObjectUrl());}文件流上传/*** 文件流方式上传** @throws Exception*/@Testpublic void uploadInputStream() throws Exception { FileInputStream inputStream = new FileInputStream(new File("/Users/yuqiyu/Downloads/logo.png")); ApiBootObjectStorageResponse response = apiBootOssService.upload(“测试.png”, inputStream); logger.info(“文件名称:{}”, response.getObjectName()); logger.info(“文件访问路径:{}”, response.getObjectUrl());}通过文件的输入流完成对象存储文件的上传下载文件/** * 下载文件 /@Testpublic void download() { apiBootOssOverrideService.download(“测试.png”, “/Users/yuqiyu/Downloads/测试.png”);}在上面的示例中,文件会自动下载到/Users/yuqiyu/Downloads/目录下,文件名称为测试.png。删除文件/** 删除文件示例*/@Testpublic void delete() { apiBootOssOverrideService.delete(“测试.png”);}删除对象存储空间内的文件时只需要传递文件名即可。MultipartFile 上传文件如果你是通过SpringMvc提供的MultipartFile对象进行上传文件,可以通过如下示例进行上传:MultipartFile multipartFile = ..;// 流方式上传ApiBootObjectStorageResponse responseByte = apiBootOssService.upload(“测试.png”, multipartFile.getBytes());// 文件输入流方式上传ApiBootObjectStorageResponse responseIs = apiBootOssService.upload(“测试.png”, multipartFile.getInputStream());自定义扩展ApiBoot Alibaba Oss提供的方法毕竟是有限的,因此ApiBoot提供了自定义的扩展方式,让使用者可以根据Oss官方文档进行扩展,包含上传文件进度条、下载文件进度条、存储空间操作、静态网站托管、访问日志、防盗链、分片上传、追加上传、断点续传等等。自定义扩展首先需要创建类并继承ApiBootOssService,如下所示://…public class ApiBootOssOverrideService extends ApiBootOssService { /** * logger instance / static Logger logger = LoggerFactory.getLogger(ApiBootOssOverrideService.class); /* * 实现父类构造函数 * * @param endpoint 外网节点 * @param bucketName 存储空间名称 * @param accessKeyId 阿里云账号授权Id * @param accessKeySecret 阿里云账号授权Secret * @param domain 自定义域名 / public ApiBootOssOverrideService(String endpoint, String bucketName, String accessKeyId, String accessKeySecret, String domain) { super(endpoint, bucketName, accessKeyId, accessKeySecret, domain); } /* * 创建bucket存储 * * @param bucketName 存储名称 */ public void createBucket(String bucketName) { OSSClient ossClient = getOssClient(); Bucket bucket = ossClient.createBucket(bucketName); logger.info(“新创建存储空间名称:{}”, bucket.getName()); logger.info(“新创建存储空间所属人:{}”, bucket.getOwner().getDisplayName()); closeOssClient(ossClient); }}如上createBucket方法所示ApiBootOssService内部提供了获取OssClient以及关闭OssClient连接的方法,可以直接调用。扩展生效我们自定义的扩展,需要将实例放入SpringIOC容器内,方便我们在使用处进行注入,要注意,由于构造函数参数的原因,无法直接通过@Service或者@Component注解进行标注,需要通过如下方式://…@Bean@ConditionalOnMissingBeanApiBootOssOverrideService apiBootOssOverrideService(ApiBootOssProperties apiBootOssProperties) { return new ApiBootOssOverrideService(apiBootOssProperties.getRegion().getEndpoint(), apiBootOssProperties.getBucketName(), apiBootOssProperties.getAccessKeyId(), apiBootOssProperties.getAccessKeySecret(), apiBootOssProperties.getDomain());}ApiBootOssProperties属性配置类,是ApiBoot内置的,可以在任意地方进行注入,这里目的只是为了拿到相关配置进行构造参数实例化使用。本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-alibaba-oss ...

April 4, 2019 · 1 min · jiezi

ApiBoot - ApiBoot Alibaba Sms 使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。ApiBoot的短信服务模块是由阿里云的国际短信服务提供的,支持国内和国际快速发送验证码、短信通知和推广短信。前提:需要到阿里云控制台申请开通短信服务。引入ApiBoot Alibaba Sms在pom.xml配置文件内添加如下:<!–ApiBoot Alibaba Sms–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-alibaba-sms</artifactId></dependency>ApiBoot所提供的依赖都不需要添加版本号,具体查看ApiBoot版本依赖配置参数列表配置参数参数介绍默认值是否必填api.boot.sms.access-key-idRAM账号的AccessKey ID空是api.boot.sms.access-key-secretRAM账号Access Key Secret空是api.boot.sms.sign-name短信签名空是api.boot.sms.connection-timeout短信发送连接超时时长10000否api.boot.sms.read-timeout短信接收消息连接超时时长10000否api.boot.sms.profile短信区域环境default否发送短信在ApiBoot Alibaba Sms模块内置了ApiBootSmsService接口实现类,通过send方法即可完成短信发送,如下所示: /** * logger instance */ static Logger logger = LoggerFactory.getLogger(ApiBootSmsTest.class); @Autowired private ApiBootSmsService apiBootSmsService; @Test public void sendSms() { // 参数 ApiBootSmsRequestParam param = new ApiBootSmsRequestParam(); param.put(“code”, “192369”); // 请求对象 ApiBootSmsRequest request = ApiBootSmsRequest.builder().phone(“171xxxxx”).templateCode(“SMS_150761253”).param(param).build(); // 发送短信 ApiBootSmsResponse response = apiBootSmsService.send(request); logger.info(“短信发送反馈,是否成功:{}”, response.isSuccess()); }短信模板code自行从阿里云控制台获取。如果在阿里云控制台定义的短信模板存在多个参数,可以通过ApiBootSmsRequestParam#put方法来进行挨个添加,该方法返回值为ApiBootSmsRequestParam本对象。多参数多参数调用如下所示:// 参数ApiBootSmsRequestParam param = new ApiBootSmsRequestParam();param.put(“code”, “192369”).put(“name”, “测试名称”);发送结果反馈执行短信发送后会返回ApiBootSmsResponse实例,通过该实例即可判断短信是否发送成功。本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-alibaba-sms

April 4, 2019 · 1 min · jiezi

ApiBoot - ApiBoot Http Converter 使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。FastJson是阿里巴巴提供的一款Json格式化插件。ApiBoot提供了FastJson驱动转换接口请求的Json字符串数据,添加该依赖后会自动格式化时间(格式:YYYY-MM-DD HH:mm:ss)、空对象转换为空字符串返回、空Number转换为0等,还会自动装载ValueFilter接口的实现类来完成自定义的数据格式转换。引入Http ConverterApiBoot Http Converter使用非常简单,只需要在pom.xml添加如下依赖:<!–ApiBoot Http Converter–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-http-converter</artifactId></dependency>ApiBoot所提供的依赖都不需要添加版本号,具体查看ApiBoot版本依赖相关配置ApiBoot Http Converter通过使用SpringBoot内置的配置参数名来确定是否开启,在SpringBoot内可以通过spring.http.converters.preferred-json-mapper来修改首选的Json格式化插件,SpringBoot已经提供了三种,分别是:gson、jackson、jsonb,当我们配置该参数为fastJson或不进行配置就会使用ApiBoot Http Converter提供的fastJson来格式化转换Json返回数据。如下所示:spring: http: converters: # 不配置默认使用fastJson preferred-json-mapper: fastJson自定义ValueFilterValueFilter是FastJson的概念,用于自定义转换实现,比如:自定义格式化日期、自动截取小数点等。下面提供一个ValueFilter的简单示例,具体的使用请参考FastJson官方文档。ValueFilter示例在使用ValueFilter时一般都会搭配一个对应的自定义@Annotation来进行组合使用,保留自定义小数点位数的示例如下所示:创建 BigDecimalFormatter Annotation@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)public @interface BigDecimalFormatter { /** * 小数位数,默认保留两位 * @return / int scale() default 2;}创建 BigDecimal ValueFilterpublic class BigDecimalValueFilter implements ValueFilter { /* * logback / Logger logger = LoggerFactory.getLogger(BigDecimalValueFilter.class); /* * @param object 对象 * @param name 对象的字段的名称 * @param value 对象的字段的值 / @Override public Object process(Object object, String name, Object value) { if (ValidateTools.isEmpty(value) || !(value instanceof BigDecimal)) { return value; } return convertValue(object, name, value); } /* * 转换值 * * @param object 字段所属对象实例 * @param name 字段名称 * @param value 字段的值 * @return / Object convertValue(Object object, String name, Object value) { try { /* * 反射获取field / Field field = object.getClass().getDeclaredField(name); /* *判断字段是否存在@BigDecimalFormatter注解 */ if (field.isAnnotationPresent(BigDecimalFormatter.class)) { BigDecimalFormatter bigDecimalFormatter = field.getAnnotation(BigDecimalFormatter.class); // 执行格式化 BigDecimal decimal = (BigDecimal) value; System.out.println(bigDecimalFormatter.scale()); // 保留小数位数,删除多余 value = decimal.setScale(bigDecimalFormatter.scale(), BigDecimal.ROUND_DOWN).doubleValue(); } } catch (Exception e) { logger.error(“格式化BigDecimal字段出现异常:{}”, e.getMessage()); } return value; }}使用 BigDecimalFormatter Annotation@BigDecimalFormatterprivate BigDecimal decimalValue;本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-http-converter ...

April 4, 2019 · 1 min · jiezi

ApiBoot - ApiBoot Security Oauth 依赖使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。引入 ApiBoot Security Oauth在pom.xml配置文件内添加如下:<!–ApiBoot Security Oauth–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-security-oauth-jwt</artifactId></dependency>ApiBoot所提供的依赖都不需要添加版本号,但是需要添加版本依赖,具体查看ApiBoot版本依赖配置参数列表ApiBoot在整合SpringSecurity、Oauth2时把配置参数进行了分离,配置列表如下所示:整合SpringSecurity配置列表配置名称介绍默认值生效方式api.boot.security.awaySpringSecurity读取用户的方式,默认为内存方式memoryallapi.boot.security.auth-prefix拦截的接口路径前缀,如:/api/users就会被默认拦截/api/memory/jdbcapi.boot.security.users配置用户列表,具体使用查看内存方式介绍无memoryapi.boot.security.ignoring-urlsSpring Security所排除的路径,默认排除Swagger、Actuator相关路径前缀/v2/api-docs/swagger-ui.html/swagger-resources/configuration/security/META-INF/resources/webjars//swagger-resources/swagger-resources/configuration/ui/actuator/memory/jdbcapi.boot.security.enable-default-store-delegate仅在Jdbc方式生效truejdbc整合Oauth2配置列表配置名称介绍默认值绑定awayapi.boot.oauth.awayOauth存储Token、读取Client信息方式memoryallapi.boot.oauth.cleint-idOauth2 Client IDApiBootmemoryapi.boot.oauth.client-secretOauth2 Client SecretApiBootSecretmemoryapi.boot.oauth.grant-types客户端授权方式Srtring[]{“password”}memoryapi.boot.oauth.scopes客户端作用域String[]{“api”}memoryapi.boot.oauth.jwt.enable是否启用JWT格式化AccessTokenfalsememory/jdbcapi.boot.oauth.jwt.sign-key使用JWT格式化AccessToken时的签名ApiBootmemory/jdbcApiBoot在整合SpringSecurity、Oauth2时配置进行了分离,也就意味着我们可以让SpringSecurity读取内存用户、Oauth2将生成的AccessToken存放到数据库,当然反过来也是可以的,相互不影响!!!内存方式(默认方式)Spring SecurityApiBoot在整合Spring Security的内存方式时,仅仅需要配置api.boot.security.users用户列表参数即可,就是这么的简单,配置用户示例如下所示:api: boot: security: # Spring Security 内存方式用户列表示例 users: - username: hengboy password: 123456 - username: apiboot password: abc321api.boot.security.users是一个List<SecurityUser>类型的集合,所以这里可以配置多个用户。Oauth2如果全部使用默认值的情况话不需要做任何配置!!!Jdbc方式前提:项目需要添加数据源依赖。Spring Security默认用户表ApiBoot在整合Spring Security的Jdbc方式时,在使用ApiBoot提供的默认结构用户表时只需要修改api.boot.security.away: jdbc即可,ApiBoot提供的用户表结构如下所示:CREATE TABLE api_boot_user_info ( UI_ID int(11) NOT NULL AUTO_INCREMENT COMMENT ‘用户编号,主键自增’, UI_USER_NAME varchar(30) DEFAULT NULL COMMENT ‘用户名’, UI_NICK_NAME varchar(50) DEFAULT NULL COMMENT ‘用户昵称’, UI_PASSWORD varchar(255) DEFAULT NULL COMMENT ‘用户密码’, UI_EMAIL varchar(30) DEFAULT NULL COMMENT ‘用户邮箱地址’, UI_AGE int(11) DEFAULT NULL COMMENT ‘用户年龄’, UI_ADDRESS varchar(200) DEFAULT NULL COMMENT ‘用户地址’, UI_IS_LOCKED char(1) DEFAULT ‘N’ COMMENT ‘是否锁定’, UI_IS_ENABLED char(1) DEFAULT ‘Y’ COMMENT ‘是否启用’, UI_STATUS char(1) DEFAULT ‘O’ COMMENT ‘O:正常,D:已删除’, UI_CREATE_TIME timestamp NULL DEFAULT current_timestamp() COMMENT ‘用户创建时间’, PRIMARY KEY (UI_ID)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT=‘ApiBoot默认的用户信息表’;自定义用户表如果你的系统已经存在了自定义用户表结构,ApiBoot是支持的,而且很简单就可以完成整合,我们需要先修改api.boot.security.enable-default-store-delegate参数为false,如下所示:api: boot: security: # Spring Security jdbc方式用户列表示例 enable-default-store-delegate: false away: jdbc添加ApiBootStoreDelegate接口实现类,如下所示:@Componentpublic class DisableDefaultUserTableStoreDelegate implements ApiBootStoreDelegate { @Autowired private PasswordEncoder passwordEncoder; / * 用户列表示例 * 从该集合内读取用户信息 * 可以使用集合内的用户获取access_token / static List<String> users = new ArrayList() { { add(“api-boot”); add(“hengboy”); add(“yuqiyu”); } }; /* * 根据用户名查询用户信息 * * @param username 用户名 * @return * @throws UsernameNotFoundException / @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (!users.contains(username)) { throw new UsernameNotFoundException(“用户:” + username + “不存在”); } return new DisableDefaultUserDetails(username); } @Data @AllArgsConstructor @NoArgsConstructor class DisableDefaultUserDetails implements UserDetails { private String username; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return new ArrayList() { { add((GrantedAuthority) () -> “ROLE_USER”); } }; } /* * 示例密码使用123456 * * @return */ @Override public String getPassword() { return passwordEncoder.encode(“123456”); } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }}根据上面代码示例,我们可以通过users用户列表进行访问获取access_token。Oauth2创建Oauth所需表结构Oauth2如果使用Jdbc方式进行存储access_token、client_details时,需要在数据库内初始化Oauth2所需相关表结构,oauth-mysql.sql添加客户端数据初始化Oauth2表结构后,需要向oauth_client_details表内添加一个客户端信息,下面是对应ApiBoot Security Oauth配置信息的数据初始化,如下所示:INSERT INTO oauth_client_details VALUES (‘ApiBoot’,‘api’,’$2a$10$M5t8t1fHatAj949RCHHB/.j1mrNAbxIz.mOYJQbMCcSPwnBMJLmMK’,‘api’,‘password’,NULL,NULL,7200,7200,NULL,NULL);AppSecret加密方式统一使用BCryptPasswordEncoder,数据初始化时需要注意。在上面memory/jdbc两种方式已经配置完成,接下来我们就可以获取access_token。获取AccessToken通过CURL获取➜ ~ curl ApiBoot:ApiBootSecret@localhost:8080/oauth/token -d “grant_type=password&username=api-boot&password=123456”{“access_token”:“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTMxMDk1MjMsInVzZXJfbmFtZSI6ImFwaS1ib290IiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjBmZTUyY2RlLTBhZjctNDI1YS04Njc2LTFkYTUyZTA0YzUxYiIsImNsaWVudF9pZCI6IkFwaUJvb3QiLCJzY29wZSI6WyJhcGkiXX0.ImqGZssbDEOmpf2lQZjLQsch4ukE0C4SCYJsutfwfx0”,“token_type”:“bearer”,“expires_in”:42821,“scope”:“api”,“jti”:“0fe52cde-0af7-425a-8676-1da52e04c51b”}启用JWTApiBoot Security Oauth在使用JWT格式化access_token时非常简单的,配置如下所示:api: boot: oauth: jwt: # 开启Jwt转换AccessToken enable: true # 转换Jwt时所需加密key,默认为ApiBoot sign-key: 恒宇少年 - 于起宇默认不启用JWT,sign-key签名建议进行更换。本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-security-oauth-jwt ...

April 4, 2019 · 2 min · jiezi

ApiBoot - ApiBoot Quartz 使用文档

ApiBoot QuartzApiBoot内部集成了Quartz,提供了数据库方式、内存方式的进行任务的存储,其中数据库方式提供了分布式集群任务调度,任务自动平滑切换执行节点。引用ApiBoot Quartz在pom.xml配置文件内添加,如下配置:<!–ApiBoot Quartz–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-quartz</artifactId></dependency>备注:如果使用ApiBoot Quartz的内存方式,仅需要添加上面的依赖即可。相关配置参数名称是否必填默认值描述api.boot.quartz.job-store-type否memory任务存储源方式,默认内存方式api.boot.quartz.scheduler-name否scheduler调度器名称api.boot.quartz.auto-startup否true初始化后是否自动启动调度程序api.boot.quartz.startup-delay否0初始化完成后启动调度程序的延迟。api.boot.quartz.wait-for-jobs-to-complete-on-shutdown否false是否等待正在运行的作业在关闭时完成。api.boot.quartz.overwrite-existing-jobs否false配置的作业是否应覆盖现有的作业定义。api.boot.quartz.properties否 Quartz自定义的配置属性,具体参考quartz配置api.boot.quartz.jdbc否 配置数据库方式的Jdbc相关配置内存方式ApiBoot Quartz在使用内存方式存储任务时,不需要做配置调整。数据库方式需要在application.yml配置文件内修改api.boot.quartz.job-store-type参数,如下所示:api: boot: quartz: # Jdbc方式 job-store-type: jdbcQuartz所需表结构Quartz的数据库方式内部通过DataSource获取数据库连接对象来进行操作数据,所操作数据表的表结构是固定的,ApiBoot把Quartz所支持的所有表结构都进行了整理,访问Quartz支持数据库建表语句列表查看,复制执行对应数据库语句即可。创建任务类我们只需要让新建类集成QuartzJobBean就可以完成创建一个任务类,如下简单示例:/** * 任务定义示例 * 与Quartz使用方法一致,ApiBoot只是在原生基础上进行扩展,不影响原生使用 * <p> * 继承QuartzJobBean抽象类后会在项目启动时会自动加入Spring IOC * * @author:恒宇少年 - 于起宇 * <p> * DateTime:2019-03-28 17:26 * Blog:http://blog.yuqiyu.com * WebSite:http://www.jianshu.com/u/092df3f77bca * Gitee:https://gitee.com/hengboy * GitHub:https://github.com/hengboy /public class DemoJob extends QuartzJobBean { /* * logger instance */ static Logger logger = LoggerFactory.getLogger(DemoJob.class); @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { logger.info(“定时任务Job Key : {}”, context.getJobDetail().getKey()); logger.info(“定时任务执行时所携带的参数:{}”, JSON.toJSONString(context.getJobDetail().getJobDataMap())); //…处理逻辑 }}任务参数在任务执行时传递参数是必须的,ApiBoot Quartz提供了比较方便的传递方式,不过最终Quartz会把传递的值都会转换为String类型数据。任务Key默认值ApiBoot Quartz的newJob方法所创建的定时任务,如果在不传递Job Key参数时,会默认使用UUID随机字符串作为Job Key以及Trigger Key。自定义任务开始时间任务开始时间可以通过startAtTime方法进行设置,在不设置的情况下,任务创建完成后会立刻执行。Cron 表达式任务创建Cron类型任务如下所示:String jobKey = apiBootQuartzService.newJob(ApiBootCronJobWrapper.Context() .jobClass(DemoJob.class) .cron(“0/5 * * * * ?”) .param( ApiBootJobParamWrapper.wrapper().put(“param”, “测试”)) .wrapper());Cron 表达式任务由ApiBootCronJobWrapper类进行构建。上面的DemoJob任务类将会每隔5秒执行一次。Loop 重复任务Loop循环任务,当在不传递重复执行次数时,不进行重复执行,仅仅执行一次,如下所示:String jobKey = apiBootQuartzService.newJob( ApiBootLoopJobWrapper.Context() // 参数 .param( ApiBootJobParamWrapper.wrapper() .put(“userName”, “恒宇少年”) .put(“userAge”, 24) ) // 每次循环的间隔时间,单位:毫秒 .loopIntervalTime(2000) // 循环次数 .repeatTimes(5) // 开始时间,10秒后执行 .startAtTime(new Date(System.currentTimeMillis() + 10000)) // 任务类 .jobClass(DemoJob.class) .wrapper() );Loop 任务由ApiBootLoopJobWrapper类进行构建。上面的定时任务将会重复执行5次,连上自身执行的一次也就是会执行6次,每次的间隔时间为2秒,在任务创建10秒后进行执行。Once 一次性任务Once一次性任务,任务执行一次会就会被自动释放,如下所示:Map paramMap = new HashMap(1);paramMap.put(“paramKey”, “参数值”);String jobKey = apiBootQuartzService.newJob( ApiBootOnceJobWrapper.Context() .jobClass(DemoJob.class) // 参数 .param( ApiBootJobParamWrapper.wrapper() .put(“mapJson”, JSON.toJSONString(paramMap)) ) // 开始时间,2秒后执行 .startAtTime(new Date(System.currentTimeMillis() + 2000)) .wrapper());Once 任务由ApiBootOnceJobWrapper类进行构建。在参数传递时可以是对象、集合,不过需要进行转换成字符串才可以进行使用。暂停任务执行任务在执行过程中可以进行暂停操作,通过ApiBoot Quartz提供的pauseJob方法就可以很简单的实现,当然暂停时需要传递Job Key,Job Key可以从创建任务方法返回值获得。暂停任务如下所示:// 暂停指定Job Key的任务apiBootQuartzService.pauseJob(jobKey);// 暂停多个执行中任务apiBootQuartzService.pauseJobs(jobKey,jobKey,jobKey);恢复任务执行任务执行完暂停后,如果想要恢复可以使用如下方式:// 恢复指定Job Key的任务执行apiBootQuartzService.resumeJob(jobKey);// 恢复多个暂停任务apiBootQuartzService.resumeJobs(jobKey,jobKey,jobKey);修改Cron表达式修改Cron表达式的场景如下:已创建 & 未执行已创建 & 已执行修改方法如下所示:// 修改执行Job Key任务的Cron表达式apiBootQuartzService.updateJobCron(jobKey, “0/5 * * * * ?”);删除任务想要手动释放任务时可以使用如下方式:// 手动删除指定Job Key任务apiBootQuartzService.deleteJob(jobKey);// 手动删除多个任务apiBootQuartzService.deleteJobs(jobKey,jobKey,jobKey);删除任务的顺序如下:暂停触发器移除触发器删除任务本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-quartz ...

April 4, 2019 · 1 min · jiezi

ApiBoot - ApiBoot Swagger 使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。ApiBoot通过整合Swagger2完成自动化接口文档生成,只需要一个简单的注解我们就可以实现文档的开启,而且文档上面的所有元素都可以自定义配置,通过下面的介绍来详细了解ApiBoot Swagger的简易之处。引入ApiBoot Swagger在pom.xml配置文件内通过添加如下依赖进行集成:<!–ApiBoot Swagger–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-swagger</artifactId></dependency>注意:ApiBoot所提供的依赖都不需要添加版本号,但是需要添加版本依赖,具体查看ApiBoot版本依赖@EnableApiBootSwagger在添加依赖后需要通过@EnableApiBootSwagger注解进行开启ApiBoot Swagger相关的配置信息自动化构建,可以配置在XxxApplication入口类上,也可以是配置类,让SpringBoot加载到即可。相关配置配置参数参数介绍默认值api.boot.swagger.enable是否启用trueapi.boot.swagger.title文档标题ApiBoot快速集成Swagger文档api.boot.swagger.description文档描述ApiBoot通过自动化配置快速集成Swagger2文档,仅需一个注解、一个依赖即可。api.boot.swagger.base-package文档扫描的packageXxxApplication同级以及子级packageapi.boot.swagger.version文档版本号api.boot.versionapi.boot.swagger.license文档版权ApiBootapi.boot.swagger.license-url文档版权地址https://github.com/hengboy/ap…api.boot.swagger.contact.name文档编写人名称恒宇少年api.boot.swagger.contact.website文档编写人主页http://blog.yuqiyu.comapi.boot.swagger.contact.email文档编写人邮箱地址jnyuqy@gmail.comapi.boot.swagger.authorization.name整合Oauth2后授权名称ApiBoot Security Oauth 认证头信息api.boot.swagger.authorization.key-name整合Oauth2后授权Header内的key-nameAuthorizationapi.boot.swagger.authorization.auth-regex整合Oauth2后授权表达式^.*$以上是目前版本的所有配置参数,大多数都存在默认值,可自行修改。整合ApiBoot Security Oauth如果你的项目添加了Oauth2资源保护,在Swagger界面上访问接口时需要设置AccessToken到Header才可以完成接口的访问,ApiBoot Security Oauth默认开放Swagger所有相关路径,如果项目内并非通过ApiBoot Security Oauth2来做安全认证以及资源保护,需要自行开放Swagger相关路径。整合ApiBoot Security Oauth很简单,访问ApiBoot Security Oauth 查看。携带Token访问Api启动添加ApiBoot-Swagger依赖的项目后,访问http://localhost:8080/swagger-ui.html页面查看Swagger所生成的全部文档,页面右侧可以看到Authorize,点击后打开配置AccessToken的界面,配置的AccessToken必须携带类型,如:Bearer 0798e1c7-64f4-4a2f-aad1-8c616c5aa85b。注意:通过ApiBoot Security Oauth所获取的AccessToken类型都为Bearer。本章源码地址:https://github.com/hengboy/api-boot/tree/master/api-boot-samples/api-boot-sample-swagger

April 4, 2019 · 1 min · jiezi

ApiBoot DataSource Switch 使用文档

ApiBoot是一款基于SpringBoot1.x,2.x的接口服务集成基础框架, 内部提供了框架的封装集成、使用扩展、自动化完成配置,让接口开发者可以选着性完成开箱即用, 不再为搭建接口框架而犯愁,从而极大的提高开发效率。ApiBoot DataSource Switch顾名思义,DataSource Switch是用于数据源选择切换的框架,这是一款基于Spring AOP切面指定注解实现的,通过简单的数据源注解配置就可以完成访问时的自动切换,DataSource Switch切换过程中是线程安全的。添加依赖使用DataSource Switch很简单,在pom.xml配置文件内添加如下依赖:<!–ApiBoot DataSource Switch–><dependency> <groupId>org.minbox.framework</groupId> <artifactId>api-boot-starter-datasource-switch</artifactId></dependency>ApiBoot所提供的依赖都不需要添加版本号,具体查看ApiBoot版本依赖集成数据源实现目前ApiBoot DataSource Switch集成了Druid、HikariCP两种数据源实现依赖,在使用方面也有一定的差异,因为每一个数据源的内置参数不一致。Druid:参数配置前缀为api.boot.datasource.druidHikariCP:参数配置前缀为api.boot.datasource.hikari具体使用请查看下面功能配置介绍。配置参数参数名参数默认值是否必填参数描述api.boot.datasource.primarymaster否主数据源名称api.boot.datasource.druid.{poolName}.url无是数据库连接字符串api.boot.datasource.druid.{poolName}.username无是用户名api.boot.datasource.druid.{poolName}.password无是密码api.boot.datasource.druid.{poolName}.driver-class-namecom.mysql.cj.jdbc.Driver否驱动类型api.boot.datasource.druid.{poolName}.filtersstat,wall,slf4j否Druid过滤api.boot.datasource.druid.{poolName}.max-active20否最大连接数api.boot.datasource.druid.{poolName}.initial-size1否初始化连接数api.boot.datasource.druid.{poolName}.max-wait60000否最大等待市场,单位:毫秒api.boot.datasource.druid.{poolName}.validation-queryselect 1 from dual否检查sqlapi.boot.datasource.druid.{poolName}.test-while-idletrue否建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。api.boot.datasource.druid.{poolName}.test-on-borrowfalse否申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。api.boot.datasource.druid.{poolName}.test-on-returnfalse否归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。api.boot.datasource.hikari.{poolName}.url无是数据库连接字符串api.boot.datasource.hikari.{poolName}.username无是用户名api.boot.datasource.hikari.{poolName}.password无是密码api.boot.datasource.hikari.{poolName}.driver-class-namecom.mysql.cj.jdbc.Driver否数据库驱动类全限定名api.boot.datasource.hikari.{poolName}.property无否HikariCP属性配置HikariCP数据源是SpringBoot2.x自带的,配置参数请访问HikariCP。单主配置ApiBoot DataSource Switch支持单主数据源的配置,application.yml配置文件如下所示:api: boot: datasource: # 配置使用hikari数据源 hikari: # master datasource config master: url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456修改主数据源名称master为默认的主数据源的poolName,这里可以进行修改为其他值,不过需要对应修改primary参数,如下所示:api: boot: datasource: # 主数据源,默认值为master primary: main # 配置使用hikari数据源 hikari: # main datasource config main: url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456在上面配置主数据源的poolName修改为main。主从配置如果你的项目内存在单主单从、一主多从的配置方式,如下所示:api: boot: datasource: # 配置使用hikari数据源 hikari: # master datasource config master: url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 # 默认值为【com.mysql.cj.jdbc.Driver】 #driver-class-name: com.mysql.cj.jdbc.Driver # slave 1 datasource config slave_1: url: jdbc:mysql://localhost:3306/oauth2?characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 # slave 2 datasource config slave_2: url: jdbc:mysql://localhost:3306/resources?characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456在上面是一主多从的配置方式,分别是master、slave_1、slave_2。多类型数据库配置ApiBoot DataSource Switch提供了一个项目内连接多个不同类型的数据库,如:MySQL、Oracle…等,如下所示:api: boot: # 主数据源,默认值为master primary: mysql hikari: mysql: url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 oracle: url: jdbc:oracle:thin:@172.16.10.25:1521:torcl username: root password: 123456 driver-class-name: oracle.jdbc.driver.OracleDriver在上面配置中,master主数据源使用的MySQL驱动连接MySQL数据库,而slave从数据源则是使用的Oracle驱动连接的Oracle数据库。动态创建数据源ApiBoot DataSource Switch内部提供了动态创建数据源的方法,可以通过注入ApiBootDataSourceFactoryBean来进行添加,如下所示:@Autowiredprivate ApiBootDataSourceFactoryBean factoryBean;public void createNewDataSource() throws Exception { // 创建Hikari数据源 // 如果创建Druid数据源,使用DataSourceDruidConfig DataSourceHikariConfig config = new DataSourceHikariConfig(); // 数据库连接:必填 config.setUrl(“jdbc:mysql://localhost:3306/resources”); // 用户名:必填 config.setUsername(“root”); // 密码:必填 config.setPassword(“123456”); // 数据源名称:必填(用于@DataSourceSwitch注解value值使用) config.setPoolName(“dynamic”); // 创建数据源 DataSource dataSource = factoryBean.newDataSource(config); Connection connection = dataSource.getConnection(); System.out.println(connection.getCatalog()); connection.close();}自动切换ApiBoot DataSource Switch的数据源自动切换主要归功于Spring的AOP,通过切面@DataSourceSwitch注解,获取注解配置的value值进行设置当前线程所用的数据源名称,从而通过AbstractRoutingDataSource进行数据源的路由切换。我们沿用上面一主多从的配置进行代码演示,配置文件application.yml参考上面配置,代码示例如下:从数据源示例类@Service@DataSourceSwitch(“slave”)public class SlaveDataSourceSampleService { /** * DataSource Instance / @Autowired private DataSource dataSource; /* * 演示输出数据源的catalog * * @throws Exception / public void print() throws Exception { // 获取链接 Connection connection = dataSource.getConnection(); // 输出catalog System.out.println(this.getClass().getSimpleName() + " ->" + connection.getCatalog()); // 关闭链接 connection.close(); }}主数据源示例类@Service@DataSourceSwitch(“master”)public class MasterDataSourceSampleService { /* * DataSource Instance / @Autowired private DataSource dataSource; /* * Slave Sample Service / @Autowired private SlaveDataSourceSampleService slaveDataSourceSampleService; /* * 演示输出主数据源catalog * 调用从数据源类演示输出catalog * * @throws Exception / public void print() throws Exception { Connection connection = dataSource.getConnection(); System.out.println(this.getClass().getSimpleName() + " ->" + connection.getCatalog()); connection.close(); slaveDataSourceSampleService.print(); }}在主数据源的示例类内,我们通过@DataSourceSwitch(“master”)注解的value进行定位连接master数据源数据库。同样在从数据库的示例类内,我们也可以通过@DataSourceSwitch(“slave”)注解的value进行定位连接slave数据源数据库。单元测试示例在上面的测试示例中,我们使用交叉的方式进行验证数据源路由是否可以正确的进行切换,可以编写一个单元测试进行验证结果,如下所示:@Autowiredprivate MasterDataSourceSampleService masterDataSourceSampleService;@Testpublic void contextLoads() throws Exception { masterDataSourceSampleService.print();}运行上面测试方法,结果如下所示:MasterDataSourceSampleService ->test2019-04-04 10:20:45.407 INFO 7295 — [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Starting…2019-04-04 10:20:45.411 INFO 7295 — [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Start completed.SlaveDataSourceSampleService ->oauth2单次执行数据源切换没有任何的问题,master数据源获取catalog输出后,调用slave示例类进行输出catalog。ApiBoot DataSource Switch会在项目启动时首先初始化master节点DataSource实例,其他实例会在第一次调用时进行初始化。压力性能测试单次执行单线程操作没有问题,不代表多线程下不会出现问题,在开头说到过ApiBoot DataSource Switch是线程安全的,所以接下来我们来验证这一点,我们需要添加压力测试的依赖,如下所示:<dependency> <groupId>org.databene</groupId> <artifactId>contiperf</artifactId> <version>2.3.4</version> <scope>test</scope></dependency>接下来把上面的单元测试代码改造下,如下所示:// 初始化压力性能测试对象@Rulepublic ContiPerfRule i = new ContiPerfRule();@Autowiredprivate MasterDataSourceSampleService masterDataSourceSampleService;/** 开启500个线程执行10000次*/@Test@PerfTest(invocations = 10000, threads = 500)public void contextLoads() throws Exception { masterDataSourceSampleService.print();}测试环境:硬件:i7、16G、256SSD系统:OS X整个过程大约是10秒左右,ApiBoot DataSource Switch并没有发生出现切换错乱的情况。注意事项在使用ApiBoot DataSource Switch时需要添加对应数据库的依赖如果使用Druid连接池,不要配置使用druid-starter的依赖,请使用druid依赖。配置poolName时不要添加特殊字符、中文、中横线等。 ...

April 4, 2019 · 2 min · jiezi

Spring Boot 中的静态资源到底要放在哪里?

当我们使用 SpringMVC 框架时,静态资源会被拦截,需要添加额外配置,之前老有小伙伴在微信上问松哥Spring Boot 中的静态资源加载问题:“松哥,我的HTML页面好像没有样式?”,今天我就通过一篇文章,来和大伙仔细聊一聊这个问题。SSM 中的配置要讲 Spring Boot 中的问题,我们得先回到 SSM 环境搭建中,一般来说,我们可以通过 <mvc:resources /> 节点来配置不拦截静态资源,如下:<mvc:resources mapping="/js/" location="/js/"/><mvc:resources mapping="/css/" location="/css/"/><mvc:resources mapping="/html/" location="/html/"/>由于这是一种Ant风格的路径匹配符,/ 表示可以匹配任意层级的路径,因此上面的代码也可以像下面这样简写:<mvc:resources mapping="/" location="/"/>这种配置是在 XML 中的配置,大家知道,SpringMVC 的配置除了在XML中配置,也可以在 Java 代码中配置,如果在Java代码中配置的话,我们只需要自定义一个类,继承自WebMvcConfigurationSupport即可:@Configuration@ComponentScan(basePackages = “org.sang.javassm”)public class SpringMVCConfig extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/").addResourceLocations("/"); }}重写 WebMvcConfigurationSupport 类中的addResourceHandlers方法,在该方法中配置静态资源位置即可,这里的含义和上面 xml 配置的含义一致,因此无需多说。 这是我们传统的解决方案,在Spring Boot 中,其实配置方式和这个一脉相承,只是有一些自动化的配置了。Spring Boot 中的配置在 Spring Boot 中,如果我们是从 https://start.spring.io 这个网站上创建的项目,或者使用 IntelliJ IDEA 中的 Spring Boot 初始化工具创建的项目,默认都会存在 resources/static 目录,很多小伙伴也知道静态资源只要放到这个目录下,就可以直接访问,除了这里还有没有其他可以放静态资源的位置呢?为什么放在这里就能直接访问了呢?这就是本文要讨论的问题了。整体规划首先,在 Spring Boot 中,默认情况下,一共有5个位置可以放静态资源,五个路径分别是如下5个:classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public//前四个目录好理解,分别对应了resources目录下不同的目录,第5个 / 是啥意思呢?我们知道,在 Spring Boot 项目中,默认是没有 webapp 这个目录的,当然我们也可以自己添加(例如在需要使用JSP的时候),这里第5个 / 其实就是表示 webapp 目录中的静态资源也不被拦截。如果同一个文件分别出现在五个目录下,那么优先级也是按照上面列出的顺序。 不过,虽然有5个存储目录,除了第5个用的比较少之外,其他四个,系统默认创建了 classpath:/static/ , 正常情况下,我们只需要将我们的静态资源放到这个目录下即可,也不需要额外去创建其他静态资源目录,例如我在 classpath:/static/ 目录下放了一张名为1.png 的图片,那么我的访问路径是:http://localhost:8080/1.png 这里大家注意,请求地址中并不需要 static,如果加上了static反而多此一举会报404错误。很多人会觉得奇怪,为什么不需要添加 static呢?资源明明放在 static 目录下。其实这个效果很好实现,例如在SSM配置中,我们的静态资源拦截配置如果是下面这样:<mvc:resources mapping="/" location="/static/"/>如果我们是这样配置的话,请求地址如果是 http://localhost:8080/1.png 实际上系统会去 /static/1.png 目录下查找相关的文件。 所以我们理所当然的猜测,在 Spring Boot 中可能也是类似的配置。源码解读胡适之先生说:“大胆猜想,小心求证”,我们这里就通过源码解读来看看 Spring Boot 中的静态资源到底是怎么配置的。 首先我们在 WebMvcAutoConfiguration 类中看到了 SpringMVC 自动化配置的相关的内容,找到了静态资源拦截的配置,如下: 可以看到这里静态资源的定义和我们前面提到的Java配置SSM中的配置非常相似,其中,this.mvcProperties.getStaticPathPattern() 方法对应的值是 “/”,this.resourceProperties.getStaticLocations()方法返回了四个位置,分别是:“classpath:/META-INF/resources/”, “classpath:/resources/”,“classpath:/static/”, “classpath:/public/",然后在getResourceLocations方法中,又添加了“/”,因此这里返回值一共有5个。其中,/表示webapp目录,即webapp中的静态文件也可以直接访问。静态资源的匹配路径按照定义路径优先级依次降低。因此这里的配置和我们前面提到的如出一辙。这样大伙就知道了为什么Spring Boot 中支持5个静态资源位置,同时也明白了为什么静态资源请求路径中不需要/static,因为在路径映射中已经自动的添加上了/static了。自定义配置当然,这个是系统默认配置,如果我们并不想将资源放在系统默认的这五个位置上,也可以自定义静态资源位置和映射,自定义的方式也有两种,可以通过 application.properties 来定义,也可以在 Java 代码中来定义,下面分别来看。application.properties在配置文件中定义的方式比较简单,如下:spring.resources.static-locations=classpath:/spring.mvc.static-path-pattern=/第一行配置表示定义资源位置,第二行配置表示定义请求 URL 规则。以上文的配置为例,如果我们这样定义了,表示可以将静态资源放在 resources目录下的任意地方,我们访问的时候当然也需要写完整的路径,例如在resources/static目录下有一张名为1.png 的图片,那么访问路径就是 http://localhost:8080/static/1.png ,注意此时的static不能省略。Java 代码定义当然,在Spring Boot中我们也可以通过 Java代码来自定义,方式和 Java 配置的 SSM 比较类似,如下:@Configurationpublic class WebMVCConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(”/").addResourceLocations(“classpath:/aaa/”); }}这里代码基本和前面一致,比较简单,不再赘述。总结这里需要提醒大家的是,松哥见到有很多人用了 Thymeleaf 之后,会将静态资源也放在 resources/templates 目录下,注意,templates 目录并不是静态资源目录,它是一个放页面模板的位置(你看到的 Thymeleaf 模板虽然后缀为 .html,其实并不是静态资源)。好了,通过上面的讲解,相信大家对 Spring Boot 中静态资源的位置有一个深刻了解了,应该不会再在项目中出错了吧! 更多资料,请关注公众号牧码小子,回复 Java, 获取松哥为你精心准备的Java干货! ...

April 4, 2019 · 1 min · jiezi

SpringBoot | 自动配置原理

微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。前言这个月过去两天了,这篇文章才跟大家见面,最近比较累,大家见谅下。下班后闲着无聊看了下 SpringBoot 中的自动配置,把我的理解跟大家说下。配置文件能写什么?相信接触过 SpringBoot 的朋友都知道 SpringBoot 有各种 starter 依赖,想要什么直接勾选加进来就可以了。想要自定义的时候就直接在配置文件写自己的配置就好。但你们有没有困惑,为什么 SpringBoot 如此智能,到底配置文件里面能写什么呢?带着这个疑问,我翻了下 SpringBoot 官网看到这么一些配置样例:发现 SpringBoot 可配置的东西非常多,上图只是节选。有兴趣的查看这个网址:https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/reference/htmlsingle/#boot-features-external-config-yaml自动配置原理这里我拿之前创建过的 SpringBoot 来举例讲解 SpringBoot 的自动配置原理,首先看这么一段代码:@SpringBootApplicationpublic class JpaApplication { public static void main(String[] args) { SpringApplication.run(JpaApplication.class, args); }}毫无疑问这里只有 @SpringBootApplication 值得研究,进入 @SpringBootApplication 的源码:SpringBoot 启动的时候加载主配置类,开启了自动配置功能 @EnableAutoConfiguration,再进入 @EnableAutoConfiguration 源码:发现最重要的就是 @Import(AutoConfigurationImportSelector.class) 这个注解,其中的 AutoConfigurationImportSelector 类的作用就是往 Spring 容器中导入组件,我们再进入这个类的源码,发现有这几个方法:/*** 方法用于给容器中导入组件**/@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader .loadMetadata(this.beanClassLoader); AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry( autoConfigurationMetadata, annotationMetadata); // 获取自动配置项 return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());}// 获取自动配置项protected AutoConfigurationEntry getAutoConfigurationEntry( AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } AnnotationAttributes attributes = getAttributes(annotationMetadata); List < String > configurations = getCandidateConfigurations(annotationMetadata, attributes); // 获取一个自动配置 List ,这个 List 就包含了所有自动配置的类名 configurations = removeDuplicates(configurations); Set < String > exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = filter(configurations, autoConfigurationMetadata); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions);}// 获取一个自动配置 List ,这个 List 就包含了所有的自动配置的类名protected List < String > getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 通过 getSpringFactoriesLoaderFactoryClass 获取默认的 EnableAutoConfiguration.class 类名,传入 loadFactoryNames 方法 List < String > configurations = SpringFactoriesLoader.loadFactoryNames( getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, “No auto configuration classes found in META-INF/spring.factories. If you " + “are using a custom packaging, make sure that file is correct.”); return configurations;}// 默认的 EnableAutoConfiguration.class 类名protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class;}代码注释很清楚:首先注意到 selectImports 方法,其实从方法名就能看出,这个方法用于给容器中导入组件,然后跳到 getAutoConfigurationEntry 方法就是用于获取自动配置项的。再来进入 getCandidateConfigurations 方法就是 获取一个自动配置 List ,这个 List 就包含了所有的自动配置的类名 。再进入 SpringFactoriesLoader 类的 loadFactoryNames 方法,跳转到 loadSpringFactories 方法发现 ClassLoader 类加载器指定了一个 FACTORIES_RESOURCE_LOCATION 常量。然后利用PropertiesLoaderUtils 把 ClassLoader 扫描到的这些文件的内容包装成 properties 对象,从 properties 中获取到 EnableAutoConfiguration.class 类(类名)对应的值,然后把他们添加在容器中。public static List < String > loadFactoryNames(Class < ? > factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());}private static Map < String, List < String >> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap < String, String > result = cache.get(classLoader); if (result != null) { return result; } try { // 扫描所有 jar 包类路径下 META-INF/spring.factories Enumeration < URL > urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap < > (); while (urls.hasMoreElements()) { URL url = urls.nextElement(); UrlResource resource = new UrlResource(url); // 把扫描到的这些文件的内容包装成 properties 对象 Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry < ? , ? > entry : properties.entrySet()) { String factoryClassName = ((String) entry.getKey()).trim(); for (String factoryName: StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { // 从 properties 中获取到 EnableAutoConfiguration.class 类(类名)对应的值,然后把他们添加在容器中 result.add(factoryClassName, factoryName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException(“Unable to load factories from location [” + FACTORIES_RESOURCE_LOCATION + “]”, ex); }}点击 FACTORIES_RESOURCE_LOCATION 常量,我发现它指定的是 jar 包类路径下 META-INF/spring.factories 文件:public static final String FACTORIES_RESOURCE_LOCATION = “META-INF/spring.factories”;将类路径下 META-INF/spring.factories 里面配置的所有 EnableAutoConfiguration 的值加入到了容器中,所有的 EnableAutoConfiguration 如下所示:注意到 EnableAutoConfiguration 有一个 = 号,= 号后面那一串就是这个项目需要用到的自动配置类。# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\org.springframework.boot.autoconfigure.reactor.core.ReactorCoreAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.SecurityRequestMatcherProviderAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration每一个这样的 xxxAutoConfiguration 类都是容器中的一个组件,都加入到容器中,用他们来做自动配置。上述的每一个自动配置类都有自动配置功能,也可在配置文件中自定义配置。举例说明 Http 编码自动配置原理@Configuration // 表示这是一个配置类,以前编写的配置文件一样,也可以给容器中添加组件@EnableConfigurationProperties(HttpEncodingProperties.class) // 启动指定类的 ConfigurationProperties 功能;将配置文件中对应的值和 HttpEncodingProperties 绑定起来;并把 HttpEncodingProperties 加入到 ioc 容器中@ConditionalOnWebApplication // Spring 底层 @Conditional 注解,根据不同的条件,如果满足指定的条件,整个配置类里面的配置就会生效;判断当前应用是否是 web 应用,如果是,当前配置类生效@ConditionalOnClass(CharacterEncodingFilter.class) // 判断当前项目有没有这个类 CharacterEncodingFilter;SpringMVC 中进行乱码解决的过滤器;@ConditionalOnProperty(prefix = “spring.http.encoding”, value = “enabled”, matchIfMissing = true) // 判断配置文件中是否存在某个配置 spring.http.encoding.enabled;如果不存在,判断也是成立的// 即使我们配置文件中不配置 pring.http.encoding.enabled=true,也是默认生效的;public class HttpEncodingAutoConfiguration { // 已经和 SpringBoot 的配置文件建立映射关系了 private final HttpEncodingProperties properties; //只有一个有参构造器的情况下,参数的值就会从容器中拿 public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) { this.properties = properties; } @Bean // 给容器中添加一个组件,这个组件的某些值需要从 properties 中获取 @ConditionalOnMissingBean(CharacterEncodingFilter.class) public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); filter.setEncoding(this.properties.getCharset().name()); filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST)); filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE)); return filter; }自动配置类做了什么不在这里赘述,请见上面代码。所有在配置文件中能配置的属性都是在 xxxxProperties 类中封装的;配置文件能配置什么就可以参照某个功能对应的这个属性类,例如上述提到的 @EnableConfigurationProperties(HttpProperties.class) ,我们打开 HttpProperties 文件源码节选:@ConfigurationProperties(prefix = “spring.http”)public class HttpProperties { public static class Encoding { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; /** * Charset of HTTP requests and responses. Added to the “Content-Type” header if * not set explicitly. / private Charset charset = DEFAULT_CHARSET; /* * Whether to force the encoding to the configured charset on HTTP requests and * responses. */ private Boolean force;}在上面可以发现里面的属性 charset 、force 等,都是我们可以在配置文件中指定的,它的前缀就是 spring.http.encoding 如:另外,如果配置文件中有配该属性就取配置文件的,若无就使用 XxxxProperties.class 文件的默认值,比如上述代码的 Charset 属性,如果不配那就使用 UTF-8 默认值。总结1. SpringBoot 启动会加载大量的自动配置类2. 我们看我们需要的功能有没有 SpringBoot 默认写好的自动配置类;3. 我们再来看这个自动配置类中到底配置了哪些组件 ( 只要我们要用的组件有,我们就不需要再来配置,若没有,我们可能就要考虑自己写一个配置类让 SpringBoot 扫描了)4. 给容器中自动配置类添加组件的时候,会从 properties 类中获取某些属性。我们就可以在配置文件中指定这些属性的值;xxxxAutoConfigurartion 自动配置类的作用就是给容器中添加组件xxxxProperties 的作用就是封装配置文件中相关属性至此,总算弄明白了 SpringBoot 的自动配置原理。我水平优先,如有不当之处,敬请指出,相互交流学习,希望对你们有帮助。后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

April 4, 2019 · 3 min · jiezi

SpringBoot引入Thymeleaf

1.Thymeleaf简介 Thymeleaf是个XML/XHTML/HTML5模板引擎,可以用于Web与非Web应用 Thymeleaf的主要目标在于提供一种可被浏览器正确显示的、格式良好的模板创建方式,因此也可以用作静态建模,Thymeleaf的可扩展性也非常棒。你可以使用它定义自己的模板属性集合,这样就可以计算自定义表达式并使用自定义逻辑,Thymeleaf还可以作为模板引擎框架。2.引入Thymeleaf引入依赖在maven(pom.xml)中直接引入:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>配置Thymeleaf在application.yml配置Thymeleafserver: port: 8000spring: thymeleaf: cache: false # 关闭页面缓存 encoding: UTF-8 # 模板编码 prefix: classpath:/templates/ # 页面映射路径 suffix: .html # 试图后的后缀 mode: HTML5 # 模板模式# 其他具体配置可参考org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties# 上面的配置实际上就是注入该类的属性值demo示例创建IndexController@Controllerpublic class IndexController { // 返回视图页面 @RequestMapping(“index”) public String index(){ return “index”; }} 创建index.html<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Title</title></head><body> Hello Thymeleaf!</body></html> 创建TestController@RestControllerpublic class TestController { // 返回整个页面 @RequestMapping("/test") public ModelAndView test(){ return new ModelAndView(“test”); }} 创建test.html<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Title</title></head><body>Hello Thymeleaf! </br>By: ModelAndView</body></html>3.测试结果4.Thymeleaf基础语法及使用1.引入标签 html标签里引入xmlns:th=“http://www.thymeleaf.org"才能使用th:这样的语法 2.引入URL @{…} 例如:<a th:href=”@{http://www.baidu.com}">绝对路径</a> 是访问绝对路径下的URL, <a th:href="@{/}">相对路径</a> 是访问相对路径下的URL。<a th:href="@{css/bootstrap.min.css}">是引入默认的static下的css文件夹下的bootstrap文件,类似的标签有: th:href 和 th:src 3.获取变量 通过${}取值,对于JavaBean的话,使用变量名.属性名获取 4.字符串替换<span th:text="‘Welcome to our application, ’ + ${user.name} + ‘!’"></span>或者<span th:text="|Welcome to our application, ${user.name}!|"></span>注意:|…|中只能包含变量表达式${…},不能包含其他常量、条件表达式等5.运算符 在表达式中可以使用各类算术运算符 例如 (+, -, , /, %) 例如:th:with=“isEven=(${stat.number} % 1 == 0)” 逻辑运算符 (>, <, <=,>=,==,!=) 需要注意的是使用<,>的时候需要转义th:if="${stat.number} &gt; 1"th:text="‘Execution mode is ’ + ( (${execMode} == ‘dev’)? ‘Development’ : ‘Production’)“6.条件 if/unless th:if是该标签在满足条件的时候才会显示,unless是不成立时候才显示<a th:href=”@{/login}" th:unless=${user.number != null}>Login</a> switch thymeleaf支持switch结构,默认属性(default)用表示<div th:switch="${user.role}"> <p th:case="‘admin’">User is an administrator</p> <p th:case="#{roles.manager}">User is a manager</p> <p th:case="">User is some other thing</p></div>7.循环<tr th:each=“prod : ${prods}"> <td th:text="${prod.name}">Onions</td> <td th:text="${prod.price}">2.41</td> <td th:text="${prod.inStock}? #{true} : #{false}">yes</td></tr>8.Utilities内置在Context中,可以直接通过#访问#dates #calendars #numbers #strings arrays lists sets maps … 5.小结 本文讲述了如何在Spring Boot中引入模板引擎Thymeleaf以及Thymeleaf基础语法和实际使用本文GitHub地址:https://github.com/ishuibo/Sp… ...

April 3, 2019 · 1 min · jiezi

两年了,我写了这些干货!

开公众号差不多两年了,有不少原创教程,当原创越来越多时,大家搜索起来就很不方便,因此做了一个索引帮助大家快速找到需要的文章!Spring Boot系列SpringBoot+SpringSecurity处理Ajax登录请求SpringBoot+Vue前后端分离(一):使用SpringSecurity完美处理权限问题1SpringBoot+Vue前后端分离(二):使用SpringSecurity完美处理权限问题2SpringBoot+Vue前后端分离(三):SpringSecurity中密码加盐与SpringBoot中异常统一处理SpringBoot+Vue前后端分离(四):axios请求封装和异常统一处理SpringBoot+Vue前后端分离(五):权限管理模块中动态加载Vue组件SpringBoot+Vue前后端分离(六):使用SpringSecurity完美处理权限问题SpringBoot中自定义参数绑定SpringBoot中使用POI,快速实现Excel导入导出SpringBoot中发送QQ邮件SpringBoot中使用Freemarker构建邮件模板SpringBoot+WebSocket实现在线聊天(一)SpringBoot+WebSocket实现在线聊天(二)SpringSecurity登录使用JSON格式数据SpringSecurity登录添加验证码SpringSecurity中的角色继承问题Spring Boot中通过CORS解决跨域问题Spring Boot数据持久化之JdbcTemplateSpring Boot多数据源配置之JdbcTemplate最简单的SpringBoot整合MyBatis教程极简Spring Boot整合MyBatis多数据源Spring Boot中的yaml配置简介SpringBoot整合Swagger2,再也不用维护接口文档了Spring Boot中,Redis缓存还能这么用!干货|一文读懂 Spring Data Jpa!Spring基础配置Spring常用配置Spring常用配置(二)SpringMVC基础配置SpringMVC常用配置JavaWeb之最简洁的配置实现文件上传初识Spring Boot框架DIY一个Spring Boot的自动配置使用Spring Boot开发Web项目为我们的Web添加HTTPS支持在Spring Boot框架下使用WebSocket实现消息推送一个JavaWeb搭建的开源Blog系统,整合SSM框架Spring Cloud系列1.使用Spring Cloud搭建服务注册中心2.使用Spring Cloud搭建高可用服务注册中心3.Spring Cloud中服务的发现与消费4.Eureka中的核心概念5.什么是客户端负载均衡6.Spring RestTemplate中几种常见的请求方式7.RestTemplate的逆袭之路,从发送请求到负载均衡8.Spring Cloud中负载均衡器概览9.Spring Cloud中的负载均衡策略10.Spring Cloud中的断路器Hystrix11.Spring Cloud自定义Hystrix请求命令12.Spring Cloud中Hystrix的服务降级与异常处理13.Spring Cloud中Hystrix的请求缓存14.Spring Cloud中Hystrix的请求合并15.Spring Cloud中Hystrix仪表盘与Turbine集群监控16.Spring Cloud中声明式服务调用Feign17.Spring Cloud中Feign的继承特性18.Spring Cloud中Feign配置详解19.Spring Cloud中的API网关服务Zuul20.Spring Cloud Zuul中路由配置细节21.Spring Cloud Zuul中异常处理细节22.分布式配置中心Spring Cloud Config初窥23.Spring Cloud Config服务端配置细节(一)24.Spring Cloud Config服务端配置细节(二)之加密解密25.Spring Cloud Config客户端配置细节26.Spring Cloud Bus之RabbitMQ初窥27.Spring Cloud Bus整合RabbitMQ28.Spring Cloud Bus整合Kafka29.Spring Cloud Stream初窥30.Spring Cloud Stream使用细节31.Spring Cloud系列勘误Docker系列Docker教程合集MongoDB系列1.Linux上安装MongoDB2.MongoDB基本操作3.MongoDB数据类型4.MongoDB文档更新操作5.MongoDB文档查询操作(一)6.MongoDB文档查询操作(二)7.MongoDB文档查询操作(三)8.MongoDB查看执行计划9.初识MongoDB中的索引10.MongoDB中各种类型的索引11.MongoDB固定集合12.MongoDB管道操作符(一)13.MongoDB管道操作符(二)14.MongoDB中MapReduce使用15.MongoDB副本集搭建16.MongoDB副本集配置17.MongoDB副本集其他细节18.初识MongoDB分片19.Java操作MongoDBRedis系列教程1.Linux上安装Redis2.Redis中的五种数据类型简介3.Redis字符串(STRING)介绍4.Redis字符串(STRING)中BIT相关命令5.Redis列表与集合6.Redis散列与有序集合7.Redis中的发布订阅和事务8.Redis快照持久化9.Redis之AOF持久化10.Redis主从复制(一)11.Redis主从复制(二)12.Redis集群搭建13.Jedis使用14.Spring Data Redis使用Git系列1.Git概述2.Git基本操作3.Git中的各种后悔药4.Git分支管理5.Git关联远程仓库6.Git工作区储藏兼谈分支管理中的一个小问题7.Git标签管理Elasticsearch系列引言elasticsearch安装与配置初识elasticsearch中的REST接口elasticsearch修改数据elasticsearch文档操作elasticsearch API约定(一)elasticsearch API约定(二)elasticsearch文档读写模型elasticsearch文档索引API(一)elasticsearch文档索引API(二)elasticsearch文档Get APIelasticsearch文档Delete APIelasticsearch文档Delete By Query API(一)elasticsearch文档Delete By Query API(二)elasticsearch文档Update API我的Github开源项目开源项目(一): SpringBoot+Vue前后端分离开源项目-微人事开源项目(二): SpringBoot+Vue前后端分离开源项目-V部落开源项目(三): 一个开源的五子棋大战送给各位小伙伴!开源项目(四):一个开源的会议管理系统献给给位小伙伴!开源项目(五):一个JavaWeb搭建的开源Blog系统,整合SSM框架杂谈从高考到程序员之毕业流水帐从高考到现在起早贪黑几个月,我写完了人生第一本书!当公司倒闭时,你在干什么?华为云 open day,带你看看别人家的公司其他小程序开发框架WePY和mpvue使用感受两步解决maven依赖导入失败问题干货|6个牛逼的基于Vue.js的后台控制面板,接私活必备Ajax上传图片以及上传之前先预览一个简单的案例带你入门Dubbo分布式框架WebSocket刨根问底(一)WebSocket刨根问底(二)WebSocket刨根问底(三)之群聊Nginx+Tomcat搭建集群,Spring Session+Redis实现Session共享IntelliJ IDEA中创建Web聚合项目(Maven多模块项目)Linux上安装Zookeeper以及一些注意事项初识ShiroShiro中的授权问题Shiro中的授权问题(二)更多资料,请关注公众号牧码小子,回复 Java, 获取松哥为你精心准备的Java干货! ...

April 3, 2019 · 1 min · jiezi

Spring 官方文档完整翻译

以下所有文档均包含多个版本,并支持多语言(英文及中文)。Spring Boot 中文文档Spring Framework 中文文档Spring Cloud 中文文档Spring Security 中文文档Spring Session 中文文档Spring AMQP 中文文档Spring DataSpring Data JPASpring Data JDBCSpring Data RedisContributing如果你希望参与文档的校对及翻译工作,请在 这里 提 PR。

April 3, 2019 · 1 min · jiezi

Spring Boot 整合 docker

一、什么是docker ?简介Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。开发者在笔记本上编译测试通过的容器可以批量地在生产环境中部署,包括VMs(虚拟机)、bare metal、OpenStack 集群和其他的基础应用平台。docker的应用场景web应用的自动化打包和发布;自动化测试和持续集成、发布;在服务型环境中部署和调整数据库或其他的后台应用;从头编译或者扩展现有的OpenShift或Cloud Foundry平台来搭建自己的PaaS环境。二、整合 docker创建工程创建一个springboot工程springboot-docker1. 启动类package com.gf;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;@SpringBootApplication@RestControllerpublic class SpringbootDockerApplication { public static void main(String[] args) { SpringApplication.run(SpringbootDockerApplication.class, args); } @GetMapping("/{name}") public String hi(@PathVariable(value = “name”) String name) { return “hi , " + name; }}2. 将springboot工程容器化我们编写一个Dockerfile来定制镜像,在src/main/resources/docker 下创建Dockerfile文件FROM frolvlad/alpine-oraclejdk8:slimVOLUME /tmpADD springboot-docker-0.0.1-SNAPSHOT.jar app.jarRUN sh -c ’touch /app.jar’ENV JAVA_OPTS=““ENTRYPOINT [ “sh”, “-c”, “java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar” ]3. pom.xml我们通过maven 构建docker镜像。在maven的pom目录,加上docker镜像构建的插件<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.1.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>com.gf</groupId> <artifactId>springboot-docker</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot-docker</name> <description>Demo project for Spring Boot</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <docker.image.prefix>gf</docker.image.prefix> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>1.2.0</version> <configuration> <imageName>${docker.image.prefix}/${project.artifactId}</imageName> <dockerDirectory>src/main/resources/docker</dockerDirectory> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin> </plugins> </build></project>构建镜像我们运行下面的命令构建镜像:mvn cleanmvn package docker:bulid构建成功后,我们通过下面的命令查看镜像:docker images启动镜像:#c2dba352c3c1 为镜像IDdocker run -p 8080:8080 -t c2dba352c3c1之后我们就可以访问服务了。源码下载:https://github.com/gf-huanchupk/SpringBootLearning关注我的公众号,精彩内容不能错过~ ...

April 1, 2019 · 1 min · jiezi

Spring Boot 整合 elasticsearch

一、简介我们的应用经常需要添加检索功能,开源的 ElasticSearch 是目前全文搜索引擎的 首选。他可以快速的存储、搜索和分析海量数据。Spring Boot通过整合Spring Data ElasticSearch为我们提供了非常便捷的检索功能支持; Elasticsearch是一个分布式搜索服务,提供Restful API,底层基于Lucene,采用 多shard(分片)的方式保证数据安全,并且提供自动resharding的功能,github 等大型的站点也是采用了ElasticSearch作为其搜索服务,二、安装elasticsearch我们采用 docker镜像安装的方式。#下载镜像docker pull elasticsearch#启动镜像,elasticsearch 启动是会默认分配2G的内存 ,我们启动是设置小一点,防止我们内存不够启动失败#9200是elasticsearch 默认的web通信接口,9300是分布式情况下,elasticsearch个节点通信的端口docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 –name es01 5c1e1ecfe33a访问 127.0.0.1:9200 如下图,说明安装成功三、elasticsearch的一些概念以 员工文档 的形式存储为例:一个文档代表一个员工数据。存储数据到 ElasticSearch 的行为叫做索引 ,但在索引一个文档之前,需要确定将文档存 储在哪里。一个 ElasticSearch 集群可以 包含多个索引 ,相应的每个索引可以包含多个类型。这些不同的类型存储着多个文档 ,每个文档又有 多个 属性 。类似关系:索引-数据库类型-表文档-表中的记录 – 属性-列elasticsearch使用可以参早官方文档,在这里不在讲解。四、整合 elasticsearch创建项目 springboot-elasticsearch,引入web支持SpringBoot 提供了两种方式操作elasticsearch,Jest 和 SpringData。Jest 操作 elasticsearchJest是ElasticSearch的Java HTTP Rest客户端。ElasticSearch已经有一个Java API,ElasticSearch也在内部使用它,但是Jest填补了空白,它是ElasticSearch Http Rest接口缺少的客户端。1. pom.xml<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.gf</groupId> <artifactId>springboot-elasticsearch</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot-elasticsearch</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.1.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <dependency> <groupId>io.searchbox</groupId> <artifactId>jest</artifactId> <version>5.3.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>2. application.propertiesspring.elasticsearch.jest.uris=http://127.0.0.1:92003. Articlepackage com.gf.entity;import io.searchbox.annotations.JestId;public class Article { @JestId private Integer id; private String author; private String title; private String content; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public String toString() { final StringBuilder sb = new StringBuilder( “{"Article":{” ); sb.append( “"id":” ) .append( id ); sb.append( “,"author":"” ) .append( author ).append( ‘"’ ); sb.append( “,"title":"” ) .append( title ).append( ‘"’ ); sb.append( “,"content":"” ) .append( content ).append( ‘"’ ); sb.append( “}}” ); return sb.toString(); }}4. springboot测试类package com.gf;import com.gf.entity.Article;import io.searchbox.client.JestClient;import io.searchbox.core.Index;import io.searchbox.core.Search;import io.searchbox.core.SearchResult;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.io.IOException;@RunWith(SpringRunner.class)@SpringBootTestpublic class SpringbootElasticsearchApplicationTests { @Autowired JestClient jestClient; @Test public void createIndex() { //1. 给ES中索引(保存)一个文档 Article article = new Article(); article.setId( 1 ); article.setTitle( “好消息” ); article.setAuthor( “张三” ); article.setContent( “Hello World” ); //2. 构建一个索引 Index index = new Index.Builder( article ).index( “gf” ).type( “news” ).build(); try { //3. 执行 jestClient.execute( index ); } catch (IOException e) { e.printStackTrace(); } } @Test public void search() { //查询表达式 String query = “{\n” + " "query" : {\n” + " "match" : {\n” + " "content" : "hello"\n” + " }\n" + " }\n" + “}”; //构建搜索功能 Search search = new Search.Builder( query ).addIndex( “gf” ).addType( “news” ).build(); try { //执行 SearchResult result = jestClient.execute( search ); System.out.println(result.getJsonString()); } catch (IOException e) { e.printStackTrace(); } }}Jest的更多api ,可以参照github的文档:https://github.com/searchbox-io/JestSpringData 操作 elasticsearch1. application.propertiesspring.data.elasticsearch.cluster-name=elasticsearchspring.data.elasticsearch.cluster-nodes=127.0.0.1:93002. Bookpackage com.gf.entity;@Document( indexName = “gf” , type = “book”)public class Book { private Integer id; private String bookName; private String author; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getBookName() { return bookName; } public void setBookName(String bookName) { this.bookName = bookName; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } @Override public String toString() { final StringBuilder sb = new StringBuilder( “{"Book":{” ); sb.append( “"id":” ) .append( id ); sb.append( “,"bookName":"” ) .append( bookName ).append( ‘"’ ); sb.append( “,"author":"” ) .append( author ).append( ‘"’ ); sb.append( “}}” ); return sb.toString(); } }3. BookRepositorypackage com.gf.repository;import com.gf.entity.Book;import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;import java.util.List;public interface BookRepository extends ElasticsearchRepository<Book, Integer>{ List<Book> findByBookNameLike(String bookName);}4. springboot 测试类package com.gf;import com.gf.entity.Article;import com.gf.entity.Book;import com.gf.repository.BookRepository;import io.searchbox.client.JestClient;import io.searchbox.core.Index;import io.searchbox.core.Search;import io.searchbox.core.SearchResult;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.io.IOException;import java.util.List;@RunWith(SpringRunner.class)@SpringBootTestpublic class SpringbootElasticsearchApplicationTests { @Autowired BookRepository bookRepository; @Test public void createIndex2(){ Book book = new Book(); book.setId(1); book.setBookName(“西游记”); book.setAuthor( “吴承恩” ); bookRepository.index( book ); } @Test public void useFind() { List<Book> list = bookRepository.findByBookNameLike( “游” ); for (Book book : list) { System.out.println(book); } }}我们启动测试 ,发现报错 。这个报错的原因是springData的版本与我elasticsearch的版本有冲突,下午是springData官方文档给出的适配表。我们使用的springdata elasticsearch的 版本是3.1.3 ,对应的版本应该是6.2.2版本,而我们是的 elasticsearch 是 5.6.9,所以目前我们需要更换elasticsearch的版本为6.Xdocker pull elasticsearch:6.5.1docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 –name es02 镜像ID访问127.0.0.1:9200集群名为docker-cluster,所以我们要修改application.properties的配置了spring.data.elasticsearch.cluster-name=docker-clusterspring.data.elasticsearch.cluster-nodes=127.0.0.1:9300我们再次进行测试,测试可以通过了 。我们访问http://127.0.0.1:9200/gf/book/1,可以得到我们存入的索引信息。源码https://github.com/gf-huanchu…关注我的公众号,精彩内容不能错过~ ...

April 1, 2019 · 3 min · jiezi

Spring Boot 自定义starter

一、简介SpringBoot 最强大的功能就是把我们常用的场景抽取成了一个个starter(场景启动器),我们通过引入springboot 为我提供的这些场景启动器,我们再进行少量的配置就能使用相应的功能。即使是这样,springboot也不能囊括我们所有的使用场景,往往我们需要自定义starter,来简化我们对springboot的使用。二、如何自定义starter1.实例如何编写自动配置 ?我们参照@WebMvcAutoConfiguration为例,我们看看们需要准备哪些东西,下面是WebMvcAutoConfiguration的部分代码:@Configuration@ConditionalOnWebApplication@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class})@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})@AutoConfigureOrder(-2147483638)@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class})public class WebMvcAutoConfiguration { @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class}) @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class}) public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter { @Bean @ConditionalOnBean({View.class}) @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(2147483637); return resolver; } }}我们可以抽取到我们自定义starter时同样需要的一些配置。@Configuration //指定这个类是一个配置类@ConditionalOnXXX //指定条件成立的情况下自动配置类生效@AutoConfigureOrder //指定自动配置类的顺序@Bean //向容器中添加组件@ConfigurationProperties //结合相关xxxProperties来绑定相关的配置@EnableConfigurationProperties //让xxxProperties生效加入到容器中自动配置类要能加载需要将自动配置类,配置在META-INF/spring.factories中org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\模式我们参照 spring-boot-starter 我们发现其中没有代码:我们在看它的pom中的依赖中有个 springboot-starter<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId></dependency>我们再看看 spring-boot-starter 有个 spring-boot-autoconfigure<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId></dependency>关于web的一些自动配置都写在了这里 ,所以我们有总结:启动器starter只是用来做依赖管理需要专门写一个类似spring-boot-autoconfigure的配置模块用的时候只需要引入启动器starter,就可以使用自动配置了命名规范官方命名空间前缀:spring-boot-starter-模式:spring-boot-starter-模块名举例:spring-boot-starter-web、spring-boot-starter-jdbc自定义命名空间后缀:-spring-boot-starter模式:模块-spring-boot-starter举例:mybatis-spring-boot-starter三、自定义starter实例我们需要先创建两个工程 hello-spring-boot-starter 和 hello-spring-boot-starter-autoconfigurer1. hello-spring-boot-starter1.pom.xml<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.gf</groupId> <artifactId>hello-spring-boot-starter</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>hello-spring-boot-starter</name> <!– 启动器 –> <dependencies> <!– 引入自动配置模块 –> <dependency> <groupId>com.gf</groupId> <artifactId>hello-spring-boot-starter-autoconfigurer</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies></project>同时删除 启动类、resources下的文件,test文件。2. hello-spring-boot-starter-autoconfigurer1. pom.xml<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.gf</groupId> <artifactId>hello-spring-boot-starter-autoconfigurer</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>hello-spring-boot-starter-autoconfigurer</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!– 引入spring-boot-starter,所有starter的基本配合 –> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies></project>2. HelloPropertiespackage com.gf.service;import org.springframework.boot.context.properties.ConfigurationProperties;@ConfigurationProperties(prefix = “gf.hello”)public class HelloProperties { private String prefix; private String suffix; public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getSuffix() { return suffix; } public void setSuffix(String suffix) { this.suffix = suffix; }}3. HelloServicepackage com.gf.service;public class HelloService { HelloProperties helloProperties; public HelloProperties getHelloProperties() { return helloProperties; } public void setHelloProperties(HelloProperties helloProperties) { this.helloProperties = helloProperties; } public String sayHello(String name ) { return helloProperties.getPrefix()+ “-” + name + helloProperties.getSuffix(); }}4. HelloServiceAutoConfigurationpackage com.gf.service;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration@ConditionalOnWebApplication //web应该生效@EnableConfigurationProperties(HelloProperties.class)public class HelloServiceAutoConfiguration { @Autowired HelloProperties helloProperties; @Bean public HelloService helloService() { HelloService service = new HelloService(); service.setHelloProperties( helloProperties ); return service; }}5. spring.factories在 resources 下创建文件夹 META-INF 并在 META-INF 下创建文件 spring.factories ,内容如下:# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.gf.service.HelloServiceAutoConfiguration到这儿,我们的配置自定义的starter就写完了 ,我们hello-spring-boot-starter-autoconfigurer、hello-spring-boot-starter 安装成本地jar包。三、测试自定义starter我们创建个项目 hello-spring-boot-starter-test,来测试系我们写的stater。1. pom.xml<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.gf</groupId> <artifactId>hello-spring-boot-starter-test</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>hello-spring-boot-starter-test</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!– 引入自定义starter –> <dependency> <groupId>com.gf</groupId> <artifactId>hello-spring-boot-starter</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>2. HelloControllerpackage com.gf.controller;import com.gf.service.HelloService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class HelloController { @Autowired HelloService helloService; @GetMapping("/hello/{name}”) public String hello(@PathVariable(value = “name”) String name) { return helloService.sayHello( name + " , " ); }}3. application.propertiesgf.hello.prefix = higf.hello.suffix = what’s up man ?我运行项目访问 http://127.0.0.1:8080/hello/zhangsan,结果如下:hi-zhangsan , what’s up man ? 源码下载: https://github.com/gf-huanchupk/SpringBootLearning关注我的公众号,精彩内容不能错过~ ...

April 1, 2019 · 2 min · jiezi

Spring Security项目Spring MVC开发RESTful API(二)

查询请求常用注解@RestController 标明此Controller提供RestAPI@RequestMapping 映射http请求url到java方法@RequestParam 映射请求参数到java方法到参数@PageableDefault 指定分页参数默认值编写一个简单的UserController类@RestController@RequestMapping(value = “/user”)public class UserController { @RequestMapping(method = RequestMethod.GET) public List<User> query(@RequestParam(name = “username”,required = true) String username, @PageableDefault(page = 1,size = 20,sort = “username”,direction = Sort.Direction.DESC)Pageable pageable){ System.out.println(pageable.getSort()); List<User>users=new ArrayList<>(); users.add(new User(“aaa”,“111”)); users.add(new User(“bbb”,“222”)); users.add(new User(“ddd”,“333”)); return users; }}@PageableDefault SpingData分页参数 page当前页数默认0开始 sizi每页个数默认10 sort 排序Srping boot 测试用例在demo的pom.xml里面引入spirngboot的测试 <!–spring测试框架–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>测试/user接口@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //测试用例 @Test public void whenQuerSuccess() throws Exception { String result=mockMvc.perform(MockMvcRequestBuilders.get("/user") //传过去的参数 .param(“username”,“admin”) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回的集合的长度是否是3 .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(3)) //打印信息 .andDo(MockMvcResultHandlers.print()) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }jsonPath文档语法查询地址用户详情请求常用注解@PathVariable 映射url片段到java方法参数@JsonView 控制json输出内容实体对象@NoArgsConstructor@AllArgsConstructorpublic class User { public interface UserSimpleView{}; public interface UserDetailView extends UserSimpleView{}; private String username; private String password; @JsonView(UserSimpleView.class) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @JsonView(UserDetailView.class) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; }}Controller类@RestController@RequestMapping(value = “/user”)public class UserController { @RequestMapping(value = “/{id:\d+}",method = RequestMethod.GET) // 正则表达式 :\d+ 表示只能输入数字 //用户名密码都显示 @JsonView(User.UserDetailView.class) public User userInfo(@PathVariable String id){ User user=new User(); user.setUsername(“tom”); return user; }}测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户详情用例 @Test public void whenUserInfoSuccess() throws Exception { String result=mockMvc.perform(MockMvcRequestBuilders.get("/user/1”) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回到username是不是tom .andExpect(MockMvcResultMatchers.jsonPath("$.username").value(“tom”)) //打印信息 .andDo(MockMvcResultHandlers.print()) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }}用户处理创建请求常用注解@RequestBody 映射请求体到java方法到参数@Valid注解和BindingResult验证请求参数合法性并处理校验结果实体对象@NoArgsConstructor@AllArgsConstructorpublic class User { public interface UserSimpleView{}; public interface UserDetailView extends UserSimpleView{}; private String id; private String username; //不允许password为null @NotBlank private String password; private Date birthday; @JsonView(UserSimpleView.class) public String getId() { return id; } public void setId(String id) { this.id = id; } @JsonView(UserSimpleView.class) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @JsonView(UserDetailView.class) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @JsonView(UserSimpleView.class) public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; }}Controller类 @RequestMapping(method = RequestMethod.POST) @JsonView(User.UserSimpleView.class) //@Valid启用校验password不允许为空 public User createUser(@Valid @RequestBody User user, BindingResult errors){ //如果校验有错误是true并打印错误信息 if(errors.hasErrors()){ errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage())); } System.out.println(user.getUsername()); System.out.println(user.getPassword()); System.out.println(user.getBirthday()); user.setId(“1”); return user; }测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户创建用例 @Test public void whenCreateSuccess() throws Exception { Date date=new Date(); String content="{"username":"tom","password":null,"birthday":"+date.getTime()+"}"; String result=mockMvc.perform(MockMvcRequestBuilders.post("/user") .content(content) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回到username是不是tom .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(“1”)) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }}修改和删除请求验证注解| 注解 | 解释 | | ——– | ——– | | @NotNull | 值不能为空 | | @Null | 值必须为空 | | @Pattern(regex=) | 字符串必须匹配正则表达式 | | @Size(min=,max=) | 集合的元素数量必须在min和max之间 | | @Email | 字符串必须是Email地址 | | @Length(min=,max=) | 检查字符串长度 | | @NotBlank | 字符串必须有字符 | | @NotEmpty | 字符串不为null,集合有元素 | | @Range(min=,max=) | 数字必须大于等于min,小于等于max | | @SafeHtml | 字符串是安全的html | | @URL | 字符串是合法的URL | | @AssertFalse | 值必须是false | | @AssertTrue | 值必须是true | | @DecimalMax(value=,inclusive) | 值必须小于等于(inclusive=true)/小于(inclusive=false) value指定的值 | | @DecimalMin(value=,inclusive) | 值必须大于等于(inclusive=true)/大于(inclusive=false) value指定的值 | | @Digits(integer=,fraction=) | integer指定整数部分最大长度,fraction小数部分最大长度 | | @Future | 被注释的元素必须是一个将来的日期 | | @Past | 被注释的元素必须是一个过去的日期 | | @Max(value=) | 值必须小于等于value值 | | @Min(value=) | 值必须大于等于value值 |自定义注解修改请求实体对象@NoArgsConstructor@AllArgsConstructorpublic class User { public interface UserSimpleView{}; public interface UserDetailView extends UserSimpleView{}; private String id; //自定义注解 @MyConstraint(message = “账号必须是tom”) private String username; //不允许password为null @NotBlank(message = “密码不能为空”) private String password; //加验证生日必须是过去的时间 @Past(message = “生日必须是过去的时间”) private Date birthday; @JsonView(UserSimpleView.class) public String getId() { return id; } public void setId(String id) { this.id = id; } @JsonView(UserSimpleView.class) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @JsonView(UserDetailView.class) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @JsonView(UserSimpleView.class) public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; }}Controller类 @RequestMapping(value = “/{id:\d+}",method = RequestMethod.PUT) @JsonView(User.UserSimpleView.class) //@Valid启用校验password不允许为空 public User updateUser(@Valid @RequestBody User user, BindingResult errors){ //如果校验有错误是true并打印错误信息 if(errors.hasErrors()){ errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage())); } System.out.println(user.getUsername()); System.out.println(user.getPassword()); System.out.println(user.getBirthday()); user.setId(“1”); return user; }测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户修改用例 @Test public void whenUpdateSuccess() throws Exception { //当前时间加一年 Date date = new Date(LocalDateTime.now().plusYears(1).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); String content = “{"id":"1","username":"44","password":null,"birthday":” + date.getTime() + “}”; String result = mockMvc.perform(MockMvcRequestBuilders.put("/user/1”) .content(content) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()) //判断返回到username是不是tom .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(“1”)) .andReturn().getResponse().getContentAsString(); //打印返回结果 System.out.println(result); }自定义注解MyConstraint类import javax.validation.Constraint;import javax.validation.Payload;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;//作用在字段跟方法上面@Target({ElementType.FIELD,ElementType.METHOD})//运行时注解@Retention(RetentionPolicy.RUNTIME)//需要校验注解的类@Constraint(validatedBy = MyConstraintValidator.class)public @interface MyConstraint { String message() default “{org.hibernate.validator.constraints.NotBlank.message}”; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};}MyConstraintValidator类import javax.validation.ConstraintValidator;import javax.validation.ConstraintValidatorContext;//范型1.验证的注解 2.验证的数据类型public class MyConstraintValidator implements ConstraintValidator<MyConstraint,Object> { @Override public void initialize(MyConstraint myConstraint) { //校验器初始化的规则 } @Override public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { //校验username如果是tom验证通过 if (value.equals(“tom”)){ return true; }else{ return false; } }}删除请求Controller类 @RequestMapping(value = “/{id:\d+}",method = RequestMethod.DELETE) //@Valid启用校验password不允许为空 public void deleteUser(@PathVariable String id){ System.out.println(id); }测试用例@RunWith(SpringRunner.class) //运行器@SpringBootTestpublic class UserControllerTest { @Autowired private WebApplicationContext wac; private MockMvc mockMvc; @Before public void stup(){ mockMvc= MockMvcBuilders.webAppContextSetup(wac).build(); } //用户删除用例 @Test public void whenDeleteSuccess() throws Exception { mockMvc.perform(MockMvcRequestBuilders.delete("/user/1”) .contentType(MediaType.APPLICATION_JSON_UTF8)) //判断请求的状态吗是否成功,200 .andExpect(MockMvcResultMatchers.status().isOk()); }服务异常处理把BindingResult errors去掉 @RequestMapping(method = RequestMethod.POST) @JsonView(User.UserSimpleView.class) //@Valid启用校验password不允许为空 public User createUser(@Valid @RequestBody User user){ //如果校验有错误是true并打印错误信息// if(errors.hasErrors()){// errors.getAllErrors().stream().forEach(error -> System.out.println(error.getDefaultMessage()));// } System.out.println(user.getUsername()); System.out.println(user.getPassword()); System.out.println(user.getBirthday()); user.setId(“1”); return user; }查看返回的异常信息处理状态码错误创建文件结构如下404错误将跳转对应页面RESTful API的拦截过滤器(Filter)创建filter文件@Componentpublic class TimeFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println(“TimeFilter init”); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println(“TimeFilter doFilter”); long start=new Date().getTime(); filterChain.doFilter(servletRequest,servletResponse); System.out.println(“耗时”+(new Date().getTime()-start)); } @Override public void destroy() { System.out.println(“TimeFilter destroy”); }}自定义filter需要吧filter文件@Component标签去除@Configurationpublic class WebConfig { @Bean public FilterRegistrationBean timeFilterRegistration(){ FilterRegistrationBean registration=new FilterRegistrationBean(); TimeFilter timeFilter=new TimeFilter(); registration.setFilter(timeFilter); //filter作用的地址 List<String>urls=new ArrayList<>(); urls.add("/user"); registration.setUrlPatterns(urls); return registration; }}拦截器(Interceptor)创建Interceptor文件@Componentpublic class TimeInterceptor implements HandlerInterceptor { //控制器方法调用之前 @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { System.out.println(“preHandle”); System.out.println(“进入方法”+((HandlerMethod)o).getMethod().getName()); httpServletRequest.setAttribute(“startTime”,new Date().getTime()); //是否调用后面的方法调用是true return true; } //控制器方法被调用 @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { System.out.println(“postHandle”); Long start= (Long) httpServletRequest.getAttribute(“startTime”); System.out.println(“time interceptor耗时”+(new Date().getTime()-start)); } //控制器方法完成之后 @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { System.out.println(“afterCompletion”); System.out.println(“exception is”+e); }}把过滤器添加到webconfig文件@Configurationpublic class WebConfig extends WebMvcConfigurerAdapter { @Autowired private TimeInterceptor timeInterceptor; //过滤器 @Bean public FilterRegistrationBean timeFilterRegistration(){ FilterRegistrationBean registration=new FilterRegistrationBean(); TimeFilter timeFilter=new TimeFilter(); registration.setFilter(timeFilter); //filter作用的地址 List<String>urls=new ArrayList<>(); urls.add("/user/"); registration.setUrlPatterns(urls); return registration; } //拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(timeInterceptor); }}切片(Aspect)@Aspect@Componentpublic class TimeAspect { //@Befor方法调用之前 //@After()方法调用 //@AfterThrowing方法调用之后 //包围,覆盖前面三种 @Around(“execution( com.guosh.web.controller.UserController.(..))”)//表达式表示usercontroller里所有方法其他表达式可以查询切片表达式 public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable { System.out.println(“time aspect start”); //可以获取到传入参数 Object[]args=pjp.getArgs(); for (Object arg: args) { System.out.println(“arg is”+arg); } long start=new Date().getTime(); //相当于filter里doFilter方法 Object object=pjp.proceed(); System.out.println(“time aspect耗时”+(new Date().getTime()-start)); System.out.println(“time aspect end”); return object; }}总结过滤器Filter :可以拿到原始的http请求与响应信息拦截器Interceptor :可以拿到原始的http请求与响应信息还可以拿到处理请求方法的信息切片Aspect :可以拿到方法调用传过来的值使用rest方式处理文件服务返回的上传文件后路径对象在application.yml里添加上传地址#上传文件路径uploadfiledir: filePath: /Users/shaohua/webapp/guoshsecurity@Data@NoArgsConstructor@AllArgsConstructorpublic class FileInfo { private String path;}@RestController@RequestMapping("/file")public class FileController { @Value("${uploadfiledir.filePath}") private String fileDataStorePath;//文件上传地址 @RequestMapping(method = RequestMethod.POST) public FileInfo upload(@RequestParam(“file”) MultipartFile file) throws IOException { //文件名 System.out.println(file.getOriginalFilename()); //文件大小 System.out.println(file.getSize()); //获取文件后缀名 String ext=StringUtils.getFilenameExtension(file.getOriginalFilename()); File fileDir = new File(fileDataStorePath); //判断是否创建目录 if (!fileDir.exists()) { if (!fileDir.mkdirs() || !fileDir.exists()) { // 创建目录失败 throw new RuntimeException(“无法创建目录!”); } } File localFile=new File(fileDataStorePath, UUID.randomUUID().toString().replace("-", “”)+"."+ext); file.transferTo(localFile); //返回上传的路径地址 return new FileInfo(localFile.getAbsolutePath()); } //下载文件 @RequestMapping(value ="/{id}" ,method = RequestMethod.GET) public void download(@PathVariable String id, HttpServletResponse response){ //模拟下载直接填好了下载文件名称 try(InputStream inputStream = new FileInputStream(new File(fileDataStorePath,“13a2c075b7f44025bbb3c590f7f372eb.txt”)); OutputStream outputStream=response.getOutputStream();){ response.setContentType(“application/x-download”); response.addHeader(“Content-Disposition”,“attachment;filename="+“13a2c075b7f44025bbb3c590f7f372eb.txt"”); IOUtils.copy(inputStream,outputStream); } catch (Exception e) { e.printStackTrace(); } }}使用Swagger工具在demo模块引入 <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>添加swagger的配置类@Configuration@EnableSwagger2public class Swagger2Config { @Value("${sys.swagger.enable-swgger}”) private Boolean enableSwgger; @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .enable(enableSwgger) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage(“com.guosh.web”)) //swgger插件作用范围 //.paths(PathSelectors.regex("/api/.")) .paths(PathSelectors.any()) //过滤接口 .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(“SpringSecurityDemo”) //标题 .description(“API描述”) //描述 .contact(new Contact(“guoshaohua”, “http://www.guoshaohua.cn”, “”))//作者 .version(“1.0”) .build(); }}常用注解通过@Api用于controller类上对类的功能进行描述通过@ApiOperation注解用在controller方法上对类的方法进行描述通过@ApiImplicitParams、@ApiImplicitParam注解来给参数增加说明通过@ApiIgnore来忽略那些不想让生成RESTful API文档的接口通过@ApiModel 用在返回对象类上描述返回对象的意义通过@ApiModelProperty 用在实体对象的字段上 用于描述字段含义 ...

March 30, 2019 · 6 min · jiezi

springboot+fescar注意事项

前言最近开始学习分布式的开发,需要用到分布式事务的处理,于是学习了阿里的分布式事务框架fescar。由于官方的文档尚未完全完成,看官方给的sample的话又不知道哪些配置是必须的,因此在写自己的demo的时候,遇到了不少问题。项目框架SpringBoot+Druid+MybatisPlus+Fescar+Dubbo+Nacos官方样例https://github.com/fescar-gro…我的demo:https://github.com/ksyzz/fesc…准备工作1,开发前需要启动nacos注册中心和fescar server。2,将sample中的file.conf和registry.conf放到自己的resource目录下。3,在对应的数据库中,建表’undo_log’,该表是fescar的AT模式需要用的数据表。CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) NOT NULL, xid varchar(100) NOT NULL, rollback_info longblob NOT NULL, log_status int(11) NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, ext varchar(100) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid,branch_id)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT=‘fescar AT模式表’;问题与处理日志没有显示连接fescar server的信息: 项目正常配置后,日志会显示连接fescar server的信息,例如:c.a.f.core.rpc.netty.RmRpcClient - will connect to 127.0.0.1:8091。说明fescar的配置没有生效。需要在 @SpringBootApplication中添加scanBasePackages 的信息: @SpringBootApplication(scanBasePackages = “com.ksyzz.common.config”)。这个问题偶尔出现,正常情况下不加scanBasePackages也可以使用(–懵逼脸–)报错:ERROR c.a.f.core.rpc.netty.TmRpcClient : no available server to connect. 出现该问题,首先检查fescar server是否启动,如果已经启动,那么检查下面两个配置是否一致: file.conf中,service{vgroup_mapping.xxx=“localRgoup”}和创建GlobalTransactionScanner的bean的代码中的return new GlobalTransactionScanner(applicationName, “xxx”);两个位置的xxx应该一致。分布式事务不生效 在项目启动后可以正常连接fescar server后,执行@GlobalTransactional的方法,抛出异常后,发现事务并没有回滚。出现这种情况一般是没有配置fescar的DataSourceProxy,必须使用fescar的DataSourceProxy,才可以正常的执行全局事务回滚的操作。比如mybatis,则需要手动注册SqlSessionFactory的bean,并将其中的DataSource替换为fescar的DataSourceProxy,这样才会生效。 ...

March 28, 2019 · 1 min · jiezi

起早贪黑几个月,我写完了人生第一本书!

今天有小伙伴在网上问了我一个问题:写书的整个过程是什么感受?想想我好像还没各位小伙伴聊过我写书的故事,只是在书出版后做过一次送书活动,其他的好像就没分享啥了,今天我想借这个机会和大伙聊聊我写书的故事,也希望我的经验能帮助到各位小伙伴。1.缘起故事得从我大学毕业时候说起啦。大四第一学期忙着准备考研,错过了秋招,然而研究生也没考上,过完年研究生考试成绩出来后,一看不行就赶紧出来找工作,西北农村娃,不敢耗也耗不起。大学所在的城市高校不多,所以春招的时候我回到了老家西安参加春招,花了一个多礼拜,拿了三个offer,感觉差不多了又急匆匆返回学校,返校后,回忆找工作的过程,有得意也有失落,得意的是没想到找工作这么顺利,失落的是想去的公司没去成,我的学校还被一些中等公司鄙视了。 我本科学位是管理学学士,计算机是我从大二开始自学的,自学了JavaEE和Android,当时找工作时候,招Android的,招Java的我都去面,3个offer有两个是Java,一个Android。虽然找工作整体上感觉不错,不过还是有一些不如意的地方,有一个超级大的大厂,过了笔试,也过了两轮技术面,止步于最后一轮人事面,这算是找工作期间最遗憾的一件事了。也有一些不怎么大的厂,却歧视我的学校(某末流211),这让我忿忿不平,但是学校也没法改变,思来想去,决定写博客,提高自己的技术影响力,弥补专业和学校的不足,就这样,在CSDN上注册了博客账号,当年4月15号发表了第一篇博客,从此打开了一扇新大门。 博客写了一段时间,CSDN的运营梦鸽美女邀请我申请博客专家,有了title写的就更有劲了。写博客的过程中,感觉自身的技术也在不断的提高,因为刚开始学一个新技术点的时候,很多东西没太关注,只会用,没细究,写博客则是一个整理的过程,是自己思维一个锻炼的过程,博客写完了,感觉对相关知识点的认知又上升了。 刚开始写的时候,博客的访问量并不高,好在我当时也是刚毕业,不着急,就慢慢的写着,就这样,第二年刚过完年就开始有人找我写书,被我婉拒了,我的理由是刚刚毕业半年,实在没啥好写的,也不知道该写啥。不过我却发现写书好像没那么难,好像很容易,因为竟然有人找我写书。再之后,隔三差五就会有出版社的编辑找来了,电子工业出版社、人民邮电出版社、清华大学出版社等等,不过我自己从来没下定决心,虽然心里也有想法,但是总觉得还差点火候。 2018年刚过完年,那时候我搞Spring Boot+Vue也有一段时间了,自我感觉积累了一点点料,有种想和大伙分享的欲望,另一方面也觉得该为自己的职业生涯留下一点东西,不能就这么默默无闻的搬一辈子砖,在认真考虑后,决定写一本Spring Boot相关的书,刚好清华社的夏老师没过几天就加了我微信,于是一拍即合。 这是写书的第一步,先有技术积累,有博客或者公众号,圈子里有一点点名气,就会有出版社的编辑找来,因为出版社编辑比较喜欢那种在某一领域深耕多年,对相关技术有自己的看法和认识,有原创的博客,并且博客写作思路清晰,文章脉络清楚的作者。在这个阶段我觉得最难的还是坚持,写博客积累技术和名气并非一朝一夕的事,有一些超级大牛,抓住了技术的风口一下就积累了很多的关注,刚入行的小辈看到这些大牛的博客,感觉达到这样的高度太难了,所以放弃了。其实很多时候,你不用成为执牛耳的大牛,成为一个小小的小牛,就够了。 这一阶段,总结两个字:坚持。2.写作在答应了出版社的邀请之后,就着手开始准备了。在刚开始答应的时候,需要提交一个图书选题单给出版社的老师,选题单中会列出书名,章节,作者等信息。 选题定下来之后,先和出版社签订出版合同,合同中会约定图书字数、作者、稿费计算方式等,签好合同后,和出版社的事情暂时就先告一段落了。 接下来就开始写了,细化每个章节的目录,每章大概写多少,准备写哪些内容,提纲细化之后,后面基本就不动了,主要是填内容进去。写书和写博客不一样,博客,我只需要介绍某一个知识点,解决某一个问题就行了,写书,不仅要介绍知识点解决问题,还要讲究知识点的全面,不能有遗漏,很多东西,我们可能经常用某一种方式实现,但实际上换一种方式也能实现,但是你可能就不知道,关键是你并不知道他还有另一种实现方式,这就很累了,为了不遗漏知识点,只能把官方文档反复看。有的时候卡在某一个技术点上,上班时候脑子里都是相关问题,一有解决思路就赶紧先记下来,回家后赶紧尝试。在写书之前,我在公众号上已经陆续发了Redis系列教程、MongoDB系列教程、Spring Cloud系列教程以及Git系列教程等,因此在写Spring Boot时,遇到这几方面的问题基本上都能得心应手,也算是前期准备比较充分吧(其实写这些教程的时候压根就没想到写书的事,但是掌握了,写出来的技术,总会在某一天发挥作用的)。 写书期间最大的挑战还不是来自技术,而是自信,有的时候写着写着甚至会怀疑自己,这书写出来有人看吗?但是合同签了,没人看也得硬着头皮写下去,而且得认真写。有时候一些出版的问题要和编辑老师沟通,沟通完后,又会信心满满,这一点,还是要感谢出版社编辑老师的鼓励。我自己因为不爱交流,很多问题喜欢自己瞎琢磨,其实很多出版方面的问题都可以和编辑及时沟通,避免给自己徒增烦恼(这个建议送给想要写书的小伙伴)。 那一段时间,我每天早上7点起床,写到8点半然后去上班,晚上6点下班后,差不多7点开始写,写到11点半,周末写两天,拒掉了大部分的社交活动,差不多就这样连续了几个月,交稿的时候有种高考考完的感觉,有的小伙伴可能觉得我是个假程序员,竟然不加班,老实说,敝司确实不怎么加班。稿子交到出版社之后,还要经过排版->编辑->改错->初审->复审->终审->发稿->申请书号、CIP->封面设计->出片->下厂印制->发样书->入库->上市销售,整个过程大约持续了三个多月。封面设计时候,出版社给了两个参考的封面,纠结了半天,后来选择了绿色的(可能有小伙伴要吐槽我的审美了): 关于封面这里,也可以自己提一些设计思路给出版社去做,不过我最终还是选择了出版社的方案,想想民国时那些自己给自己设计图书封面的大佬,真是佩服的五体投地。关于书的定价,也是出版社给一个参考范围,作者自己选,现在技术图书的定价基本都是按照印张来的,作者选择的范围不大,除非是超级超级大牛,可能会额外照顾(我瞎猜的)。 到了2019年1月份的时候,有一天午休醒来,有个人加我微信,备注说是读者,我才发现书已经上市销售了,至此,2018年的工程,总算告一段落了,几个月起早贪黑,甚至打了退堂鼓,还好最终没有放弃,总算有了收货。 这一阶段的总结:不要怂,就是干。3.收获图书出版后,感觉收获还是蛮大的。从以下三个方面来跟大伙聊聊:技术首先就是技术了,写书是一个非常非常系统化的工程,虽然我以前也写过多个成体系的教程,但是感觉和写书还是有很大的不同,写书的过程,也是重新梳理自己知识体系的过程,对于以前不求甚解的东西都去认真研究了,还要想办法将一些复杂的东西写的浅显易懂,让读者容易上手。在不断的锤炼中,自己的技术也得到了极大的提高。信心由于我并非科班出身,有幸在这个行业混口饭吃其实已经很满足了,计算机理论捉襟见肘,虽然我一直在努力弥补,但总是不够自信。这本书一定程度上让我更有信心在这个圈子里混下去。圈子我自己平时不怎么出去玩,比较宅,线下的圈子不多,线上的圈子倒不少,但是很多人都是听其名,不知其人。书出版之后,加入的第一个圈子就是华为云享专家,在华为云组织的openday中,认识了很多大佬,很多人名字和人终于对上了,自己也收获了很多。还有一些由于时间原因被我推掉的活动,但总体感觉就是活动多了。其实这就是我自己一向所说的,提高自己才是最重要的,与其削尖了脑袋挤进某一个圈子,不如修炼内功,时间到了,该有的就有了。4.一点建议其实经常会有一些读者在后台联系我,有刚毕业的大学生,也有在读的研究生,他们想知道在技术的道路上要如何选择,CC++Java前端,都会,但是却不精通,这里我给的建议就是苍蝇模式,因为我一开始也是自学的,我相信我曾经遇到的困惑也有后来者会遇到,那么什么是苍蝇模式呢?美国密歇根大学教授卡尔·韦克做过这样一个实验: 把一群蜜蜂和一群苍蝇同时装进一个玻璃瓶里,将瓶子横着放平,让瓶底朝着光,小蜜蜂们会一刻不停地在瓶底附近飞舞,因为蜜蜂的复眼有更强的向光性,对阳光的敏感和偏执决定它们不肯接近黑暗的地方,哪怕是出口,蜜蜂一次次撞到瓶底,直到力竭而死,而苍蝇则在瓶子里乱撞,不一会儿,就能从瓶口逃之夭夭。刚入行可以多了解、多打听、多去尝试慢慢找到适合自己的,自己喜欢的,选定了方向之后,就可以开始做技术积累了,积累可以从写博客开始,初期建议选个大平台,例如博客园、CSDN或者慕课网之类的,有了名气之后,可以考虑独立建站或者写公众号,慢慢打造个人品牌,个人品牌建立了,写书就是愿不愿意的事了。其实,事儿不难,难在坚持! 最后,请大伙关注我的公众号牧码小子,公众号后台回复Java,领取松哥为大家精心准备的Java干货!

March 27, 2019 · 1 min · jiezi

Java学习笔记(八)——数据校验(Hibernate validation)

公司转java开发也有一段时间了,在实际开发过程中还是会遇到一些问题的,本篇主要记录下接口服务中参数验证相关的开发过程和一些知识点。在接口服务开发中,难免会校验传入方的参数校验,尤其在post请求时,验证字符长度,字符类型是否满足数据库中字段的最大长度及类型,如果不符合条件应及时拦截并返回,避免后续的流程。hibernate validator constraint 注解先了解下提供的注解,基本上常用的都提供了,在代码编写时还是比较方便的,一个注解解决了验证逻辑。/Bean Validation 中内置的 constraint/@Null //被注释的元素必须为 null @NotNull //被注释的元素必须不为 null @AssertTrue //被注释的元素必须为 true @AssertFalse //被注释的元素必须为 false @Min(value) //被注释的元素必须是一个数字,其值必须大于等于指定的最小值 @Max(value) //被注释的元素必须是一个数字,其值必须小于等于指定的最大值 @DecimalMin(value) //被注释的元素必须是一个数字,其值必须大于等于指定的最小值 @DecimalMax(value) //被注释的元素必须是一个数字,其值必须小于等于指定的最大值 @Size(max=, min=) //被注释的元素的大小必须在指定的范围内 @Digits (integer, fraction) //被注释的元素必须是一个数字,其值必须在可接受的范围内 @Past //被注释的元素必须是一个过去的日期 @Future //被注释的元素必须是一个将来的日期 @Pattern(regex=,flag=) //被注释的元素必须符合指定的正则表达式 /Hibernate Validator 附加的 constraint// @NotBlank(message =) //验证字符串非null,且长度必须大于0 @Email //被注释的元素必须是电子邮箱地址 @Length(min=,max=) //被注释的字符串的大小必须在指定的范围内 @NotEmpty //被注释的字符串的必须非空 @Range(min=,max=,message=) //被注释的元素必须在合适的范围内简单使用验证字段添加需要的注解 /** * 订单号 / @Range(min=1,message = “不是正确的订单号”) private Long e_order_id; /* * 产品code / @NotBlank(message = “不是正确的产品code”) private String product_code;BindingResult接收在controller中,我们通过BindingResult来接收对应的验证信息 @ApiOperation(value = “修改订单状态”, notes = “若找不到结果则返回 null。”) @RequestMapping(value = “/status”, method = RequestMethod.PUT) @ResponseBody public String PutOrderStatus(@RequestBody @Validated @NotNull OrderStatusReq req, BindingResult bindingResult) { String validResult = assertParameterValid(bindingResult); if (validResult != null) { return validResult; } return iOrderStatusService.putOrderStatus(req).toString(); }protected String assertParameterValid(BindingResult bindingResult) { if (bindingResult.hasErrors()) { FieldError error = bindingResult.getFieldError(); return new Response<>(BusinessReturnCode.VALIDATION_FAILURE, String.format("[%s] %s.", error.getField(), error.getDefaultMessage()), null).toString(); } return null; }枚举Enum校验可惜的是,Hibernate validation中没有提供枚举相关的校验,而实际业务场景中会有很多校验类型、状态等,这里我们只能自定义了。首先我们需要自定义一个annotation来标记你的验证字段,因为Validator框架里面的基础annotation已经不够用。然后自定义一个Validator(继承ConstraintValidator),并将annotation类型给到ConstraintValidator的泛型列表,相当于做了一个绑定。然后implement ConstraintValidator的两个方法,在isValid方法里面用要验证的枚举验证参数。可以看下一个简单的demo:@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Constraint(validatedBy = {EnumValidAnnotation.EnumValidtor.class})@Documentedpublic @interface EnumValidAnnotation { String message() default “枚举不在范围内”; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; Class<?>[] target() default {}; public class EnumValidtor implements ConstraintValidator<EnumValidAnnotation, Integer> { Class<?>[] cls; //枚举类 @Override public void initialize(EnumValidAnnotation constraintAnnotation) { cls = constraintAnnotation.target(); } @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { System.out.println(“枚举值” + value); if (cls.length > 0) { for (Class<?> cl : cls) { try { if (cl.isEnum()) { //枚举类验证 Object[] objs = cl.getEnumConstants(); Method method = cl.getMethod(“getCode”); for (Object obj : objs) { Object code = method.invoke(obj); if (value.equals(code)) { return true; } } } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } return false; } }这样的话在你要验证的字段上加上对应的注解即可: /* * 更新类型 */ @EnumValidAnnotation(target = OrderStatusEnum.class) private int stype;总结java中注解还是挺有意思的,类似c#中的attribute,但java中各种框架、方法的注解真的很多,不一直使用或做对应的笔记真的很容易忘记,还是需要多多积累和记录的。 ...

March 26, 2019 · 2 min · jiezi

spring-boot下使用LogBack,使用HTTP协议将日志推送到日志服务器(二)

上文中,我们实现了将LogBack的日志信息实时的推送到日志服务器的功能。但实时进行推送,必然会增加日志服务器的压力。本文将阐述另一种定时推送的方法,以减轻日志服务器的压力。目标:每10秒钟统一推送这期间产生的日志示例代码地址https://github.com/mengyunzhi/springBootSampleCode/tree/master/log-back 开发环境: java1.8 + spring-boot:2.1.2定义application.properties为了与官方文档更加统一,在此,我们如下定义application.properties# URLlogback.loggly.endpointUrl=http://localhost:8081/log配置logback-spring.xml<?xml version=“1.0” encoding=“UTF-8”?><!–启用debug模式后,将在spring-boot 大LOG上方打印中logBack的配置信息–><configuration debug=“true” scan=“false” scanPeriod=“30 seconds”> <!–引入application配置信息–> <springProperty scope=“context” name=“logback.loggly.endpointUrl” source=“logback.loggly.endpointUrl” defaultValue=“localhost”/> <!–包含配置文件 org/springframework/boot/logging/logback/defaults.xml–> <include resource=“org/springframework/boot/logging/logback/defaults.xml”/> <!–定义变量LOG_FILE,值为${LO…}–> <property name=“LOG_FILE” value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/> <!–包含配置文件,该配置文件中,定义了 控制台日志是按什么规则,什么形式输出的–> <include resource=“org/springframework/boot/logging/logback/console-appender.xml”/> <!–包含配置文件,该配置文件中,定义了 文件日志是按什么规则,什么形式输出的–> <include resource=“org/springframework/boot/logging/logback/file-appender.xml”/> <!–引入第三方appender, 起名为http–> <appender name=“HTTP” class=“ch.qos.logback.ext.loggly.LogglyAppender”> <!–请求的地址–> <endpointUrl>${logback.loggly.endpointUrl}</endpointUrl> <!–定义过滤器–> <filter class=“com.mengyunzhi.sample.logback.Filter”/> <!–定义输出格式JSON–> <layout class=“ch.qos.logback.contrib.json.classic.JsonLayout”> <jsonFormatter class=“ch.qos.logback.contrib.jackson.JacksonJsonFormatter”> <prettyPrint>true</prettyPrint> </jsonFormatter> <timestampFormat>yyyy-MM-dd’ ‘HH:mm:ss.SSS</timestampFormat> </layout> </appender> <!–引用第三方appender, 起名为batchHttp–> <appender name=“batchHttp” class=“ch.qos.logback.ext.loggly.LogglyBatchAppender”> <endpointUrl>${logback.loggly.endpointUrl}</endpointUrl> <flushIntervalInSeconds>10</flushIntervalInSeconds> <!–定义输出格式JSON–> <layout class=“ch.qos.logback.contrib.json.classic.JsonLayout”> <jsonFormatter class=“ch.qos.logback.contrib.jackson.JacksonJsonFormatter”> <prettyPrint>true</prettyPrint> </jsonFormatter> </layout> </appender> <!–定义日志等级–> <root level=“INFO”> <!–启用第一个appender为CONSOLE, 该名称定义于org/springframework/boot/logging/logback/console-appender.xml中–> <appender-ref ref=“CONSOLE”/> <!–启用第二个appender为FILE, 该名称定义于org/springframework/boot/logging/logback/file-appender.xml中–> <appender-ref ref=“FILE”/> <!–启用第三个appender为HTTP–> <!–<appender-ref ref=“HTTP”/>–> <!–启用第4个appender为batchHttp–> <appender-ref ref=“batchHttp”/> </root></configuration>主要变更信息如下:为了更好的和官方同步,将变量名称由logUrl变更为:logback.loggly.endpointUrl引用了新的变量信息: logback.loggly.endpointUrl加了新的appender -> batchHttp启动测试启动service子模块,启动本项目。打开浏览器,并访问:http://localhost:8080/send发送方日志:2019-03-25 15:29:02.093 INFO 28934 — [nio-8080-exec-6] c.m.sample.logback.LogBackApplication : info2019-03-25 15:29:02.093 WARN 28934 — [nio-8080-exec-6] c.m.sample.logback.LogBackApplication : warn2019-03-25 15:29:02.093 ERROR 28934 — [nio-8080-exec-6] c.m.sample.logback.LogBackApplication : error2019-03-25 15:29:11.564 INFO 28934 — [nio-8080-exec-8] c.m.sample.logback.LogBackApplication : info2019-03-25 15:29:11.565 WARN 28934 — [nio-8080-exec-8] c.m.sample.logback.LogBackApplication : warn2019-03-25 15:29:11.565 ERROR 28934 — [nio-8080-exec-8] c.m.sample.logback.LogBackApplication : error挡收方日志2019-03-25 15:29:08.286 INFO 28428 — [nio-8081-exec-9] c.m.s.l.service.ServiceApplication : { “timestamp” : “1553498942093”, “level” : “INFO”, “thread” : “http-nio-8080-exec-6”, “logger” : “com.mengyunzhi.sample.logback.LogBackApplication”, “message” : “info”, “context” : “default”}{ “timestamp” : “1553498942093”, “level” : “WARN”, “thread” : “http-nio-8080-exec-6”, “logger” : “com.mengyunzhi.sample.logback.LogBackApplication”, “message” : “warn”, “context” : “default”}{ “timestamp” : “1553498942093”, “level” : “ERROR”, “thread” : “http-nio-8080-exec-6”, “logger” : “com.mengyunzhi.sample.logback.LogBackApplication”, “message” : “error”, “context” : “default”}2019-03-25 15:29:18.289 INFO 28428 — [nio-8081-exec-1] c.m.s.l.service.ServiceApplication : { “timestamp” : “1553498951564”, “level” : “INFO”, “thread” : “http-nio-8080-exec-8”, “logger” : “com.mengyunzhi.sample.logback.LogBackApplication”, “message” : “info”, “context” : “default”}{ “timestamp” : “1553498951565”, “level” : “WARN”, “thread” : “http-nio-8080-exec-8”, “logger” : “com.mengyunzhi.sample.logback.LogBackApplication”, “message” : “warn”, “context” : “default”}{ “timestamp” : “1553498951565”, “level” : “ERROR”, “thread” : “http-nio-8080-exec-8”, “logger” : “com.mengyunzhi.sample.logback.LogBackApplication”, “message” : “error”, “context” : “default”}我们发现:发送方于29分02秒时,生成了3条日志;接收方于29分08秒接收到了1条日志,该日志包括了上面3条日志信息。发送方于29分11秒时,生成了3条日志;接收方于29分18秒(距离上次接收的间隔为10秒)接收到了第2条日志,该日志包括了29分11秒时生成的3条日志信息。总结对于小白级别的我们而言,我们想到的,别人其它早就实现了。所以大多数的我们,最终比接是DEBUG的能力、看文档的能力、对软件工程理解的能力、对业务的掌控能力。而编码能力,则是基本功,没有它不行,只有它也不行。参考文档:https://github.com/qos-ch/logback-extensions/wiki/Loggly ...

March 25, 2019 · 2 min · jiezi

Spring Boot 2 - 初识与新工程的创建

Spring Boot的由来相信大家都听说过Spring框架。Spring从诞生到现在一直是流行的J2EE开发框架。随着Spring的发展,它的功能越来越强大,随之而来的缺点也越来越明显,以至于发展到后来变得越来越臃肿,使用起来也非常的麻烦。到后来由于过于强调配置的灵活性,有时即使只为了加入一个简单的特性,而需要相当多的XML配置,从而被人们诟病为"配置地狱"!后来许多优秀的服务端框架涌现出来,比如基于JavaScript的nodeJS,基于Python的Django,Flask,Tornado框架。都由于其使用简单的特性被越来越多的开发者采用。Sprint Boot就是为了应对这些框架的挑战而出现的,它彻底改变了Spring框架臃肿的现状。使得J2EE的框架变得简单起来,目前越来越多的公司和项目选择了它。Spring Boot最新的版本是2.x,本文我们就来介绍它的安装与配置,快速创建你的第一个Spring Boot工程,享受她的优雅与强大。Spring Boot的特性Spring Boot的主要有以下几个杀手级特性,可以大大减少学习与使用的复杂性,让我们更多地关注业务,提升开发效率:可创建独立可运行的应用程序,打包后仅一个jar包,运行即可。内置应用服务器Tomcat,Jetty等,无需部署。零XML配置,彻底摆脱"配置地狱"。自动配置各种第三方库,常用的第三方库引入即可用。内置各种服务监控系统,实时观察服务运行状态。创建Spring Boot工程我们废话不多说,现在就开始介绍创建Spring Boot 2工程的方法,这是进行Spring Boot学习与开发的第一步。方法一:通过Idea内置工具创建如果你使用IntelliJ IDEA作为你的开发IDE的话,这种方式最为方便,不过前提是使用Ultimate版(最终版),在IntelliJ的官网可以下载到(当然如果条件允许推荐购买正版)。打开Idea选择创建新工程选择导航栏中的Spring Initializr然后填入工程信息注意这里有使用Maven还是Gradle的选择。我们这里既然要零XML配置,这里选择使用Gradle工程,如图。我们使用Sprint Boot的目的也就是简化我们的开发生活,不是吗?添加第三方依赖我们这里添加需要的第三方依赖。如果你第一次接触Spring Boot,为了避免复杂性,可以选择添加以下两个依赖。其他的依赖不必担心,你可以在任何时候非常容易地添加依赖。DevTools:是一系列开发工具配置,比如热部署。Web: 对Web开发的基础支持。完成工程创建填入工程名和保存目录后,点击完成。创建完工程后,会有一个gradle配置的一个界面,这里我们选择使用默认的wrapper。这个选项会自动为我们下载对应版本的gradle进行配置和编译,无需我们自己安装配置等,非常方便。点击OK后我们就成功地创建了新工程!恭喜!方法二:通过Spring Initializr创建这种方式适用于不使用IntelliJ IDEA和使用免费版Idea的同学,通过官方创建Spring Boot工程的网站直接创建。方法一其实也是使用这个网站作为模板来集成到Idea中的。点击这里进入到这个网站(https://start.spring.io/)输入工程信息,并选择Gradle工程输入工程的信息后,如果需要更详细的信息设置,可以点击下方的"More options"按钮进行设置。添加依赖这里我们可以直接搜索需要的依赖进行添加,比如我们添加Web和Devtools库。生成工程在我们把所有信息填完后,接下来我们就可以点击页面底部的按钮(Generate Project)开始生成。生成后会自动把工程下载到本地,我们解压后,将该工程保存到开发目录(你喜欢的任何位置都可以),然后使用IDE打开即可。比如我这里使用的是IntelliJ IDEA,打开即可。运行工程!至此我们的工程已经创建完毕,下面就是运行它了。我们观察工程源码包的结构,发现有一个Hellospringboot2Application的类,这个类就是我们服务的运行入口。运行它后,我们的服务就可以正常启动了!总结通过创建Spring Boot新工程的过程,我们就会发现它的简洁之处,不会像以前使用Spring那样要花费很多时间和精力去创建和配置,我们现在甚至可以在短短的两分钟之内创建好工程!后面的文章我们会深入讨论Spring Boot的方方面面。我的博客中其他关于Spring Boot的所有文章可以点击这里找到,欢迎关注!如果有问题可以留言,或者给我发邮件lloyd@examplecode.cn,期待我们共同学习与成长!

March 19, 2019 · 1 min · jiezi

最简单的SpringBoot整合MyBatis教程

前面两篇文章和读者聊了Spring Boot中最简单的数据持久化方案JdbcTemplate,JdbcTemplate虽然简单,但是用的并不多,因为它没有MyBatis方便,在Spring+SpringMVC中整合MyBatis步骤还是有点复杂的,要配置多个Bean,Spring Boot中对此做了进一步的简化,使MyBatis基本上可以做到开箱即用,本文就来看看在Spring Boot中MyBatis要如何使用。工程创建首先创建一个基本的Spring Boot工程,添加Web依赖,MyBatis依赖以及MySQL驱动依赖,如下: 创建成功后,添加Druid依赖,并且锁定MySQL驱动版本,完整的依赖如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.28</version> <scope>runtime</scope></dependency>如此,工程就算是创建成功了。读者注意,MyBatis和Druid依赖的命名和其他库的命名不太一样,是属于xxx-spring-boot-stater模式的,这表示该starter是由第三方提供的。基本用法MyBatis的使用和JdbcTemplate基本一致,首先也是在application.properties中配置数据库的基本信息:spring.datasource.url=jdbc:mysql:///test01?useUnicode=true&characterEncoding=utf-8spring.datasource.username=rootspring.datasource.password=rootspring.datasource.type=com.alibaba.druid.pool.DruidDataSource配置完成后,MyBatis就可以创建Mapper来使用了,例如我这里直接创建一个UserMapper2,如下:public interface UserMapper2 { @Select(“select * from user”) List<User> getAllUsers(); @Results({ @Result(property = “id”, column = “id”), @Result(property = “username”, column = “u”), @Result(property = “address”, column = “a”) }) @Select(“select username as u,address as a,id as id from user where id=#{id}”) User getUserById(Long id); @Select(“select * from user where username like concat(’%’,#{name},’%’)”) List<User> getUsersByName(String name); @Insert({“insert into user(username,address) values(#{username},#{address})”}) @SelectKey(statement = “select last_insert_id()”, keyProperty = “id”, before = false, resultType = Integer.class) Integer addUser(User user); @Update(“update user set username=#{username},address=#{address} where id=#{id}”) Integer updateUserById(User user); @Delete(“delete from user where id=#{id}”) Integer deleteUserById(Integer id);}这里是通过全注解的方式来写SQL,不写XML文件,@Select、@Insert、@Update以及@Delete四个注解分别对应XML中的select、insert、update以及delete标签,@Results注解类似于XML中的ResultMap映射文件(getUserById方法给查询结果的字段取别名主要是向小伙伴们演示下@Results注解的用法),另外使用@SelectKey注解可以实现主键回填的功能,即当数据插入成功后,插入成功的数据id会赋值到user对象的id属性上。 UserMapper2创建好之后,还要配置mapper扫描,有两种方式,一种是直接在UserMapper2上面添加@Mapper注解,这种方式有一个弊端就是所有的Mapper都要手动添加,要是落下一个就会报错,还有一个一劳永逸的办法就是直接在启动类上添加Mapper扫描,如下:@SpringBootApplication@MapperScan(basePackages = “org.sang.mybatis.mapper”)public class MybatisApplication { public static void main(String[] args) { SpringApplication.run(MybatisApplication.class, args); }}好了,做完这些工作就可以去测试Mapper的使用了。mapper映射当然,开发者也可以在XML中写SQL,例如创建一个UserMapper,如下:public interface UserMapper { List<User> getAllUser(); Integer addUser(User user); Integer updateUserById(User user); Integer deleteUserById(Integer id);}然后创建UserMapper.xml文件,如下:<?xml version=“1.0” encoding=“UTF-8” ?><!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN” “http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace=“org.sang.mybatis.mapper.UserMapper”> <select id=“getAllUser” resultType=“org.sang.mybatis.model.User”> select * from t_user; </select> <insert id=“addUser” parameterType=“org.sang.mybatis.model.User”> insert into user (username,address) values (#{username},#{address}); </insert> <update id=“updateUserById” parameterType=“org.sang.mybatis.model.User”> update user set username=#{username},address=#{address} where id=#{id} </update> <delete id=“deleteUserById”> delete from user where id=#{id} </delete></mapper>将接口中方法对应的SQL直接写在XML文件中。 那么这个UserMapper.xml到底放在哪里呢?有两个位置可以放,第一个是直接放在UserMapper所在的包下面: 放在这里的UserMapper.xml会被自动扫描到,但是有另外一个Maven带来的问题,就是java目录下的xml资源在项目打包时会被忽略掉,所以,如果UserMapper.xml放在包下,需要在pom.xml文件中再添加如下配置,避免打包时java目录下的XML文件被自动忽略掉:<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> </resource> </resources></build>当然,UserMapper.xml也可以直接放在resources目录下,这样就不用担心打包时被忽略了,但是放在resources目录下,又不能自动被扫描到,需要添加额外配置。例如我在resources目录下创建mapper目录用来放mapper文件,如下: 此时在application.properties中告诉mybatis去哪里扫描mapper:mybatis.mapper-locations=classpath:mapper/.xml如此配置之后,mapper就可以正常使用了。注意第二种方式不需要在pom.xml文件中配置文件过滤。原理分析在SSM整合中,开发者需要自己提供两个Bean,一个SqlSessionFactoryBean,还有一个是MapperScannerConfigurer,在Spring Boot中,这两个东西虽然不用开发者自己提供了,但是并不意味着这两个Bean不需要了,在org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration类中,我们可以看到Spring Boot提供了这两个Bean,部分源码如下:@org.springframework.context.annotation.Configuration@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })@ConditionalOnSingleCandidate(DataSource.class)@EnableConfigurationProperties(MybatisProperties.class)@AutoConfigureAfter(DataSourceAutoConfiguration.class)public class MybatisAutoConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); return factory.getObject(); } @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); if (executorType != null) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); } } @org.springframework.context.annotation.Configuration @Import({ AutoConfiguredMapperScannerRegistrar.class }) @ConditionalOnMissingBean(MapperFactoryBean.class) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean { @Override public void afterPropertiesSet() { logger.debug(“No {} found.”, MapperFactoryBean.class.getName()); } }}从类上的注解可以看出,当当前类路径下存在SqlSessionFactory、 SqlSessionFactoryBean以及DataSource时,这里的配置才会生效,SqlSessionFactory和SqlTemplate都被提供了。为什么要看这段代码呢?下篇文章,松哥和大伙分享Spring Boot中MyBatis多数据源的配置时,这里将是一个重要的参考。 好了,本文就先说到这里,关于在Spring Boot中整合MyBatis,这里还有一个小小的视频教程,加入我的星球即可免费观看: 关于我的星球【Java达摩院】,大伙可以参考这篇文章推荐一个技术圈子,Java技能提升就靠它了. ...

March 18, 2019 · 2 min · jiezi

Springboot 配置RabbitMQ文档

简介 RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗概念: 生产者 消息的产生方,负责将消息推送到消息队列 消费者 消息的最终接受方,负责监听队列中的对应消息,消费消息 队列 消息的寄存器,负责存放生产者发送的消息 交换机 负责根据一定规则分发生产者产生的消息 绑定 完成交换机和队列之间的绑定模式: direct:直连模式,用于实例间的任务分发 topic:话题模式,通过可配置的规则分发给绑定在该exchange上的队列 headers:适用规则复杂的分发,用headers里的参数表达规则 fanout:分发给所有绑定到该exchange上的队列,忽略routing keySpringBoot集成RabbitMQ 一、引入maven依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>1.5.2.RELEASE</version></dependency>二、配置application.properties# rabbitmqspring.rabbitmq.host = dev-mq.a.pa.comspring.rabbitmq.port = 5672spring.rabbitmq.username = adminspring.rabbitmq.password = adminspring.rabbitmq.virtualHost = /message-test/三、编写AmqpConfiguration配置文件package message.test.configuration;import org.springframework.amqp.core.AcknowledgeMode;import org.springframework.amqp.core.AmqpTemplate;import org.springframework.amqp.core.Binding;import org.springframework.amqp.core.BindingBuilder;import org.springframework.amqp.core.DirectExchange;import org.springframework.amqp.core.Queue;import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;import org.springframework.amqp.rabbit.connection.ConnectionFactory;import org.springframework.amqp.rabbit.core.RabbitTemplate;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.boot.autoconfigure.amqp.RabbitProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class AmqpConfiguration {/** * 消息编码 */ public static final String MESSAGE_ENCODING = “UTF-8”; public static final String EXCHANGE_ISSUE = “exchange_message_issue”; public static final String QUEUE_ISSUE_USER = “queue_message_issue_user”; public static final String QUEUE_ISSUE_ALL_USER = “queue_message_issue_all_user”; public static final String QUEUE_ISSUE_ALL_DEVICE = “queue_message_issue_all_device”; public static final String QUEUE_ISSUE_CITY = “queue_message_issue_city”; public static final String ROUTING_KEY_ISSUE_USER = “routing_key_message_issue_user”; public static final String ROUTING_KEY_ISSUE_ALL_USER = “routing_key_message_issue_all_user”; public static final String ROUTING_KEY_ISSUE_ALL_DEVICE = “routing_key_message_issue_all_device”; public static final String ROUTING_KEY_ISSUE_CITY = “routing_key_message_issue_city”; public static final String EXCHANGE_PUSH = “exchange_message_push”; public static final String QUEUE_PUSH_RESULT = “queue_message_push_result”; @Autowired private RabbitProperties rabbitProperties; @Bean public Queue issueUserQueue() { return new Queue(QUEUE_ISSUE_USER); } @Bean public Queue issueAllUserQueue() { return new Queue(QUEUE_ISSUE_ALL_USER); } @Bean public Queue issueAllDeviceQueue() { return new Queue(QUEUE_ISSUE_ALL_DEVICE); } @Bean public Queue issueCityQueue() { return new Queue(QUEUE_ISSUE_CITY); } @Bean public Queue pushResultQueue() { return new Queue(QUEUE_PUSH_RESULT); } @Bean public DirectExchange issueExchange() { return new DirectExchange(EXCHANGE_ISSUE); } @Bean public DirectExchange pushExchange() { // 参数1:队列 // 参数2:是否持久化 // 参数3:是否自动删除 return new DirectExchange(EXCHANGE_PUSH, true, true); } @Bean public Binding issueUserQueueBinding(@Qualifier(“issueUserQueue”) Queue queue, @Qualifier(“issueExchange”) DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY_ISSUE_USER); } @Bean public Binding issueAllUserQueueBinding(@Qualifier(“issueAllUserQueue”) Queue queue, @Qualifier(“issueExchange”) DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY_ISSUE_ALL_USER); } @Bean public Binding issueAllDeviceQueueBinding(@Qualifier(“issueAllDeviceQueue”) Queue queue, @Qualifier(“issueExchange”) DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY_ISSUE_ALL_DEVICE); } @Bean public Binding issueCityQueueBinding(@Qualifier(“issueCityQueue”) Queue queue, @Qualifier(“issueExchange”) DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY_ISSUE_CITY); } @Bean public Binding pushResultQueueBinding(@Qualifier(“pushResultQueue”) Queue queue, @Qualifier(“pushExchange”) DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).withQueueName(); } @Bean public ConnectionFactory defaultConnectionFactory() { CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); connectionFactory.setHost(rabbitProperties.getHost()); connectionFactory.setPort(rabbitProperties.getPort()); connectionFactory.setUsername(rabbitProperties.getUsername()); connectionFactory.setPassword(rabbitProperties.getPassword()); connectionFactory.setVirtualHost(rabbitProperties.getVirtualHost()); return connectionFactory; } @Bean public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( @Qualifier(“defaultConnectionFactory”) ConnectionFactory connectionFactory) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); return factory; } @Bean public AmqpTemplate rabbitTemplate(@Qualifier(“defaultConnectionFactory”) ConnectionFactory connectionFactory) { return new RabbitTemplate(connectionFactory); }}三、编写生产者body = JSON.toJSONString(issueMessage).getBytes(AmqpConfiguration.MESSAGE_ENCODING); rabbitTemplate.convertAndSend(AmqpConfiguration.EXCHANGE_ISSUE, AmqpConfiguration.ROUTING_KEY_ISSUE_USER, body);四、编写消费者@RabbitListener(queues = AmqpConfiguration.QUEUE_PUSH_RESULT)public void handlePushResult(@Payload byte[] data, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) { } ...

March 18, 2019 · 2 min · jiezi

扩展jwt解决oauth2 性能瓶颈

oauth2 性能瓶颈资源服务器的请求都会被拦截 到认证服务器校验合法性 (如下图)用户携带token 请求资源服务器资源服务器拦截器 携带token 去认证服务器 调用tokenstore 对token 合法性校验资源服务器拿到token,默认只会含有用户名信息通过用户名调用userdetailsservice.loadbyusername 查询用户全部信息如上步骤在实际使用,会造成认证中心的负载压力过大,成为造成整个系统瓶颈的关键点。check-token 过程中涉及的源码更为详细的源码讲解可以参考我上篇文章《Spring Cloud OAuth2 资源服务器CheckToken 源码解析》check-token 涉及到的核心类扩展jwt 生成携带用户详细信息为什么使用jwt 替代默认的UUID token ?通过jwt 访问资源服务器后,不再使用check-token 过程,通过对jwt 的解析即可实现身份验证,登录信息的传递。减少网络开销,提高整体微服务集群的性能spring security oauth 默认的jwttoken 只含有username,通过扩展TokenEnhancer,实现关键字段的注入到 JWT 中,方便资源服务器使用 @Bean public TokenEnhancer tokenEnhancer() { return (accessToken, authentication) -> { if (SecurityConstants.CLIENT_CREDENTIALS .equals(authentication.getOAuth2Request().getGrantType())) { return accessToken; } final Map<String, Object> additionalInfo = new HashMap<>(8); PigxUser pigxUser = (PigxUser) authentication.getUserAuthentication().getPrincipal(); additionalInfo.put(“user_id”, pigxUser.getId()); additionalInfo.put(“username”, pigxUser.getUsername()); additionalInfo.put(“dept_id”, pigxUser.getDeptId()); additionalInfo.put(“tenant_id”, pigxUser.getTenantId()); additionalInfo.put(“license”, SecurityConstants.PIGX_LICENSE); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; }; }生成的token 如下,含有关键的字段重写默认的资源服务器处理行为不再使用RemoteTokenServices 去掉用认证中心 CheckToken,自定义客户端TokenService@Slf4jpublic class PigxCustomTokenServices implements ResourceServerTokenServices { @Setter private TokenStore tokenStore; @Setter private DefaultAccessTokenConverter defaultAccessTokenConverter; @Setter private JwtAccessTokenConverter jwtAccessTokenConverter; @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken); UserAuthenticationConverter userTokenConverter = new PigxUserAuthenticationConverter(); defaultAccessTokenConverter.setUserTokenConverter(userTokenConverter); Map<String, ?> map = jwtAccessTokenConverter.convertAccessToken(readAccessToken(accessToken), oAuth2Authentication); return defaultAccessTokenConverter.extractAuthentication(map); } @Override public OAuth2AccessToken readAccessToken(String accessToken) { return tokenStore.readAccessToken(accessToken); }}解析jwt 组装成Authentication/** * @author lengleng * @date 2019-03-17 * <p> * jwt 转化用户信息 */public class PigxUserAuthenticationConverter implements UserAuthenticationConverter { private static final String USER_ID = “user_id”; private static final String DEPT_ID = “dept_id”; private static final String TENANT_ID = “tenant_id”; private static final String N_A = “N/A”; @Override public Authentication extractAuthentication(Map<String, ?> map) { if (map.containsKey(USERNAME)) { Collection<? extends GrantedAuthority> authorities = getAuthorities(map); String username = (String) map.get(USERNAME); Integer id = (Integer) map.get(USER_ID); Integer deptId = (Integer) map.get(DEPT_ID); Integer tenantId = (Integer) map.get(TENANT_ID); PigxUser user = new PigxUser(id, deptId, tenantId, username, N_A, true , true, true, true, authorities); return new UsernamePasswordAuthenticationToken(user, N_A, authorities); } return null; } private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) { Object authorities = map.get(AUTHORITIES); if (authorities instanceof String) { return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities); } if (authorities instanceof Collection) { return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils .collectionToCommaDelimitedString((Collection<?>) authorities)); } throw new IllegalArgumentException(“Authorities must be either a String or a Collection”); }}资源服务器配置中注入以上配置即可@Slf4jpublic class PigxResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) { DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter(); UserAuthenticationConverter userTokenConverter = new PigxUserAuthenticationConverter(); accessTokenConverter.setUserTokenConverter(userTokenConverter); PigxCustomTokenServices tokenServices = new PigxCustomTokenServices(); // 这里的签名key 保持和认证中心一致 JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(“123”); converter.setVerifier(new MacSigner(“123”)); JwtTokenStore jwtTokenStore = new JwtTokenStore(converter); tokenServices.setTokenStore(jwtTokenStore); tokenServices.setJwtAccessTokenConverter(converter); tokenServices.setDefaultAccessTokenConverter(accessTokenConverter); resources .authenticationEntryPoint(resourceAuthExceptionEntryPoint) .tokenServices(tokenServices); }}使用JWT 扩展后带来的问题JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。去认证服务器校验的过程就是 通过tokenstore 来控制jwt 安全性的一个方法,去掉Check-token 意味着 jwt token 安全性不可保证JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。关注我个人项目 基于Spring Cloud、OAuth2.0开发基于Vue前后分离的开发平台QQ: 2270033969 一起来聊聊你们是咋用 spring cloud 的吧。 ...

March 18, 2019 · 2 min · jiezi

Spring Boot多数据源配置之JdbcTemplate

多数据源配置也算是一个常见的开发需求,Spring和SpringBoot中,对此都有相应的解决方案,不过一般来说,如果有多数据源的需求,我还是建议首选分布式数据库中间件MyCat去解决相关问题,之前有小伙伴在我的知识星球上提问,他的数据根据条件的不同,可能保存在四十多个不同的数据库中,怎么办?这种场景下使用多数据源其实就有些费事了,我给的建议是使用MyCat,然后分表策略使用sharding-by-intfile。当然如果一些简单的需求,还是可以使用多数据源的,Spring Boot中,JdbcTemplate、MyBatis以及Jpa都可以配置多数据源,本文就先和大伙聊一聊JdbcTemplate中多数据源的配置(关于JdbcTemplate的用法,如果还有小伙伴不了解,可以参考我的上篇文章)。创建工程首先是创建工程,和前文一样,创建工程时,也是选择Web、Jdbc以及MySQL驱动,如下图: 创建成功之后,一定接下来手动添加Druid依赖,由于这里一会需要开发者自己配置DataSoruce,所以这里必须要使用druid-spring-boot-starter依赖,而不是传统的那个druid依赖,因为druid-spring-boot-starter依赖提供了DruidDataSourceBuilder类,这个可以用来构建一个DataSource实例,而传统的Druid则没有该类。完整的依赖如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.28</version> <scope>runtime</scope></dependency><dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version></dependency>配置数据源接下来,在application.properties中配置数据源,不同于上文,这里的数据源需要配置两个,如下:spring.datasource.one.url=jdbc:mysql:///test01?useUnicode=true&characterEncoding=utf-8spring.datasource.one.username=rootspring.datasource.one.password=rootspring.datasource.one.type=com.alibaba.druid.pool.DruidDataSourcespring.datasource.two.url=jdbc:mysql:///test02?useUnicode=true&characterEncoding=utf-8spring.datasource.two.username=rootspring.datasource.two.password=rootspring.datasource.two.type=com.alibaba.druid.pool.DruidDataSource这里通过one和two对数据源进行了区分,但是加了one和two之后,这里的配置就没法被SpringBoot自动加载了(因为前面的key变了),需要我们自己去加载DataSource了,此时,需要自己配置一个DataSourceConfig,用来提供两个DataSource Bean,如下:@Configurationpublic class DataSourceConfig { @Bean @ConfigurationProperties(prefix = “spring.datasource.one”) DataSource dsOne() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = “spring.datasource.two”) DataSource dsTwo() { return DruidDataSourceBuilder.create().build(); }}这里提供了两个Bean,其中@ConfigurationProperties是Spring Boot提供的类型安全的属性绑定,以第一个Bean为例,@ConfigurationProperties(prefix = “spring.datasource.one”)表示使用spring.datasource.one前缀的数据库配置去创建一个DataSource,这样配置之后,我们就有了两个不同的DataSource,接下来再用这两个不同的DataSource去创建两个不同的JdbcTemplate。配置JdbcTemplate实例创建JdbcTemplateConfig类,用来提供两个不同的JdbcTemplate实例,如下:@Configurationpublic class JdbcTemplateConfig { @Bean JdbcTemplate jdbcTemplateOne(@Qualifier(“dsOne”) DataSource dsOne) { return new JdbcTemplate(dsOne); } @Bean JdbcTemplate jdbcTemplateTwo(@Qualifier(“dsTwo”) DataSource dsTwo) { return new JdbcTemplate(dsTwo); }}每一个JdbcTemplate的创建都需要一个DataSource,由于Spring容器中现在存在两个DataSource,默认使用类型查找,会报错,因此加上@Qualifier注解,表示按照名称查找。这里创建了两个JdbcTemplate实例,分别对应了两个DataSource。 接下来直接去使用这个JdbcTemplate就可以了。测试使用关于JdbcTemplate的详细用法大伙可以参考我的上篇文章,这里我主要演示数据源的差异,在Controller中注入两个不同的JdbcTemplate,这两个JdbcTemplate分别对应了不同的数据源,如下:@RestControllerpublic class HelloController { @Autowired @Qualifier(“jdbcTemplateOne”) JdbcTemplate jdbcTemplateOne; @Resource(name = “jdbcTemplateTwo”) JdbcTemplate jdbcTemplateTwo; @GetMapping("/user") public List<User> getAllUser() { List<User> list = jdbcTemplateOne.query(“select * from t_user”, new BeanPropertyRowMapper<>(User.class)); return list; } @GetMapping("/user2") public List<User> getAllUser2() { List<User> list = jdbcTemplateTwo.query(“select * from t_user”, new BeanPropertyRowMapper<>(User.class)); return list; }}和DataSource一样,Spring容器中的JdbcTemplate也是有两个,因此不能通过byType的方式注入进来,这里给大伙提供了两种注入思路,一种是使用@Resource注解,直接通过byName的方式注入进来,另外一种就是@Autowired注解加上@Qualifier注解,两者联合起来,实际上也是byName。将JdbcTemplate注入进来之后,jdbcTemplateOne和jdbcTemplateTwo此时就代表操作不同的数据源,使用不同的JdbcTemplate操作不同的数据源,实现了多数据源配置。 好了,这个问题就先说到这里,关于这个多数据源配置,还有一个小小的视频教程,加入我的星球免费观看: 关于我的星球【Java达摩院】,大伙可以参考这篇文章推荐一个技术圈子,Java技能提升就靠它了. ...

March 17, 2019 · 1 min · jiezi

SpringCloud服务间调用

本篇简介在上一篇我们介绍了SpringCloud中的注册中心组件Eureka。Eureka的作用是做服务注册与发现的,目的是让不同的服务与服务之间都可以通过注册中心进行间接关联,并且可以通过注册中心有效的管理不同服务与服务的运行状态。但在微服务的架构中,服务与服务只知道对方的服务地址是没有用的,它们的本质还是需要彼此进行通信的,这也是微服务最核心的功能之一。既然提到了服务与服务之间的通信,那我们自然而然会想到大名鼎鼎的HttpClient。因为在其它的项目架构中我们基本都可以通过它来进行不同服务与服务之间的调用。在SpringCloud中我们依然可以使用HttpClient进行服务与服务调用,只不过如果采用HttpClient调用的话,会有一些弊端。例如: 如果同一个服务有多个负载的话,采用HttpClient调用时,没有办法处理负载均衡的问题。还有另一个问题就是HttpClient只是提供了核心调用的方法并没有对调用进行封装,所以在使用上不太方便,需要自己对HttpClient进行简单的封装。调用方式在SpringCloud中为了解决服务与服务调用的问题,于是提供了两种方式来进行调用。也就是RestTemplate和Feign。虽然从名字上看这两种调用的方式不同,但在底层还是和HttpClient一样,采用http的方式进行调用的。只不过是对HttpClient进行的封装。下面我们来详细的介绍一下这两种方式的区别,我们首先看一下RestTemplate的方式。RestTemplate方式调用RestTemplate为了方便掩饰我们服务间的调用,所以我们需要创建三个项目。它们分别为eureka(注册中心)、server(服务提供方)、client(服务调用方)。因为上一篇中我们已经介绍了eureka的相关内容。所以在这一篇中我们将不在做过多的介绍了。下面我们看一下server端的配置。因为实际上Server端和Client端是相互的。不一定client端一定要调用server端。server端一样可以调用client端。但对于eureka来说,它们都是client端。因为上一篇中我们已经介绍了eureka是分为server端和client端的,并且已经介绍client端相关内容。所以我们下面我们直接看一下server端的配置内容:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/spring: application: name: jilinwula-springcloud-feign-serverserver: port: 8082为了掩饰我们服务间的调用,所以我们需要创建一个Controller,并编写一个简单的接口来供client调用。下面为server的源码。package com.jilinwula.feign.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;import java.util.Map;@RestController@RequestMapping("/server")public class Controller { @GetMapping("/get") public Object get() { Map<String, String> map = new HashMap<String, String>(); map.put(“code”, “0”); map.put(“msg”, “success”); map.put(“data”, “吉林乌拉”); return map; }}下面我们访问一下这个接口看看,是否能正确返回数据。(备注:注意别忘记了在启动类上添加@EnableEurekaClient注解。)下面我们还是使用.http文件的方式发起接口请求。GET http://127.0.0.1:8082/server/get返回结果:GET http://127.0.0.1:8082/server/getHTTP/1.1 200 Content-Type: application/json;charset=UTF-8Transfer-Encoding: chunkedDate: Fri, 15 Mar 2019 08:20:33 GMT{ “msg”: “success”, “code”: “0”, “data”: “吉林乌拉”}Response code: 200; Time: 65ms; Content length: 42 bytes我们看已经成功的返回了接口的数据了。下面我们看一下eureka。看看是否成功的检测到了server端的服务。下面为eureka管理界面地址:http://127.0.0.1:8761 我们看eureka已经成功的检测到了server端注册成功了。下面我们看一下client端的代码,我们还是向server端一样,创建一个Controller,并编写一个接口。下面为具体配置及代码。application.yml:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/spring: application: name: jilinwula-springcloud-feign-clientserver: port: 8081 Controller:package com.jilinwula.feign.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;import java.util.Map;@RestController@RequestMapping("/client")public class Controller { @GetMapping("/get") public Object get() { Map<String, String> map = new HashMap<String, String>(); map.put(“code”, “0”); map.put(“msg”, “success”); map.put(“data”, “吉林乌拉”); return map; }}下面为访问的接口地址:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: application/json;charset=UTF-8Transfer-Encoding: chunkedDate: Fri, 15 Mar 2019 08:56:42 GMT{ “msg”: “success”, “code”: “0”, “data”: “吉林乌拉”}Response code: 200; Time: 273ms; Content length: 42 bytes现在我们在访问一下Eureka地址看一下Client服务注册的是否成功。http://127.0.0.1:8761 RestTemplate实例化我们发现server和client端都已经成功的在注册中心注册成功了。这也就是我们接下来要介绍的服务间调用的前提条件。在开发Spring项目时我们知道如果我们想要使有哪个类或者哪个对象,那就需要在xml中或者用注解的方式实例化对象。所以既然我们打算使用RestTemplate类进行调用,那我们必须要先实例化RestTemplate类。下面我们就看一下怎么在实例化RestTemplate类。因为不论采用的是RestTemplate方式调用还是采用Feign方式,均是在服务的client端进行开发的,在服务的server是无需做任何更改的。所以下面我们看一下client端的改动。下面为项目源码:package com.jilinwula.feign;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.context.annotation.Bean;import org.springframework.web.client.RestTemplate;@SpringBootApplication@EnableEurekaClientpublic class JilinwulaSpringcloudFeignClientApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudFeignClientApplication.class, args); } @Bean public RestTemplate initRestTemplate() { return new RestTemplate(); }}RestTemplate调用方式一为了掩饰方便我们直接在启动类上添加了一个@Bean注解。然后手动实例化了一个对象,并且要特别注意,在使用RestTemplate时,必须要先实例化,否则会抛出空指针异常。下面我们演示一下怎么使用RestTemplate来调用server端的接口。下面为Controller中的代码的改动。package com.jilinwula.feign.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;@RestController@RequestMapping("/client")public class Controller { @Autowired private RestTemplate template; @GetMapping("/get") public Object get() { String result = template.getForObject(“http://127.0.0.1:8082/server/get”, String.class); return result; }}上面的代码比较简单,我们就不详细的介绍了,主要是RestTemplate中提供了getForObject方法(实际上RestTemplate提供了很多种调用的方法,主要分为Get或者Post),可以指定要调用接口的地址,指定返回的值的类型。然后就会直接返回要调用接口的结果。下面我们测试一下,还是调用client接口,看看能否正确的返回server端的数据。http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Fri, 15 Mar 2019 09:42:02 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 362ms; Content length: 42 bytesRestTemplate调用方式二我们看结果,已经成功的返回的server端的数据了,虽然返回的数据没有格式化,但返回的结果数据确实是server端的数据。这也就是RestTemplate的简单使用。但上述的代码是有弊端的,因为我们直接将调用的server端的接口地址直接写死了,这样当服务接口变更时,是需要更改客户端代码的,这显示是不合理的。那怎么办呢?这时就知道注册中心的好处了。因为注册中心知道所有服务的地址,这样我们通过注册中心就可以知道server端的接口地址,这样就避免了server端服务更改时,要同步更改client代码了。下面我们在优化一下代码,看看怎么通过注册中心来获取server端的地址。package com.jilinwula.feign.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.cloud.client.ServiceInstance;import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;@RestController@RequestMapping("/client")public class Controller { @Autowired private RestTemplate template; @Autowired private LoadBalancerClient loadBalancerClient; @GetMapping("/get") public Object get() { ServiceInstance serviceInstance = loadBalancerClient.choose(“jilinwula-springcloud-feign-server”); String url = String.format(“http://%s:%s/server/get”, serviceInstance.getHost(), serviceInstance.getPort()); String result = template.getForObject(url, String.class); return result; }}在SpringClourd中提供了LoadBalancerClient接口。通过这个接口我们可以通过用户中心的Application的名字来获取该服务的地址和端口。也就是下图中红色标红的名字(注意名字大小写)。 通过这些我们就可以获取到完整的服务接口地址了,这样就可以直接通过RestTemplate进行接口调用了。下面我们在看一下调用的结果。接口地址:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 09:08:32 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 53ms; Content length: 42 bytesRestTemplate调用方式三这样我们就解决了第一次服务接口地址写死的问题了。但上述的接口还有一个弊端就是我们每次调用服务时都要先通过Application的名字来获取ServiceInstance对象,然后才可以发起接口调用。实际上在SpringCloud中为我们提供了@LoadBalanced注解,只要将该注解添加到RestTemplate中的获取的地方就可以了。下面为具体修改:启动类:package com.jilinwula.feign;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.loadbalancer.LoadBalanced;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.context.annotation.Bean;import org.springframework.web.client.RestTemplate;@SpringBootApplication@EnableEurekaClientpublic class JilinwulaSpringcloudFeignClientApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudFeignClientApplication.class, args); } @Bean @LoadBalanced public RestTemplate initRestTemplate() { return new RestTemplate(); }}我们在RestTemplate实例化的地方添加了@LoadBalanced注解,这样在我们使用RestTemplate时就该注解就会自动将调用接口的地址替换成真正的服务地址。下面我们看一下Controller中的改动:package com.jilinwula.feign.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;@RestController@RequestMapping("/client")public class Controller { @Autowired private RestTemplate template; @GetMapping("/get") public Object get() { String url = String.format(“http://%s/server/get”, “jilinwula-springcloud-feign-server”); String result = template.getForObject(url, String.class); return result; }}代码和第一次的代码基本一样,唯一的区别就是获取服务地址和端口的地方替换成了注册中心中的Application的名字,并且我们的RestTemplate在使用上和第一次没有任何区别,只是在url中不同。下面我们看一下返回的结果。GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 09:55:46 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 635ms; Content length: 42 bytes默认负载均衡策略上述内容就是使用RestTemplate来进行服务间调用的方式。并且采用这样的方式可以很方便的解决负载均衡的问题。因为@LoadBalanced注解会自动采用默信的负载策略。下面我们看验证一下SpringCloud默认的负载策略是什么。为了掩饰负载策略,所以我们在新增一个server服务,并且为了掩饰这两个server返回结果的不同,我们故意让接口返回的数据不一致,来方便我们测试。下面为新增的server服务端的配置信息及controller源码。application.yml:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/spring: application: name: jilinwula-springcloud-feign-serverserver: port: 8083 Controller:package com.jilinwula.feign.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;import java.util.Map;@RestController@RequestMapping("/server")public class Controller { @GetMapping("/get") public Object get() { Map<String, String> map = new HashMap<String, String>(); map.put(“code”, “0”); map.put(“msg”, “success”); map.put(“data”, “jilinwula”); return map; }}调用以下接口:GET http://127.0.0.1:8083/server/get返回结果:GET http://127.0.0.1:8083/server/getHTTP/1.1 200 Content-Type: application/json;charset=UTF-8Transfer-Encoding: chunkedDate: Sat, 16 Mar 2019 10:49:07 GMT{ “msg”: “success”, “code”: “0”, “data”: “jilinwula”}Response code: 200; Time: 100ms; Content length: 47 bytes现在我们访问一下注册中心看一下现在注册中心的变化。注册中心地址:http://127.0.0.1:8761 我们看上图注册中心已经显示Application名字为JILINWULA-SPRINGCLOUD-FEIGN-SERVER的有两个服务已经注册成功了。下面我们直接调用client中的接口,看一下client默认会返回哪个server端的信息。client接口地址:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 47Date: Sat, 16 Mar 2019 10:58:39 GMT{“msg”:“success”,“code”:“0”,“data”:“jilinwula”}Response code: 200; Time: 24ms; Content length: 47 bytes看上面返回的结果是server2的接口数据。我们在请求一下接口在看一下返回的结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 11:01:01 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 15ms; Content length: 42 bytes更改默认负载均衡策略一我们看这回返回的接口数据就是第一个server端的信息了。并且我们可以频繁的调用client中的接口,并观察发现它们会交替返回的。所以我们基本可以确定SpringCloud默认的负载策略为轮询方式。也就是会依次调用。在SpringCloud中提供了很多种负载策略。比较常见的为:随机、轮询、哈希、权重等。下面我们介绍一下怎么修改默认的负载策略。SpringCloud底层采用的是Ribbon来实现的负载均衡。Ribbon是一个负载均衡器,Ribbon的核心组件为IRule,它也就是所有负载策略的父类。下面为IRule接口的源码:package com.netflix.loadbalancer;public interface IRule { Server choose(Object var1); void setLoadBalancer(ILoadBalancer var1); ILoadBalancer getLoadBalancer();}该类只提供了3个方法,它们的作用分别是选择一个服务名字、设置ILoadBalancer和返回ILoadBalancer。下面我们看一下IRule接口的常见策略子类。常见的有RandomRule、RoundRobinRule、WeightedResponseTimeRule等。分别对应着随机、轮询、和权重。下面我们看一下怎么更改默认的策略方式。更改默认策略也是在client端中操作的,所以我们看一下client端的代码更改:package com.jilinwula.feign;import com.netflix.loadbalancer.IRule;import com.netflix.loadbalancer.RandomRule;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.loadbalancer.LoadBalanced;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.context.annotation.Bean;import org.springframework.web.client.RestTemplate;@SpringBootApplication@EnableEurekaClientpublic class JilinwulaSpringcloudFeignClientApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudFeignClientApplication.class, args); } @Bean @LoadBalanced public RestTemplate initRestTemplate() { return new RestTemplate(); } @Bean public IRule initIRule() { return new RandomRule(); }}我们在启动类上新实例化了一个IRule对象,并且指定该对象实例化的子类为RandomRule,也就是随机的方式。所以当我们Client端启动服务调用服务时,就会采用随机的方式进行调用,因为我们已经将IRule对象默认的实例化方式更改了。下面我们测试一下,继续访问Client端接口:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 11:36:01 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 15ms; Content length: 42 bytes更改默认负载均衡策略二在这里我们就不依依演示了,但如果我们多次调用接口就会发现,Client接口返回的结果不在是轮询的方式了,而是变成了随机了,这就说明我们已经成功的将SpringCloud默认的负载策略更改了。下面我们换一种方式来更改默认的负载策略。这种方式和上面的有所不同,而是在配置文件中配置的,下面为具体的配置。(备注:为了不影响测试效果,我们需要将刚刚在启动类中的实例化的IRule注释掉)eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/spring: application: name: jilinwula-springcloud-feign-clientserver: port: 8081jilinwula-springcloud-feign-server: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule我们在配置文件中指定了注册中心中的server端的Application名字,然后指定了默认的负载策略类。下面我们测试一下。访问以下接口:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 11:54:42 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 13ms; Content length: 42 bytesFeign方式调用我们在实际的开发中,可以使用上述两种方式来更改SpringCloud中默认的负载策略。下面我们看一下SpringCloud中另一种服务间调用方式也就是Feign方式。使用Feign方式和RestTemplate不同,我们需要先添加Feign的依赖,具体依赖如下(备注:该依赖同样是在client端添加的):pom.xml:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.1.1.RELEASE</version></dependency>其次我们还需要在启动类中添加@EnableFeignClients注解。具体代码如下:package com.jilinwula.feign;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.cloud.netflix.feign.EnableFeignClients;@SpringBootApplication@EnableEurekaClient@EnableFeignClientspublic class JilinwulaSpringcloudFeignClientApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudFeignClientApplication.class, args); }}接下来我们需要在Client端创建一个新的接口并定义Client端需要调用的服务方法。具体代码如下:package com.jilinwula.feign.server;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;@FeignClient(name = “jilinwula-springcloud-feign-server”)public interface ServerApi { @GetMapping("/server/get") String get();}上述接口基本上和server端的Controller一致,唯一的不同就是我们指定了@FeignClient注解,该注解的需要指定一个名字,也就是注册中心中Applicaiton的名字,也就是要调用的服务名字。下面我们看一下Controller中的代码更改:package com.jilinwula.feign.controller;import com.jilinwula.feign.server.ServerApi;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/client")public class Controller { @Autowired private ServerApi serverApi; @GetMapping("/get") public Object get() { String result = serverApi.get(); return result; }}我们在Controller中直接使用了我们自定义的接口,并直接调用我们接口中定义的方法,下面我们调用一下Client接口看看这样的方式是否可以调用成功。接口地址:GET http://127.0.0.1:8081/client/get返回结果:GET http://127.0.0.1:8081/client/getHTTP/1.1 200 Content-Type: text/plain;charset=UTF-8Content-Length: 50Date: Sat, 16 Mar 2019 12:54:50 GMT{“msg”:“success”,“code”:“0”,“data”:“吉林乌拉”}Response code: 200; Time: 14ms; Content length: 42 bytes我们看这样的方式也是可以成功的调用server端的接口的,只不过这样的方式可能会让觉的不太方便,因为这样的方式是需要Client端定义和Server端一样的接口的。上述内容就是本篇的全部内容,在实际的项目开发中,这两种方式均可实现服务与服务间的调用,并且这两种方式都有彼此的弊端,所以并没有特别推荐的方式。在下一篇中,我们将介绍配置中心相关内容,谢谢。项目源码https://github.com/jilinwula/jilinwula-springcloud-feign原文链接http://jilinwula.com/article/… ...

March 16, 2019 · 3 min · jiezi

项目debug记录

在写前台获取数据时,报了如下错误:看了一下好像是拦截器那报的错,去看拦截器报错的地方:发现也没啥问题,就后台一步一步debug,发现确实有返回值,但为啥前台接受不到呢?然后我重新启动了一次项目,查看后台日志,发现了如下情况:意思是json序列化了,去检查控制器,发现果然没有加jsonView,加上jsonView后,问题解决但为什么json序列化,前台却是拦截器报错呢?后来朴世超组长告诉我说json序列化的数据太长了,后台没有给响应,而前台拦截器长时间接收不到响应,就报错了。虽然听懂了一些,但还是一知半解。总结感觉自己还是有好多知识盲区,即使知道怎样解决问题,但是具体原理还是不清楚,就是凭经验解决。

March 16, 2019 · 1 min · jiezi

Spring Boot数据持久化之JdbcTemplate

在Java领域,数据持久化有几个常见的方案,有Spring自带的JdbcTemplate、有MyBatis,还有JPA,在这些方案中,最简单的就是Spring自带的JdbcTemplate了,这个东西虽然没有MyBatis那么方便,但是比起最开始的Jdbc已经强了很多了,它没有MyBatis功能那么强大,当然也意味着它的使用比较简单,事实上,JdbcTemplate算是最简单的数据持久化方案了,本文就和大伙来说说这个东西的使用。基本配置JdbcTemplate基本用法实际上很简单,开发者在创建一个SpringBoot项目时,除了选择基本的Web依赖,再记得选上Jdbc依赖,以及数据库驱动依赖即可,如下: 项目创建成功之后,记得添加Druid数据库连接池依赖(注意这里可以添加专门为Spring Boot打造的druid-spring-boot-starter,而不是我们一般在SSM中添加的Druid),所有添加的依赖如下:<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.27</version> <scope>runtime</scope></dependency>项目创建完后,接下来只需要在application.properties中提供数据的基本配置即可,如下:spring.datasource.type=com.alibaba.druid.pool.DruidDataSourcespring.datasource.username=rootspring.datasource.password=123spring.datasource.url=jdbc:mysql:///test01?useUnicode=true&characterEncoding=UTF-8如此之后,所有的配置就算完成了,接下来就可以直接使用JdbcTemplate了?咋这么方便呢?其实这就是SpringBoot的自动化配置带来的好处,我们先说用法,一会来说原理。基本用法首先我们来创建一个User Bean,如下:public class User { private Long id; private String username; private String address; //省略getter/setter}然后来创建一个UserService类,在UserService类中注入JdbcTemplate,如下:@Servicepublic class UserService { @Autowired JdbcTemplate jdbcTemplate;}好了,如此之后,准备工作就算完成了。增JdbcTemplate中,除了查询有几个API之外,增删改统一都使用update来操作,自己来传入SQL即可。例如添加数据,方法如下:public int addUser(User user) { return jdbcTemplate.update(“insert into user (username,address) values (?,?);”, user.getUsername(), user.getAddress());}update方法的返回值就是SQL执行受影响的行数。 这里只需要传入SQL即可,如果你的需求比较复杂,例如在数据插入的过程中希望实现主键回填,那么可以使用PreparedStatementCreator,如下:public int addUser2(User user) { KeyHolder keyHolder = new GeneratedKeyHolder(); int update = jdbcTemplate.update(new PreparedStatementCreator() { @Override public PreparedStatement createPreparedStatement(Connection connection) throws SQLException { PreparedStatement ps = connection.prepareStatement(“insert into user (username,address) values (?,?);”, Statement.RETURN_GENERATED_KEYS); ps.setString(1, user.getUsername()); ps.setString(2, user.getAddress()); return ps; } }, keyHolder); user.setId(keyHolder.getKey().longValue()); System.out.println(user); return update;}实际上这里就相当于完全使用了JDBC中的解决方案了,首先在构建PreparedStatement时传入Statement.RETURN_GENERATED_KEYS,然后传入KeyHolder,最终从KeyHolder中获取刚刚插入数据的id保存到user对象的id属性中去。 你能想到的JDBC的用法,在这里都能实现,Spring提供的JdbcTemplate虽然不如MyBatis,但是比起Jdbc还是要方便很多的。删删除也是使用update API,传入你的SQL即可:public int deleteUserById(Long id) { return jdbcTemplate.update(“delete from user where id=?”, id);}当然你也可以使用PreparedStatementCreator。改public int updateUserById(User user) { return jdbcTemplate.update(“update user set username=?,address=? where id=?”, user.getUsername(), user.getAddress(),user.getId());}当然你也可以使用PreparedStatementCreator。查查询的话,稍微有点变化,这里主要向大伙介绍query方法,例如查询所有用户:public List<User> getAllUsers() { return jdbcTemplate.query(“select * from user”, new RowMapper<User>() { @Override public User mapRow(ResultSet resultSet, int i) throws SQLException { String username = resultSet.getString(“username”); String address = resultSet.getString(“address”); long id = resultSet.getLong(“id”); User user = new User(); user.setAddress(address); user.setUsername(username); user.setId(id); return user; } });}查询的时候需要提供一个RowMapper,就是需要自己手动映射,将数据库中的字段和对象的属性一一对应起来,这样。。。。嗯看起来有点麻烦,实际上,如果数据库中的字段和对象属性的名字一模一样的话,有另外一个简单的方案,如下:public List<User> getAllUsers2() { return jdbcTemplate.query(“select * from user”, new BeanPropertyRowMapper<>(User.class));}至于查询时候传参也是使用占位符,这个和前文的一致,这里不再赘述。其他除了这些基本用法之外,JdbcTemplate也支持其他用法,例如调用存储过程等,这些都比较容易,而且和Jdbc本身都比较相似,这里也就不做介绍了,有兴趣可以留言讨论。原理分析那么在SpringBoot中,配置完数据库基本信息之后,就有了一个JdbcTemplate了,这个东西是从哪里来的呢?源码在org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration类中,该类源码如下:@Configuration@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })@ConditionalOnSingleCandidate(DataSource.class)@AutoConfigureAfter(DataSourceAutoConfiguration.class)@EnableConfigurationProperties(JdbcProperties.class)public class JdbcTemplateAutoConfiguration { @Configuration static class JdbcTemplateConfiguration { private final DataSource dataSource; private final JdbcProperties properties; JdbcTemplateConfiguration(DataSource dataSource, JdbcProperties properties) { this.dataSource = dataSource; this.properties = properties; } @Bean @Primary @ConditionalOnMissingBean(JdbcOperations.class) public JdbcTemplate jdbcTemplate() { JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSource); JdbcProperties.Template template = this.properties.getTemplate(); jdbcTemplate.setFetchSize(template.getFetchSize()); jdbcTemplate.setMaxRows(template.getMaxRows()); if (template.getQueryTimeout() != null) { jdbcTemplate .setQueryTimeout((int) template.getQueryTimeout().getSeconds()); } return jdbcTemplate; } } @Configuration @Import(JdbcTemplateConfiguration.class) static class NamedParameterJdbcTemplateConfiguration { @Bean @Primary @ConditionalOnSingleCandidate(JdbcTemplate.class) @ConditionalOnMissingBean(NamedParameterJdbcOperations.class) public NamedParameterJdbcTemplate namedParameterJdbcTemplate( JdbcTemplate jdbcTemplate) { return new NamedParameterJdbcTemplate(jdbcTemplate); } }}从这个类中,大致可以看出,当当前类路径下存在DataSource和JdbcTemplate时,该类就会被自动配置,jdbcTemplate方法则表示,如果开发者没有自己提供一个JdbcOperations的实例的话,系统就自动配置一个JdbcTemplate Bean(JdbcTemplate是JdbcOperations接口的一个实现)。好了,不知道大伙有没有收获呢? 关于JdbcTemplate,我还有一个小小视频,加入我的知识星球,免费观看: 加入我的星球,和众多大牛一起切磋技术推荐一个技术圈子,Java技能提升就靠它了。 ...

March 16, 2019 · 2 min · jiezi

SpringBoot 填坑 | Shiro 与 Redis 多级缓存问题

微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。前言来自不愿意透露姓名的小师弟的投稿。这篇主要讲了,项目中配置了多缓存遇到的坑,以及解决办法。发现问题在一次项目实践中有实现多级缓存其中有已经包括了 Shiro 的 Cache ,本以为开启 redis 的缓存是一件很简单的事情只需要在启动类上加上 @EnableCaching 注解就会启动缓存管理了,但是问题出现了。重要错误日志截图java.lang.IllegalStateException: @Bean method ShiroConfig.cacheManager called as a bean reference for type [org.apache.shiro.cache.ehcache.EhCacheManager] but overridden by non-compatible bean instance of type [org.springframework.data.redis.cache.RedisCacheManager]. Overriding bean of same name declared in: class path resource [org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.class]错误日志分析看日志大概就发现一个非法状态异常,我们继续查看接下来的日志有一段非常的重要日志 Overriding bean of same name 翻译过来的意思是帮你重写了一个名字一样的 Bean,我再看看日志里有提到 RedisCacheManager 与我自己实现的 cacheManager 到这里我已经感觉到问题所在了,以下图一为 RedisCacheManager 部分实现代码。图二为我自己的 Shiro 的 cacheManager 实现方法。解决问题有 Spring 基础的大家都应该还记得 Spring 不允许有相同的 Bean 出现。现在问题就在于 Redis 缓存管理器和 Shiro 的缓存管理器重名了,而这二者又是通过 Spring 管理,所以 Spring 读取这二者的时候,产生冲突了。解决问题的方法很简单:在自己实现 EhCacheManager 时把 @Bean 指定一个名字可以像这样 @Bean(name =“ehCacheManager” ),还有其他办法大家可以在想办法实现一下嘿嘿。结语虽然我们都知道 Spring 的报错是非常多的,但是在 Spring 的报错日志中查找问题所在是非常有用的,大部分的错误,日志都会给你反馈。如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 16, 2019 · 1 min · jiezi

vue 使用 el-upload 上传文件及Feign 服务间传递文件

一、前端代码<el-upload class=“step_content” drag action=“string” ref=“upload” :multiple=“false” :http-request=“createAppVersion” :data=“appVersion” :auto-upload=“false” :limit=“1” :on-change=“onFileUploadChange” :on-remove=“onFileRemove”> <i class=“el-icon-upload”></i> <div class=“el-upload__text”>将文件拖到此处,或<em>点击上传</em></div></el-upload> <div class=“mgt30”> <el-button v-show=“createAppVisible” :disabled=“createAppDisable” type=“primary” class=“mgt30” @click=“onSubmitClick”>立即创建 </el-button> </div>…. onSubmitClick() { this.$refs.upload.submit(); }, createAppVersion(param) { this.globalLoading = true; const formData = new FormData(); formData.append(‘file’, param.file); formData.append(‘appVersion’, JSON.stringify(this.appVersion)); addAppVersionApi(formData).then(res => { this.globalLoading = false; this.$message({message: ‘创建APP VERION 成功’, type: ‘success’}); this.uploadFinish(); }).catch(reason => { this.globalLoading = false; this.showDialog(reason); }) },说明:el-upload 的 ref=“upload” 给这个组件起个变量名,以便 js逻辑代码可以引用http-request=“createAppVersion” el-upload 上传使使用自定义的方法:data=“appVersion” 上传时提交的表单数据onSubmitClick 方法中会调用el-upload.submit()方法,进而调用createAppVersion()方法组成表单参数,取得上传的file 和 其它参数 const formData = new FormData(); formData.append(‘file’, param.file); formData.append(‘appVersion’, JSON.stringify(this.appVersion));6.addAppVersionApi 最终会调用下面的方法,其中formData就是param, 请求需要加header: ‘Content-Type’: ‘multipart/form-data’ function httpPostMutipartFileAsyn(url, param) { return new Promise((resolve, reject) => { request({ url: url, headers: { ‘Content-Type’: ‘multipart/form-data’ }, method: ‘post’, data: param }).then(response => { if (response.data.status.code === 0) { resolve(response.data) } else { reject(response.data.status) } }).catch(response => { reject(response) }) })}二、后端代码1.后端controller接口@PostMapping("/version/add") public RestResult addAppVersion(@RequestParam(“appVersion”) String appVersion, @RequestParam(“file”) MultipartFile multipartFile) { …. return new RestResult(); }三、Feign 实现服务间传递MultipartFile参数Controller的addAppVersion()接口,收到上传的文件后,需要通过Http调用远程接口,将文件上传到资源服务。一开始尝试使用OKHttp 和 RestTemplate 实现,但是这两种方法都必须将文件先保存,无法直接传递MultipartFile参数,然后才能通过OKHttp 和 RestTemplate提供的相关接口去实现。 本着不想在本地保存临时文件的,找到了通过Feign解决的一种方式1.添加如下依赖: <dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>io.github.openfeign.form</groupId> <artifactId>feign-form-spring</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.3</version> </dependency>2.Feign 接口实现@FeignClient(name = “resource-client”,url = “http://xxxx”,path = “resource”,configuration = ResourceServiceFeignClient.MultipartSupportConfig.class)public interface ResourceServiceFeignClient { @PostMapping(value = “/upload”, consumes = MediaType.MULTIPART_FORM_DATA_VALUE) Resource upload(@RequestPart(“file”) MultipartFile file); class MultipartSupportConfig { @Bean public Encoder feignFormEncoder() { return new SpringFormEncoder(); } }}这里Feign是通过url实现的接口调用,并没有通过SpringCloud注册中心服务发现来实现接口调用,因为我所在的项目是独立在服务化体系外的3.将Feign接口自动注入,正常使用就行了。 ...

March 14, 2019 · 1 min · jiezi

基于Spring Security和 JWT的权限系统设计

写在前面关于 Spring SecurityWeb系统的认证和权限模块也算是一个系统的基础设施了,几乎任何的互联网服务都会涉及到这方面的要求。在Java EE领域,成熟的安全框架解决方案一般有 Apache Shiro、Spring Security等两种技术选型。Apache Shiro简单易用也算是一大优势,但其功能还是远不如 Spring Security强大。Spring Security可以为 Spring 应用提供声明式的安全访问控制,起通过提供一系列可以在 Spring应用上下文中可配置的Bean,并利用 Spring IoC和 AOP等功能特性来为应用系统提供声明式的安全访问控制功能,减少了诸多重复工作。关于JWTJSON Web Token (JWT),是在网络应用间传递信息的一种基于 JSON的开放标准((RFC 7519),用于作为JSON对象在不同系统之间进行安全地信息传输。主要使用场景一般是用来在 身份提供者和服务提供者间传递被认证的用户身份信息。关于JWT的科普,可以看看阮一峰老师的《JSON Web Token 入门教程》。本文则结合 Spring Security和 JWT两大利器来打造一个简易的权限系统。本文实验环境如下:Spring Boot版本:2.0.6.RELEASEIDE:IntelliJ IDEA 2018.2.4另外本文实验代码置于文尾,需要自取。设计用户和角色本文实验为了简化考虑,准备做如下设计:设计一个最简角色表role,包括角色ID和角色名称设计一个最简用户表user,包括用户ID,用户名,密码再设计一个用户和角色一对多的关联表user_roles一个用户可以拥有多个角色创建 Spring Security和 JWT加持的 Web工程pom.xml 中引入 Spring Security和 JWT所必需的依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version></dependency>项目配置文件中加入数据库和 JPA等需要的配置server.port=9991spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://121.196.XXX.XXX:3306/spring_security_jwt?useUnicode=true&characterEncoding=utf-8spring.datasource.username=rootspring.datasource.password=XXXXXXlogging.level.org.springframework.security=infospring.jpa.hibernate.ddl-auto=updatespring.jpa.show-sql=truespring.jackson.serialization.indent_output=true创建用户、角色实体用户实体 User:/** * @ www.codesheep.cn * 20190312 /@Entitypublic class User implements UserDetails { @Id @GeneratedValue private Long id; private String username; private String password; @ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER) private List<Role> roles; … // 下面为实现UserDetails而需要的重写方法! @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role : roles) { authorities.add( new SimpleGrantedAuthority( role.getName() ) ); } return authorities; } …}此处所创建的 User类继承了 Spring Security的 UserDetails接口,从而成为了一个符合 Security安全的用户,即通过继承 UserDetails,即可实现 Security中相关的安全功能。角色实体 Role:/* * @ www.codesheep.cn * 20190312 /@Entitypublic class Role { @Id @GeneratedValue private Long id; private String name; … // 省略 getter和 setter}创建JWT工具类主要用于对 JWT Token进行各项操作,比如生成Token、验证Token、刷新Token等/* * @ www.codesheep.cn * 20190312 /@Componentpublic class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -5625635588908941275L; private static final String CLAIM_KEY_USERNAME = “sub”; private static final String CLAIM_KEY_CREATED = “created”; public String generateToken(UserDetails userDetails) { … } String generateToken(Map<String, Object> claims) { … } public String refreshToken(String token) { … } public Boolean validateToken(String token, UserDetails userDetails) { … } … // 省略部分工具函数}创建Token过滤器,用于每次外部对接口请求时的Token处理/* * @ www.codesheep.cn * 20190312 /@Componentpublic class JwtTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader( Const.HEADER_STRING ); if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) { final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() ); String username = jwtTokenUtil.getUsernameFromToken(authToken); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); }}Service业务编写主要包括用户登录和注册两个主要的业务public interface AuthService { User register( User userToAdd ); String login( String username, String password );}/* * @ www.codesheep.cn * 20190312 /@Servicepublic class AuthServiceImpl implements AuthService { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserRepository userRepository; // 登录 @Override public String login( String username, String password ) { UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password ); final Authentication authentication = authenticationManager.authenticate(upToken); SecurityContextHolder.getContext().setAuthentication(authentication); final UserDetails userDetails = userDetailsService.loadUserByUsername( username ); final String token = jwtTokenUtil.generateToken(userDetails); return token; } // 注册 @Override public User register( User userToAdd ) { final String username = userToAdd.getUsername(); if( userRepository.findByUsername(username)!=null ) { return null; } BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); final String rawPassword = userToAdd.getPassword(); userToAdd.setPassword( encoder.encode(rawPassword) ); return userRepository.save(userToAdd); }}Spring Security配置类编写(非常重要)这是一个高度综合的配置类,主要是通过重写 WebSecurityConfigurerAdapter 的部分 configure配置,来实现用户自定义的部分。/* * @ www.codesheep.cn * 20190312 /@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled=true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Bean public JwtTokenFilter authenticationTokenFilterBean() throws Exception { return new JwtTokenFilter(); } @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure( AuthenticationManagerBuilder auth ) throws Exception { auth.userDetailsService( userService ).passwordEncoder( new BCryptPasswordEncoder() ); } @Override protected void configure( HttpSecurity httpSecurity ) throws Exception { httpSecurity.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS, “/”).permitAll() // OPTIONS请求全部放行 .antMatchers(HttpMethod.POST, “/authentication/”).permitAll() //登录和注册的接口放行,其他接口全部接受验证 .antMatchers(HttpMethod.POST).authenticated() .antMatchers(HttpMethod.PUT).authenticated() .antMatchers(HttpMethod.DELETE).authenticated() .antMatchers(HttpMethod.GET).authenticated(); // 使用前文自定义的 Token过滤器 httpSecurity .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); httpSecurity.headers().cacheControl(); }}编写测试 Controller登录和注册的 Controller:/* * @ www.codesheep.cn * 20190312 /@RestControllerpublic class JwtAuthController { @Autowired private AuthService authService; // 登录 @RequestMapping(value = “/authentication/login”, method = RequestMethod.POST) public String createToken( String username,String password ) throws AuthenticationException { return authService.login( username, password ); // 登录成功会返回JWT Token给用户 } // 注册 @RequestMapping(value = “/authentication/register”, method = RequestMethod.POST) public User register( @RequestBody User addedUser ) throws AuthenticationException { return authService.register(addedUser); }}再编写一个测试权限的 Controller:/* * @ www.codesheep.cn * 20190312 */@RestControllerpublic class TestController { // 测试普通权限 @PreAuthorize(“hasAuthority(‘ROLE_NORMAL’)”) @RequestMapping( value="/normal/test", method = RequestMethod.GET ) public String test1() { return “ROLE_NORMAL /normal/test接口调用成功!”; } // 测试管理员权限 @PreAuthorize(“hasAuthority(‘ROLE_ADMIN’)”) @RequestMapping( value = “/admin/test”, method = RequestMethod.GET ) public String test2() { return “ROLE_ADMIN /admin/test接口调用成功!”; }}这里给出两个测试接口用于测试权限相关问题,其中接口 /normal/test需要用户具备普通角色(ROLE_NORMAL)即可访问,而接口/admin/test则需要用户具备管理员角色(ROLE_ADMIN)才可以访问。接下来启动工程,实验测试看看效果—实验验证在文章开头我们即在用户表 user中插入了一条用户名为 codesheep的记录,并在用户-角色表 user_roles中给用户 codesheep分配了普通角色(ROLE_NORMAL)和管理员角色(ROLE_ADMIN)接下来进行用户登录,并获得后台向用户颁发的JWT Token接下来访问权限测试接口不带 Token直接访问需要普通角色(ROLE_NORMAL)的接口 /normal/test会直接提示访问不通:而带 Token访问需要普通角色(ROLE_NORMAL)的接口 /normal/test才会调用成功:同理由于目前用户具备管理员角色,因此访问需要管理员角色(ROLE_ADMIN)的接口 /admin/test也能成功:接下里我们从用户-角色表里将用户codesheep的管理员权限删除掉,再访问接口 /admin/test,会发现由于没有权限,访问被拒绝了:经过一系列的实验过程,也达到了我们的预期!写在最后本文涉及的东西还是蛮多的,最后我们也将本文的实验源码放在 Github上,需要的可以自取:源码下载地址由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

March 14, 2019 · 3 min · jiezi

这里有套SpringBoot2.1.3最新版武功秘籍,你要不要学?

这里有本最新的版的Spring Boot武功秘籍,你要不要学? ][3]整个教程17+小时: 不同于网上找到的一年前甚至两年前的视频教程,这个是上周刚刚出炉的Spring Boot2.1.3视频教程。下载地址如下: 链接:https://pan.baidu.com/s/1vqeW… 提取码:hyxb 视频播放授权码获取方式有两种: 1.加入我的知识星球,所有最新出炉的视频教程免费看。关于我的知识星球,可以查看这篇文章推荐一个技术圈子,Java技能提升就靠它了. 2.单独购买这套视频教程,售价¥19.9,不过这个视频虽然单卖,但是还是建议大伙选择方案1,因为在星球内,每周都会分享一次加密视频教程,全年分享的教程至少有几十个,如果单个买的话,很显然不划算。如果需要单独购买,请加我微信:

March 13, 2019 · 1 min · jiezi

持续集成之 Spring Boot 实战篇

本文作者: CODING 用户 - 何健这次实战篇,我们借助「CODING 持续集成」,实现一个简单的 Spring Boot 项目从编码到最后部署的完整过程。本教程还有 B 站视频版,帮助读者更好地学习理解。思路在线上环境构建、测试、部署这种情况,通常会将 jenkins 安装在服务器上,确保构建测试等操作环境和线上环境一致。此时通常会在 jenkins 中配置好需要持续集成的仓库,以及具体流程。这种方式非常简单粗暴,也非常有效,但是缺点也很明显。可能 jenkins 会成为线上环境的旁站漏洞,这是非常不安全的。那么,我们就需要更高级的方式,可以线上环境之外的构建测试,最终部署到线上环境。「CODING 持续集成」正是提供这类持续集成模式的平台。不在实际部署服务器上构建、测试为了避免占用线上服务器的资源,也为了避免安全问题,我们可以使用单独的 jenkins (或者其它此类软件)完成构建、测试、分发,实际部署通过单独的 webhook 实现。这样就可以避免在线上环境安装 Jenkins,还可以避免更复杂的系统安全维护。这样做的优点:不会影响在线服务;缺点:部署地机器最好是可以公网访问的,否则会无法完成后续分发步骤。终极解决方案:使用 SaaS 化的 JenkinsSoftware as a Service,软件即服务。「CODING 持续集成」集成了 SaaS 化的 Jenkins 等主流企业开发流程工具,实现了 DevOps 流程全自动化。开箱即用,直接用它就好!捋一下思路我们这次实战针对后一种思路检出代码构建测试分发触发部署实战实际体验,还是很不错的。视频地址:CODING 持续集成 - Spring Boot 项目第一步:初始化一个持续集成1、首先,我们需要进入准备持续集成的项目。这里我用 start.spring.io 初始化一个 demo 示例项目,并推送到仓库。为了方便大家,亲自体验,我准备了一个现成的仓库,可以直接 git clone 下来再 git push 到自己账户下使用。仓库地址:demoForCI2、解压 demo 项目,进入 demo 目录,初始化仓库。 cd g:\demo\ git init git set remote giturl git add ./ git commit -m ‘init repo’ git push -u origin master别忘了 git config user.name yourname 和 git config user.email youremail3、开始体验仓库准备好后,就可以开始体验「CODING 持续集成」。第一次的使用,需要先创建一个 Jenkinsfile,很多小伙伴会说,第一次用,不知道是啥。没关系,「CODING 持续集成」已经给我们准备好了模板,非常容易理解,可以认为是特定格式语法写一套 task 流程。点击一下 “简易模板”,更具实际情况修改一下就可以。第二步:编写 Jenkinsfile为了方便理解,我们从简易模板开始,分别修改对应阶段的任务。1、配置构建环境,「CODING 持续集成」目前支持 java-8、python-3.5、ruby-2.3、go-1.11 等等。在 Jenkinsfile 的 pipeline 里添加: agent { // 此处设定构建环境,目前可选有 // default, java-8, python-3.5, ruby-2.3, go-1.11 等 // 详情请阅 https://dev.tencent.com/help/knowledge-base/how-to-use-ci#agents label “java-8” }2、检出这里不得不说,「CODING 持续集成」这里做的还是很方便的,提供了适用于好几种不同场景的模板。默认简易模板是带有检出部分的,我们可以根据实际情况进行修改。默认情况下,env.GIT_BUILD_REF 的值就是 master 主分支,实际上我们可以定制为其它专门用于构建发的分支。这里,大家可以自己修改具体要检出的分支。 stage(“检出”) { steps { sh ‘ci-init’ checkout( [$class: ‘GitSCM’, branches: [[name: env.GIT_BUILD_REF]], userRemoteConfigs: [[url: env.GIT_REPO_URL]]] ) } }3、构建 stage(“构建”) { steps { echo “构建中…” sh ‘java -version’ sh ‘mvn package’ echo “构建完成.” archiveArtifacts artifacts: ‘/target/.jar’, fingerprint: true // 收集构建产物 } }这里需要注意,Spring Boot 的 pom 中需要添加一个插件。修改后: <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <!– 下面是添加的插件 –> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.6</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin>4、测试这里我偷个懒,只做了单元测试,没有提取测试报告,大家可以根据实际项目定制这个流程。 stage(“测试”) { steps { echo “单元测试中…” sh ‘mvn test’ echo “单元测试完成.” //junit ’target/surefire-reports/.xml’ // 收集单元测试报告的调用过程 } }5、分发 jar 包到目标服务器这里比较无奈,我没有单独针对这次演示写部署 jar 包和上传 jar 包的 webhookApi,但是构建好的 jar 包需要要放置到待部署的服务器。于是有了这个过程,借助 scp 和私钥来上传构建好的jar包。这里千万记着提前部署好密钥。并且将密钥放到仓库一份,用于分发jar包。 stage(“分发jar包”) { steps { echo “分发中…” echo “chmod 600 pkey” sh ‘chmod 600 authorized_keys.pem’ echo “upload” sh ‘scp -i authorized_keys.pem ./target/*.jar root@yourip:/root/’ echo “准备部署” } }6、部署前面有提到,这里部署仍然需要触发一个钩子,否则只能手动部署了。这里我写了一个最简单的,实际上我们可以写细致一点,判断一下接口返回的结果再根据结果输出部署情况。 stage(“部署”) { steps { sh ‘curl http://youapi’ echo “部署完毕” } }第三步:保存 Jenkinsfile 并运行修改好 Jenkinsfile 和 pom.xml。我们要保存 Jenkinsfile,编辑框可以直接编辑内容,编辑好可以直接提交到仓库下的 ./Jenkinsfile 接下来, 平台会自动读取 Jenkinsfile 并开始走持续集成的流程:持续集成的流程是可以看到的:每个阶段都对应 Jenkinsfile 一个 stage, 我们可以点击查看对应阶段的构建结果。第四步:排查持续集成的报错如果某个过程出错,「CODING 持续集成」的流程会停止,并提示失败。此时我们可以进入具体节点查看具体失败原因。比如现在是提示“分发 jar 包失败”,那么我们可以点击对应节点展开看看日志,排查具体分发失败的原因。现在可以清晰地看到,报错原因是我没有填写正确的主机 ip。文中涉及的文件及代码Jenkinsfilepipeline { agent { // 此处设定构建环境,目前可选有 // default, java-8, python-3.5, ruby-2.3, go-1.11 等 // 详情请阅 https://dev.tencent.com/help/knowledge-base/how-to-use-ci#agents label “java-8” } stages { // 检出仓库 stage(“检出”) { steps { // 这里sh调用ci-init 初始化 sh ‘ci-init’ // 这里检出仓库,默认检出分支为环境变量中的GIT_BUILD_REF checkout( [$class: ‘GitSCM’, branches: [[name: env.GIT_BUILD_REF]], userRemoteConfigs: [[url: env.GIT_REPO_URL]]] ) } } // 构建jar包 stage(“构建”) { steps { echo “构建中…” // 输出java版本 sh ‘java -version’ // 调用maven 构建jar包 sh ‘mvn package’ echo “构建完成.” //收集构建产物,这一步成功,我们就可以在平台上看到构建产物 archiveArtifacts artifacts: ‘/target/.jar’, fingerprint: true // 收集构建产物 } } // 测试 stage(“测试”) { steps { echo “单元测试中…” // 做单元测试 sh ‘mvn test’ echo “单元测试完成.” } } // 分发jar包,这里只是简单的通过scp分发jar包到目标机器指定目录 stage(“分发jar包”) { steps { echo “分发中…” echo “chmod 600 pkey” sh ‘chmod 600 authorized_keys.pem’ echo “upload” sh ‘scp -i authorized_keys.pem ./target/.jar root@youip:/root/’ echo “准备部署” } } // 部署jar包 stage(“部署”) { // 这里需要触发一个部署的webhook,可以是一个很简单的重启java进程的操作 steps { // 用curl 来触发hook sh ‘curl http://baidu.com’ echo “请登录服务器手动部署” } } }}pom.xml文中所用 Spring Boot 示例项目的 pom.xml实际上,大家可以直接去 start.spring.io 参考照这份 pom 来创建一个 demo。<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.2.RELEASE</version> <relativePath/> <!– lookup parent from repository –> </parent> <groupId>tech.hejian</groupId> <artifactId>codingj8</artifactId> <version>0.0.1-SNAPSHOT</version> <name>codingj8</name> <description>coding project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.6</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin> </plugins> </build></project>总结CODING 是一个面向开发者的云端开发平台,提供 Git/SVN 代码托管、任务管理、在线 WebIDE、Cloud Studio、开发协作、文件管理、Wiki 管理、提供个人服务及企业服务,其中「CODING 持续集成」集成了 SaaS 化的 Jenkins 等主流企业开发流程工具,实现了 DevOps 流程全自动化,为企业提供软件研发全流程管理工具,打通了从团队构建、产品策划、开发测试到部署上线的全过程。 ...

March 13, 2019 · 3 min · jiezi

程序员笔记——springboot 之常用注解

在spring boot中,摒弃了spring以往项目中大量繁琐的配置,遵循约定大于配置的原则,通过自身默认配置,极大的降低了项目搭建的复杂度。同样在spring boot中,大量注解的使用,使得代码看起来更加简洁,提高开发的效率。这些注解不光包括spring boot自有,也有一些是继承自spring的。 本文中将spring boot项目中常用的一些核心注解归类总结,并结合实际使用的角度来解释其作用。项目配置注解1、@SpringBootApplication 注解查看源码可发现,@SpringBootApplication是一个复合注解,包含了@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan这三个注解。这三个注解的作用分别为:@SpringBootConfiguration:标注当前类是配置类,这个注解继承自@Configuration。并会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到srping容器中,并且实例名就是方法名。@EnableAutoConfiguration:是自动配置的注解,这个注解会根据我们添加的组件jar来完成一些默认配置,我们做微服时会添加spring-boot-starter-web这个组件jar的pom依赖,这样配置会默认配置springmvc和tomcat。@ComponentScan:扫描当前包及其子包下被@Component,@Controller,@Service,@Repository注解标记的类并纳入到spring容器中进行管理。等价于<context:component-scan>的xml配置文件中的配置项。大多数情况下,这3个注解会被同时使用,基于最佳实践,这三个注解就被做了包装,成为了@SpringBootApplication注解。2、@ServletComponentScan:Servlet、Filter、Listener 可以直接通过 @WebServlet、@WebFilter、@WebListener 注解自动注册,这样通过注解servlet ,拦截器,监听器的功能而无需其他配置,所以这次相中使用到了filter的实现,用到了这个注解。3、@MapperScan:spring-boot支持mybatis组件的一个注解,通过此注解指定mybatis接口类的路径,即可完成对mybatis接口的扫描。它和@mapper注解是一样的作用,不同的地方是扫描入口不一样。@mapper需要加在每一个mapper接口类上面。所以大多数情况下,都是在规划好工程目录之后,通过@MapperScan注解配置路径完成mapper接口的注入。添加mybatis相应组建依赖之后。就可以使用该注解。进一步查看mybatis-spring-boot-starter包,可以找到这里已经将mybatis做了包装。这也是spring的一个理念,不重复造轮子,整合优秀的资源进入spring的体系中。4、资源导入注解:@ImportResource @Import @PropertySource 这三个注解都是用来导入自定义的一些配置文件。@ImportResource(locations={}) 导入其他xml配置文件,需要标准在主配置类上。导入property的配置文件 @PropertySource指定文件路径,这个相当于使用spring的<importresource/>标签来完成配置项的引入。@import注解是一个可以将普通类导入到spring容器中做管理controller 层1、@Controller 表明这个类是一个控制器类,和@RequestMapping来配合使用拦截请求,如果不在method中注明请求的方式,默认是拦截get和post请求。这样请求会完成后转向一个视图解析器。但是在大多微服务搭建的时候,前后端会做分离。所以请求后端只关注数据处理,后端返回json数据的话,需要配合@ResponseBody注解来完成。这样一个只需要返回数据的接口就需要3个注解来完成,大多情况我们都是需要返回数据。也是基于最佳实践,所以将这三个注解进一步整合。@RestController 是@Controller 和@ResponseBody的结合,一个类被加上@RestController 注解,数据接口中就不再需要添加@ResponseBody。更加简洁。同样的情况,@RequestMapping(value="",method= RequestMethod.GET ),我们都需要明确请求方式。这样的写法又会显得比较繁琐,于是乎就有了如下的几个注解。这几个注解是 @RequestMapping(value="",method= RequestMethod.xxx )的最佳实践。为了代码的更加简洁。2、@CrossOrigin:@CrossOrigin(origins = “”, maxAge = 1000) 这个注解主要是为了解决跨域访问的问题。这个注解可以为整个controller配置启用跨域,也可以在方法级别启用。我们在项目中使用这个注解是为了解决微服在做定时任务调度编排的时候,会访问不同的spider节点而出现跨域问题。3、@Autowired:这是个最熟悉的注解,是spring的自动装配,这个个注解可以用到构造器,变量域,方法,注解类型上。当我们需要从bean 工厂中获取一个bean时,Spring会自动为我们装配该bean中标记为@Autowired的元素。4、@EnablCaching@EnableCaching: 这个注解是spring framework中的注解驱动的缓存管理功能。自spring版本3.1起加入了该注解。其作用相当于spring配置文件中的cache manager标签。5、@PathVariable:路径变量注解,@RequestMapping中用{}来定义url部分的变量名,如:同样可以支持变量名加正则表达式的方式,变量名:[正则表达式]。servcie层注解1、@Service:这个注解用来标记业务层的组件,我们会将业务逻辑处理的类都会加上这个注解交给spring容器。事务的切面也会配置在这一层。当让 这个注解不是一定要用。有个泛指组件的注解,当我们不能确定具体作用的时候 可以用泛指组件的注解托付给spring容器。 2、@Resource:@Resource和@Autowired一样都可以用来装配bean,都可以标注字段上,或者方法上。 @resource注解不是spring提供的,是属于J2EE规范的注解。两个之前的区别就是匹配方式上有点不同,@Resource默认按照名称方式进行bean匹配,@Autowired默认按照类型方式进行bean匹配。持久层注解1、@Repository:@Repository注解类作为DAO对象,管理操作数据库的对象。总得来看,@Component, @Service, @Controller, @Repository是spring注解,注解后可以被spring框架所扫描并注入到spring容器来进行管理@Component是通用注解,其他三个注解是这个注解的拓展,并且具有了特定的功能。通过这些注解的分层管理,就能将请求处理,义务逻辑处理,数据库操作处理分离出来,为代码解耦,也方便了以后项目的维护和开发。所以我们在正常开发中,如果能用@Service, @Controller, @Repository其中一个标注这个类的定位的时候,就不要用@Component来标注。2、@Transactional: 通过这个注解可以声明事务,可以添加在类上或者方法上。在spring boot中 不用再单独配置事务管理,一般情况是我们会在servcie层添加了事务注解,即可开启事务。要注意的是,事务的开启只能在public 方法上。并且主要事务切面的回滚条件。正常我们配置rollbackfor exception时 ,如果在方法里捕获了异常就会导致事务切面配置的失效。其他相关注解@ControllerAdvice 和 @RestControllerAdvice:通常和@ExceptionHandler、@InitBinder、@ModelAttribute一起配合使用。@ControllerAdvice 和 @ExceptionHandler 配合完成统一异常拦截处理。@RestControllerAdvice 是 @ControllerAdvice 和 @ResponseBody的合集,可以将异常以json的格式返回数据。如下面对数据异常返回的统一处理。这里是对平时用到的一些注解做了归纳,及应用说明。还有其他更深的知识还需要在后续的用中继续学习。参考文档:https://docs.spring.io/spring…https://spring.io/projects/sp…本文作者:缑应奎来源:宜信技术学院

March 12, 2019 · 1 min · jiezi

Springboot 初步

一年前曾经使用Springboot实现了一个规模很小的记录用户登录情况的service, 仅仅是获取用户信息,然后插入到数据库中. 这个功能非常简单, 简单到我花在配置server(Tomcat)的certificate以及维护(修改数据库密码)上的精力要远大于写code的部分, 毕竟code的核心部分只是一个Java类而已. 但在当时使用springboot期间, 看了Springboot的官方文档, 收获很多, 现在遗憾的是没有把当时的收获记录下来, 而现在难得不算很忙, 所以尽量抽时间把springboot重新复习一遍, 学习的过程记录一下, 而且这也会是个长期的记录.

March 11, 2019 · 1 min · jiezi

ddd-lite-codegen 样板代码终结者

ddd-lite-codegen基于 ddd lite 和 ddd lite spring 体系构建,基于领域模型对象自动生成其他非核心代码。0. 运行原理ddd lite codegen 构建于 apt 技术之上。框架提供若干注解和注解处理器,在编译阶段,自动生成所需的 Base 类。这些 Base 类随着领域对象的重构而变化,从而大大减少样板代码。如有特殊需求,可以通过子类进行扩展,而无需修改 Base 类的内容(每次编译,Base 类都会自动生成)。1. 配置该框架采用两步处理,第一步由 maven 的 apt plugin 完成,第二步由编译器调用 apt 组件完成。对于 maven 项目,需要添加相关依赖和插件,具体如下:<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-demo</artifactId> <version>1.0.0-SNAPSHOT</version> <parent> <groupId>com.geekhalo</groupId> <artifactId>gh-base-parent</artifactId> <version>1.0.0-SNAPSHOT</version> </parent> <properties> <service.name>demo</service.name> <server.name>gh-${service.name}-service</server.name> <server.version>v1</server.version> <server.description>${service.name} Api</server.description> <servlet.basePath>/${service.name}-api</servlet.basePath> </properties> <dependencies> <!– 添加 ddd 相关支持–> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-spring</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> <!– 添加 code gen 依赖,将自动启用 EndpointCodeGenProcessor 处理器–> <!–编译时有效即可,运行时,不需要引用–> <dependency> <groupId>com.geekhalo</groupId> <artifactId>gh-ddd-lite-codegen</artifactId> <version>1.0.1-SNAPSHOT</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <scope>provided</scope> </dependency> <!– 持久化主要由 Spring Data 提供支持–> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-mongodb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <!– 添加测试支持–> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!– 添加 Swagger 支持–> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processors> <!–添加 Querydsl 处理器–> <processor>com.querydsl.apt.QuerydslAnnotationProcessor</processor> <!–添加 DDD 处理器–> <processor>com.geekhalo.ddd.lite.codegen.DDDCodeGenProcessor</processor> </processors> </configuration> </execution> </executions> </plugin> </plugins> </build></project>2. GenCreator领域对象为限界上下文中受保护对象,绝对不应该将其暴露到外面。因此,在创建一个新的领域对象时,需要一种机制将所需数据传递到模型中。常用的机制就是将创建时所需数据封装成一个 dto 对象,通过这个 dto 对象来传递数据,领域对象从 dto 中提取所需数据,完成对象创建工作。creator 就是这种特殊的 Dto,在封装创建对象所需的数据的同时,提供数据到领域对象的绑定操作。2.1 常规做法假设,现在有一个 Person 类:@Datapublic class Person { private String name; private Date birthday; private Boolean enable;}我们需要创建新的 Person 对象,比较正统的方式便是,创建一个 PersonCreator,用于封装所需数据:@Datapublic class PersonCreator { private String name; private Date birthday; private Boolean enable;}然后,在 Person 中添加创建方法,如:public static Person create(PersonCreator creator){ Person person = new Person(); person.setName(creator.getName()); person.setBirthday(creator.getBirthday()); person.setEnable(creator.getEnable()); return person;}大家有没有发现问题:Person 和 PersonCreator 包含的属性基本相同如果在 Person 中添加、移除、修改属性,会同时调整三处(Person、PersonCreator、create 方法),遗漏任何一处,都会导致逻辑错误对于这种机械而且有规律的场景,是否可以采用自动化方式完成?2.2 @GenCreator@GenCreator 便是基于此场景产生的。2.2.1 启用 GenCreator新建 Person 类,在类上添加 @GenCreator 注解。@GenCreator@Datapublic class Person extends JpaAggregate{ private String name; private Date birthday; private Boolean enable;}2.2.2 编译代码,生成 BaseXXXXCreator 类执行 mvn clean compile 命令,在 target/generated-sources/java 对应包下,会出现一个 BasePersonCreator 类,如下:@Datapublic abstract class BasePersonCreator<T extends BasePersonCreator> { @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “birthday” ) private Date birthday; @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “enable” ) private Boolean enable; @Setter(AccessLevel.PUBLIC) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “name” ) private String name; public void accept(Person target) { target.setBirthday(getBirthday()); target.setEnable(getEnable()); target.setName(getName()); }}该类含有与 Person 一样的属性,并提供 accept 方法,对 person 对象执行对应属性的 set 操作。2.2.3 构建 PersonCreator基于 BasePersonCreator 创建 PersonCreator 类。public class PersonCreator extends BasePersonCreator<PersonCreator>{}2.2.4 添加静态 create 方法使用 PersonCreator 为 Person 提供静态工厂方法。@GenCreator@Datapublic class Person extends JpaAggregate{ private String name; private Date birthday; private Boolean enable; public static Person create(PersonCreator creator){ Person person = new Person(); creator.accept(person); return person; }}以后 Person 属性的变化,将自动应用于 BasePersonCreator 中,程序的其他部分没有任何改变。2.3 运行原理@GenCreator 运行原理如下:自动读取当前类的 setter 方法;筛选 public 和 protected 访问级别的 setter 方法,将其作为属性添加到 BaseXXXCreator 类中;创建 accept 方法,读取 BaseXXXXCreator 的属性,并通过 setter 方法写回业务数据。对于不需要添加到 Creator 的 setter 方法,可以使用 @GenCreatorIgnore 忽略该方法。细心的同学可能注意到,在 BaseXXXXCreator 类的属性上存在 @ApiModelProperty 注解,该注解为 Swagger 注解,用于生成 Swagger 文档。我们可以使用 @Description 注解,标注字段描述信息,这些信息会自动添加的 Swagger 文档中。3. GenUpdaterGenUpdater 和 GenCreator 非常相似,主要应用于对象修改场景。相对于创建,对象修改场景有点特殊,即对 null 的处理,当用户传递 null 进来,不知道是属性不修改还是属性设置为 null。针对这种场景,常用方案是将其包装在一个 Optional 中,如果 Optional 对应的属性为 null,表示对该属性不做处理;如果 Optional 中包含的 value 为null,表示将属性值设置为 null。3.1 启用 GenUpdater在 Person 类上添加 @GenUpdater 注解。@GenUpdater@GenCreator@Datapublic class Person extends JpaAggregate{ @Description(“名称”) private String name; private Date birthday; private Boolean enable; public static Person create(PersonCreator creator){ Person person = new Person(); creator.accept(person); return person; }}3.2 编译代码,生成 Base 类执行 mvn clean compile, 生成 BasePersonUpdater。@Datapublic abstract class BasePersonUpdater<T extends BasePersonUpdater> { @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “birthday” ) private DataOptional<Date> birthday; @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “enable” ) private DataOptional<Boolean> enable; @Setter(AccessLevel.PRIVATE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “名称”, name = “name” ) private DataOptional<String> name; public T birthday(Date birthday) { this.birthday = DataOptional.of(birthday); return (T) this; } public T acceptBirthday(Consumer<Date> consumer) { if(this.birthday != null){ consumer.accept(this.birthday.getValue()); } return (T) this; } public T enable(Boolean enable) { this.enable = DataOptional.of(enable); return (T) this; } public T acceptEnable(Consumer<Boolean> consumer) { if(this.enable != null){ consumer.accept(this.enable.getValue()); } return (T) this; } public T name(String name) { this.name = DataOptional.of(name); return (T) this; } public T acceptName(Consumer<String> consumer) { if(this.name != null){ consumer.accept(this.name.getValue()); } return (T) this; } public void accept(Person target) { this.acceptBirthday(target::setBirthday); this.acceptEnable(target::setEnable); this.acceptName(target::setName); }}该类与 BasePersonCreator 存在一些差异:属性使用 DataOptional<T> 进行包装;每个属性提供 T fieldName(FieldType fieldName) 方法,用于设置对应的属性值;每个属性提供 T acceptFieldName(Consumer<FieldType> consumer) 方法,在 DataOptional 属性不为空的时候,进行业务处理;提供 void accept(Target target) 方法,将 BaseXXXXUpdater 中的数据写回到 Target 对象中。与 BaseXXXCreator 类似,BaseXXXUpdater 也提供 @GenUpdaterIgnore 注解,对方法进行忽略;也可使用 @Description 注解生成 Swagger 文档描述。备注 GenUpdate 与 GenCreator 最大差别在于,Updater 机制,只会应用于 public 的 setter 方法。因此,对于不需要更新的属性,可以使用 protected 访问级别,这样只会在 creator 中存在。3.3 创建 PersonUpdater 类创建 PersonUpdater 类继承 BasePersonUpdater。public class PersonUpdater extends BasePersonUpdater<PersonUpdater>{}3.4 创建 update 方法为 Person 类 添加 update 方法public void update(PersonUpdater updater){ updater.accept(this);}4. genDtodto 是大家最熟悉的模式,但这里的 dto,只针对返回数据。请求数据,统一使用 Creator 和 Updater 完成。4.1 启用 GenDto为 Person 类添加 @GenDto 注解。@GenUpdater@GenCreator@GenDto@Datapublic class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; public static Person create(PersonCreator creator){ Person person = new Person(); creator.accept(person); return person; } public void update(PersonUpdater updater){ updater.accept(this); }}4.2 编译代码,生成 Base 类执行 mvn clean compile 生成 BasePersonDto,如下:@Datapublic abstract class BasePersonDto extends AbstractAggregateDto implements Serializable { @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “birthday” ) private Date birthday; @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “”, name = “enable” ) private Boolean enable; @Setter(AccessLevel.PACKAGE) @Getter(AccessLevel.PUBLIC) @ApiModelProperty( value = “名称”, name = “name” ) private String name; protected BasePersonDto(Person source) { super(source); this.setBirthday(source.getBirthday()); this.setEnable(source.getEnable()); this.setName(source.getName()); }}4.3 新建 PersonDto新建 PersonDto 继承自 BasePersonDto。public class PersonDto extends BasePersonDto{ public PersonDto(Person source) { super(source); }}4.3 @GenDto 生成策略@GenDto 生成策略如下:查找类所有的 public getter 方法;为每个 getter 方法添加属性;新建构造函数,在构造函数中完成目标对象到 BaseXXXDto 的属性赋值。5. genConverterconverter 主要针对使用 Jpa 作为存储的场景。5.1 设计背景Jpa 对 enum 类型提供了两种存储方式:存储 enum 的名称;存储 enum 的定义顺序。这两者在使用上都存在一定的问题,通常情况下,需要存储自定义 code,因此,需要实现枚举类型的 Converter。5.2 @GenCodeBasedEnumConverter5.2.1 启用 GenCodeBasedEnumConverter新建 PersonStatus 枚举,实现 CodeBasedEnum 接口,添加 @GenCodeBasedEnumConverter 注解。@GenCodeBasedEnumConverterpublic enum PersonStatus implements CodeBasedEnum<PersonStatus> { ENABLE(1), DISABLE(0); private final int code; PersonStatus(int code) { this.code = code; } @Override public int getCode() { return this.code; }}5.2.2 编译代码,生成 CodeBasedPersonStatusConverter执行 mvn clean compile 命令,自动生成 CodeBasedPersonStatusConverter 类public final class CodeBasedPersonStatusConverter implements AttributeConverter<PersonStatus, Integer> { public Integer convertToDatabaseColumn(PersonStatus i) { return i == null ? null : i.getCode(); } public PersonStatus convertToEntityAttribute(Integer i) { if (i == null) return null; for (PersonStatus value : PersonStatus.values()){ if (value.getCode() == i){ return value; } } return null; }}5.2.3 应用 CodeBasedPersonStatusConverter在 Person 中使用 CodeBasedPersonStatusConverter 转化器:public class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; @Convert(converter = CodeBasedPersonStatusConverter.class) private PersonStatus status;}6. genRepositoryRepository 是领域驱动设计中很重要的一个组件,一个聚合根对于一个 Repository。Repository 与基础设施关联紧密,框架通过 @GenSpringDataRepository 提供了 Spring Data Repository 的支持。6.1 启用 GenSpringDataRepository在 Person 上添加 @GenSpringDataRepository 注解。@GenSpringDataRepository@Datapublic class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; @Convert(converter = CodeBasedPersonStatusConverter.class) private PersonStatus status;}6.2 编译代码,生成 Base 类执行 mvn clean compile 生成 BasePersonRepository 类interface BasePersonRepository extends AggregateRepository<Long, Person>, Repository<Person, Long>, QuerydslPredicateExecutor<Person> {}该接口实现了 AggregateRepository<Long, Person>、Repository<Person, Long>、QuerydslPredicateExecutor<Person> 三个接口,其中 AggregateRepository 为 ddd-lite 框架接口,另外两个为 spring data 接口。6.3 创建 PersonRepository创建 PersonRepository 继承自 BasePersonRepository。public interface PersonRepository extends BasePersonRepository{}6.4 使用 PersonRepositoryPersonRepository 为 Spring Data 标准定义接口,Spring Data 会为其自动创建代理类,无需我们实现便可以直接注入使用。6.5 Index 支持一般情况下,PersonRepository 中的方法能够满足我们大多数需求,如果存在关联关系,可以使用 @Index 处理。在 Person 中,添加 @Index({“name”, “status”}) 和 @QueryEntity 注解@GenSpringDataRepository@Index({“name”, “status”})@QueryEntity@Datapublic class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; @Convert(converter = CodeBasedPersonStatusConverter.class) private PersonStatus status;}执行 mvn clean compile,查看生成的 PersonRepositoryinterface BasePersonRepository extends AggregateRepository<Long, Person>, Repository<Person, Long>, QuerydslPredicateExecutor<Person> { Long countByName(String name); default Long countByName(String name, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(predicate); return this.count(booleanBuilder.getValue()); } Long countByNameAndStatus(String name, PersonStatus status); default Long countByNameAndStatus(String name, PersonStatus status, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(QPerson.person.status.eq(status));; booleanBuilder.and(predicate); return this.count(booleanBuilder.getValue()); } List<Person> getByName(String name); List<Person> getByName(String name, Sort sort); default List<Person> getByName(String name, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue())); } default List<Person> getByName(String name, Predicate predicate, Sort sort) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue(), sort)); } List<Person> getByNameAndStatus(String name, PersonStatus status); List<Person> getByNameAndStatus(String name, PersonStatus status, Sort sort); default List<Person> getByNameAndStatus(String name, PersonStatus status, Predicate predicate) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(QPerson.person.status.eq(status));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue())); } default List<Person> getByNameAndStatus(String name, PersonStatus status, Predicate predicate, Sort sort) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(QPerson.person.status.eq(status));; booleanBuilder.and(predicate); return Lists.newArrayList(findAll(booleanBuilder.getValue(), sort)); } Page<Person> findByName(String name, Pageable pageable); default Page<Person> findByName(String name, Predicate predicate, Pageable pageable) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(predicate); return findAll(booleanBuilder.getValue(), pageable); } Page<Person> findByNameAndStatus(String name, PersonStatus status, Pageable pageable); default Page<Person> findByNameAndStatus(String name, PersonStatus status, Predicate predicate, Pageable pageable) { BooleanBuilder booleanBuilder = new BooleanBuilder(); booleanBuilder.and(QPerson.person.name.eq(name));; booleanBuilder.and(QPerson.person.status.eq(status));; booleanBuilder.and(predicate); return findAll(booleanBuilder.getValue(), pageable); }}根据索引信息,BasePersonRepository 自动生成了各种查询,这些查询也不用我们自己去实现,直接使用即可。7. GenApplicationapplication 是领域模型最直接的使用者。通常情况下,Application 会涵盖聚合根中的 Command 方法、Repository 中的查询方法、DomainService 的操作方法。其中又以聚合根中的 Command 方法和 Repository 中的查询方法为主。框架为此提供了自动生成 BaseXXXApplication 的支持。框架提供 @GenApplication 注解,作为自动生成的入口。7.1 聚合根的 Command将 @GenApplication 添加到聚合根上,框架自动识别 Command 的方法,并将其添加到 BaseApplication 中。7.1.1 启用 GenApplication在 Person 中添加 @GenApplication 注解,为了更好的演示 Command 方法,新增 enable 和 disable 两个方法:@GenApplication@Datapublic class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; @Convert(converter = CodeBasedPersonStatusConverter.class) private PersonStatus status; public static Person create(PersonCreator creator){ Person person = new Person(); creator.accept(person); return person; } public void update(PersonUpdater updater){ updater.accept(this); } public void enable(){ setStatus(PersonStatus.ENABLE); } public void disable(){ setStatus(PersonStatus.DISABLE); }}7.1.2 编译代码,生成 Base 类执行 mvn clean compile,生成 BasePersonApplication 接口和 BasePersonApplicationSupport 实现类。BasePersonApplication 如下:public interface BasePersonApplication { Long create(PersonCreator creator); void disable(@Description(“主键”) Long id); void update(@Description(“主键”) Long id, PersonUpdater updater); void enable(@Description(“主键”) Long id);}BasePersonApplication 主要做了如下工作:对于 Person 的 create 静态工厂方法,将自动创建 create 方法;对于 Person 的返回为 void 方法(非 setter 方法),将自创建为 command 方法,为其增加一个主键参数。BasePersonApplicationSupport 如下:abstract class BasePersonApplicationSupport extends AbstractApplication implements BasePersonApplication { @Autowired private DomainEventBus domainEventBus; @Autowired private PersonRepository personRepository; protected BasePersonApplicationSupport(Logger logger) { super(logger); } protected BasePersonApplicationSupport() { } protected PersonRepository getPersonRepository() { return this.personRepository; } protected DomainEventBus getDomainEventBus() { return this.domainEventBus; } @Transactional public Long create(PersonCreator creator) { Person result = creatorFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .instance(() -> Person.create(creator)) .call(); logger().info(“success to create {} using parm {}",result.getId(), creator); return result.getId(); } @Transactional public void disable(@Description(“主键”) Long id) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.disable()) .call(); logger().info(“success to disable for {} using parm “, id); } @Transactional public void update(@Description(“主键”) Long id, PersonUpdater updater) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.update(updater)) .call(); logger().info(“success to update for {} using parm {}”, id, updater); } @Transactional public void enable(@Description(“主键”) Long id) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.enable()) .call(); logger().info(“success to enable for {} using parm “, id); }}BasePersonApplicationSupport 主要完成工作如下:自动注入 DomainEventBus 和 PersonRepository 等相关资源;实现聚合根中的 Command 方法,并为其开启事务支持。7.1.3 创建 PersonApplication 和 PersonApplicationImpl创建 PersonApplication 和 PersonApplicationImpl 类,具体如下:PersonApplication:public interface PersonApplication extends BasePersonApplication{}PersonApplicationImpl:@Servicepublic class PersonApplicationImpl extends BasePersonApplicationSupport implements PersonApplication {}7.2 Repository 中的 Query将 @GenApplication 添加到 Repository 上,框架将当前接口中的方法作为 Query 方法,自动添加到 Application 中。7.2.1 启用 GenApplication在 PersonRepository 添加 @GenApplication 注解;@GenApplicationpublic interface PersonRepository extends BasePersonRepository{ }7.2.2 添加查询方法在 PersonRepository 中添加要暴露的方法:@GenApplicationpublic interface PersonRepository extends BasePersonRepository{ @Override Page<Person> findByName(String name, Pageable pageable); @Override Page<Person> findByNameAndStatus(String name, PersonStatus status, Pageable pageable);}7.2.3 编译代码,生成 Base 类执行 mvn clean compile 查看现在的 BasePersonApplication 和 BasePersonApplicationSupport。BasePersonApplication:public interface BasePersonApplication { Long create(PersonCreator creator); void update(@Description(“主键”) Long id, PersonUpdater updater); void enable(@Description(“主键”) Long id); void disable(@Description(“主键”) Long id); Page<PersonDto> findByName(String name, Pageable pageable); Page<PersonDto> findByNameAndStatus(String name, PersonStatus status, Pageable pageable);}可见,查询方法已经添加到 BasePersonApplication 中。BasePersonApplicationSupport:abstract class BasePersonApplicationSupport extends AbstractApplication implements BasePersonApplication { @Autowired private DomainEventBus domainEventBus; @Autowired private PersonRepository personRepository; protected BasePersonApplicationSupport(Logger logger) { super(logger); } protected BasePersonApplicationSupport() { } protected PersonRepository getPersonRepository() { return this.personRepository; } protected DomainEventBus getDomainEventBus() { return this.domainEventBus; } @Transactional public Long create(PersonCreator creator) { Person result = creatorFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .instance(() -> Person.create(creator)) .call(); logger().info(“success to create {} using parm {}",result.getId(), creator); return result.getId(); } @Transactional public void update(@Description(“主键”) Long id, PersonUpdater updater) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.update(updater)) .call(); logger().info(“success to update for {} using parm {}”, id, updater); } @Transactional public void enable(@Description(“主键”) Long id) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.enable()) .call(); logger().info(“success to enable for {} using parm “, id); } @Transactional public void disable(@Description(“主键”) Long id) { Person result = updaterFor(this.getPersonRepository()) .publishBy(getDomainEventBus()) .id(id) .update(agg -> agg.disable()) .call(); logger().info(“success to disable for {} using parm “, id); } protected <T> List<T> convertPersonList(List<Person> src, Function<Person, T> converter) { if (CollectionUtils.isEmpty(src)) return Collections.emptyList(); return src.stream().map(converter).collect(Collectors.toList()); } protected <T> Page<T> convvertPersonPage(Page<Person> src, Function<Person, T> converter) { return src.map(converter); } protected abstract PersonDto convertPerson(Person src); protected List<PersonDto> convertPersonList(List<Person> src) { return convertPersonList(src, this::convertPerson); } protected Page<PersonDto> convvertPersonPage(Page<Person> src) { return convvertPersonPage(src, this::convertPerson); } @Transactional( readOnly = true ) public <T> Page<T> findByName(String name, Pageable pageable, Function<Person, T> converter) { Page<Person> result = this.getPersonRepository().findByName(name, pageable); return convvertPersonPage(result, converter); } @Transactional( readOnly = true ) public Page<PersonDto> findByName(String name, Pageable pageable) { Page<Person> result = this.getPersonRepository().findByName(name, pageable); return convvertPersonPage(result); } @Transactional( readOnly = true ) public <T> Page<T> findByNameAndStatus(String name, PersonStatus status, Pageable pageable, Function<Person, T> converter) { Page<Person> result = this.getPersonRepository().findByNameAndStatus(name, status, pageable); return convvertPersonPage(result, converter); } @Transactional( readOnly = true ) public Page<PersonDto> findByNameAndStatus(String name, PersonStatus status, Pageable pageable) { Page<Person> result = this.getPersonRepository().findByNameAndStatus(name, status, pageable); return convvertPersonPage(result); }}与上个版本相比,新增以下逻辑:添加 convertPersonList、convertPersonPage等转化方法;添加 convertPerson 抽象方法,用于完成 Person 到 PersonDto 的转化;添加 findByNameAndStatus 和 findByName 相关查询方法,并将其标准为只读。7.2.4 调整 PersonApplicationImpl为 PersonApplicationImpl 添加 convertPerson 实现。@Servicepublic class PersonApplicationImpl extends BasePersonApplicationSupport implements PersonApplication { @Override protected PersonDto convertPerson(Person src) { return new PersonDto(src); }}至此,对领域的支持就介绍完了,我们看下我们的 Person 类。@GenUpdater@GenCreator@GenDto@GenSpringDataRepository@Index({“name”, “status”})@QueryEntity@GenApplication@Datapublic class Person extends JpaAggregate { @Description(“名称”) private String name; @Setter(AccessLevel.PROTECTED) private Date birthday; private Boolean enable; @Convert(converter = CodeBasedPersonStatusConverter.class) private PersonStatus status; public static Person create(PersonCreator creator){ Person person = new Person(); creator.accept(person); return person; } public void update(PersonUpdater updater){ updater.accept(this); } public void enable(){ setStatus(PersonStatus.ENABLE); } public void disable(){ setStatus(PersonStatus.DISABLE); }}在 Person 上有一堆的 @GenXXXX,感觉有点泛滥,对此,框架提供了两个符合注解,针对聚合和实体进行优化。8. EnableGenForEntity统一开启实体相关的代码生成器。@EnableGenForEntity 等同于同时开启如下注解:注解含义@GenCreator自动生成 BaseXXXCreator@GenDto自动生成 BaseXXXXDto@GenUpdater自动生成 BaseXXXXUpdater9. EnableGenForAggregate统一开启聚合相关的代码生成器。@EnableGenForAggregate 等同于同时开启如下注解:注解含义@GenCreator自动生成 BaseXXXCreator@GenDto自动生成 BaseXXXXDto@GenUpdater自动生成 BaseXXXXUpdaterGenSpringDataRepository自动生成基于 Spring Data 的 BaseXXXRepository@GenApplication自动生成 BaseXXXXApplication 以及实现类 BaseXXXXXApplicationSupport对于领域对象的支持,已经非常完成,那对于 Application 的调用者呢?框架提供了对于 Controller 的支持。10. GenController将 @GenController 添加到 Application 接口上,将启用对 Controller 的支持。10.1 启用 GenController在 PersonApplication 启用 Controller 支持。在 PersonApplication 接口上添加 @GenController(“com.geekhalo.ddd.lite.demo.controller.BasePersonController”)@GenController(“com.geekhalo.ddd.lite.demo.controller.BasePersonController”)public interface PersonApplication extends BasePersonApplication{}10.2 编译代码,生成 Base 类执行 mvn clean compile,查看生成的 BasePersonController 类:abstract class BasePersonController { @Autowired private PersonApplication application; protected PersonApplication getApplication() { return this.application; } @ResponseBody @ApiOperation( value = “”, nickname = “create” ) @RequestMapping( value = “/_create”, method = RequestMethod.POST ) public ResultVo<Long> create(@RequestBody PersonCreator creator) { return ResultVo.success(this.getApplication().create(creator)); } @ResponseBody @ApiOperation( value = “”, nickname = “disable” ) @RequestMapping( value = “{id}/_disable”, method = RequestMethod.POST ) public ResultVo<Void> disable(@PathVariable(“id”) Long id) { this.getApplication().disable(id); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “update” ) @RequestMapping( value = “{id}/_update”, method = RequestMethod.POST ) public ResultVo<Void> update(@PathVariable(“id”) Long id, @RequestBody PersonUpdater updater) { this.getApplication().update(id, updater); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “enable” ) @RequestMapping( value = “{id}/_enable”, method = RequestMethod.POST ) public ResultVo<Void> enable(@PathVariable(“id”) Long id) { this.getApplication().enable(id); return ResultVo.success(null); } @ResponseBody @ApiOperation( value = “”, nickname = “findByNameAndStatus” ) @RequestMapping( value = “/_find_by_name_and_status”, method = RequestMethod.POST ) public ResultVo<PageVo<PersonDto>> findByNameAndStatus(@RequestBody FindByNameAndStatusReq req) { return ResultVo.success(PageVo.apply(this.getApplication().findByNameAndStatus(req.getName(), req.getStatus(), req.getPageable()))); } @ResponseBody @ApiOperation( value = “”, nickname = “findByName” ) @RequestMapping( value = “/_find_by_name”, method = RequestMethod.POST ) public ResultVo<PageVo<PersonDto>> findByName(@RequestBody FindByNameReq req) { return ResultVo.success(PageVo.apply(this.getApplication().findByName(req.getName(), req.getPageable()))); }}该类主要完成:自动注入 PersonApplication;为 Command 中的 create 方法创建对于方法,并返回创建后的主键;为 Command 中的 update 方法创建对于方法,在 path 中添加主键参数,并返回 Void;为 Query 方法创建对于的方法;统一使用 ResultVo 作为返回值;对 Spring Data 中的 Pageable 和 Page 进行封装;对于多参数方法,创建封装类,使用封装类收集数据;添加 Swagger 相关注解。10.3 新建 PersonController新建 PersonController 实现 BasePersonController。并添加 RequestMapping,设置 base path。@RequestMapping(“person”)@RestControllerpublic class PersonController extends BasePersonController{}10.4 启动,查看 Swagger 文档至此,Controller 就开发好了,启动项目,输入 http://127.0.0.1:8080/swagger-ui.html 便可以看到相关接口。 ...

March 11, 2019 · 11 min · jiezi

SpringCloud注册中心Eureka

本篇概论在上一篇中我们介绍了微服务相关的内容。微服务的本质就是让服务与服务之间进行互相调用。那么在调用之前需要有一个前提。就是不同的服务与服务之间怎么知道彼此的存在的呢?因为服务都是独立部署的,根本没有任何关联。如果都不知道要调用的服务地址,那还怎么进行互相调用呢?为了解决这样的问题,于是SrpingCloud提供了注册中心的组件。我们在开发项目时,都向注册中心注册,在进行服务调用时,注册中心返回要调用服务的地址,这样就解决了上述问题了。创建Eureka服务端步骤在SrpingCloud中Eureka是注册中心的组件,通过Eureka我们可以很方便的搭建一个注册中心。在SrpingCloud中Eureka组件分为服务端和客户端。如果有Dubbo项目开发经验的人可以理解为Eureka中的服务端,就相当于zokeerper服务,也就是记录服务地址的。而Eureka中的客户端,即是我们Dubbo项目中真真正正的服务。下面我们来详细介绍一下,怎么搭建一个Eureka服务端。搭建一个Eureka服务端非常的简单,和创建一个SpringBoot的项目差不多,不同之处,就是添加依赖不一样。下面我们来详细介绍一下。在IDEA中选择Spring Initializr选项。也就是如下图所示:设置项目的相关参数,在这一点和创建SpringBoot的项目没有任何区别。这一块是最重要的,也就是唯一和创建SpringBoot项目不同的地方。这一步还是和SpringBoot项目一样,直接点击完成就可以了。下图就是项目的架构图,比较简单和SpringBoot一样,唯一的区别就是pom.xml中的不同,也就是依赖不同。下面我启动直接启动项目看一下运行结果。启动日志:com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server at com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute(RetryableEurekaHttpClient.java:112) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$1.execute(EurekaHttpClientDecorator.java:59) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient.execute(SessionedEurekaHttpClient.java:77) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.DiscoveryClient.register(DiscoveryClient.java:829) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.InstanceInfoReplicator.run(InstanceInfoReplicator.java:121) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.InstanceInfoReplicator$1.run(InstanceInfoReplicator.java:101) [eureka-client-1.9.8.jar:1.9.8] at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_191] at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_191] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_191] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_191] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_191] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_191] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_191]2019-03-09 18:20:09.617 INFO 1752 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ‘‘2019-03-09 18:20:09.618 INFO 1752 — [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8080我们发现和SpringBoot启动有一些不同,我们什么都没有改,也就是都是用默认的配置,启动的时候,居然抛出异常了。而在SpringBoot的项目中是可以正常启动的。虽然项目抛出了异常,但项目已经启动成功了,因为日志已经正确的输出了项目的端口了。下面我们直接访问这个端口,看看会返回什么信息。访问地址:http://127.0.0.1:8080@EnableEurekaServer注解看上面的返回结果我们应该很熟悉,这是因为我们没有写Controller导致的,在SpringBoot的文章中我们介绍过,这里就不详细介绍了。但这显然是不对的,因为刚刚我们介绍过SpringCloud中是使用Eureka来提供注册中心服务的,并且Eureka有客户端和服务端之分,所以我们只在项目添加了Eureka的依赖还是不够的,我们还要在项目的代码中添加注解,来标识当前的Eureka服务是客户端服务还是服务端服务。这也就是本篇介绍的第一个注解。也就是@EnableEurekaServer注解。我们只要将该注解添加到SpringCloud项目中的启动类中即可。具体代码如下:package com.jilinwula.eureka;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication@EnableEurekaServerpublic class JilinwulaSpringcloudEurekaServerApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudEurekaServerApplication.class, args); }}然后我们继续启动项目。在访问地址:http://127.0.0.1:8080看一下项目返回的结果。项目启动日志如下:com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server at com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient.execute(RetryableEurekaHttpClient.java:112) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator$1.execute(EurekaHttpClientDecorator.java:59) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient.execute(SessionedEurekaHttpClient.java:77) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator.register(EurekaHttpClientDecorator.java:56) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.DiscoveryClient.register(DiscoveryClient.java:829) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.InstanceInfoReplicator.run(InstanceInfoReplicator.java:121) ~[eureka-client-1.9.8.jar:1.9.8] at com.netflix.discovery.InstanceInfoReplicator$1.run(InstanceInfoReplicator.java:101) [eureka-client-1.9.8.jar:1.9.8] at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_191] at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_191] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_191] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_191] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_191] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_191] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_191]2019-03-09 18:42:29.427 INFO 1837 — [ Thread-19] o.s.c.n.e.server.EurekaServerBootstrap : isAws returned false2019-03-09 18:42:29.428 INFO 1837 — [ Thread-19] o.s.c.n.e.server.EurekaServerBootstrap : Initialized server context2019-03-09 18:42:29.461 INFO 1837 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ‘‘2019-03-09 18:42:29.461 INFO 1837 — [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8080看上述日志,项目启动时还是会抛出异常,我们先不用考虑,在后续的内容中我们在解释为什么启动时会抛出异常。还是和第一次启动一样,虽然启动抛出异常了,但项目还是启动成功了。下面我们继续访问以下地址,看一下访问结果。http://127.0.0.1:8080返回结果:自定义注册中心地址我们看这回返回的不是默认的错误页面了,而是返回了一个管理界面。这个管理界面就是SpringCloud中Eureka组建为我们提供的注册中心的管理界面,通过这个界面,我们可以非常方便的,来管理哪些服务注册成功、哪些注册失败以及服务其它状态等。看上图中的界面显示,现在没有任何一个服务注册成功。下面我们看一下刚刚项目启动时,抛出的异常。如果我们现在观察项目的启动日志,我们会发现,日志是每隔一段时间,就会抛出和启动时一样的异常。这是为什么呢?这是因为Eureka服务端和客户端是通过心跳方式检测的服务状态。刚刚我们通过@EnableEurekaServer注解启动了Eureka的服务端。实际上@EnableEurekaServer注解在底层实现时,除了标识项目为Eureka的服务端外,还会默认标识项目为Eureka的客户端。也就是通过@EnableEurekaServer注解标识的项目,默认即是Eureka的客户端还是Eureka的服务端。所以上述报错的原因就是Eureka的客户端与找到Eureka的服务端才抛出的异常。那怎么解决呢?既然我们知道了异常的根本原因,那我们解决就比较简单了,我们只要在项目中正确的配置Eureka的服务端的地址就可以解决上述的问题。具体配置如下。我们知道在创建SpringClourd项目默认会为我们创建application.properties文件,我们首先将该文件修改为yml文件(原因在之前的文章中已经介绍过了)。具体配置如下。application.yml配置:eureka: client: service-url: defaultZone: http://127.0.0.1:8080/eureka/配置完上述参数后,我们重新启动项目,然后在观察一下日志,看看是不是还会抛出异常?(第一次启动项目时,还会是抛出异常的,因为我们的Eureka服务端还没有启动成功呢,所以还是会抛出异常的,我们只要看心跳之后,会不会抛出异常即可。)下面为启动后的日志:2019-03-09 21:00:27.909 INFO 1930 — [ Thread-21] o.s.c.n.e.server.EurekaServerBootstrap : isAws returned false2019-03-09 21:00:27.909 INFO 1930 — [ Thread-21] o.s.c.n.e.server.EurekaServerBootstrap : Initialized server context2019-03-09 21:00:27.949 INFO 1930 — [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ‘‘2019-03-09 21:00:27.949 INFO 1930 — [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 80802019-03-09 21:00:27.952 INFO 1930 — [ main] inwulaSpringcloudEurekaServerApplication : Started JilinwulaSpringcloudEurekaServerApplication in 4.318 seconds (JVM running for 4.816)2019-03-09 21:00:28.288 INFO 1930 — [(1)-192.168.0.3] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet ‘dispatcherServlet'2019-03-09 21:00:28.288 INFO 1930 — [(1)-192.168.0.3] o.s.web.servlet.DispatcherServlet : Initializing Servlet ‘dispatcherServlet'2019-03-09 21:00:28.295 INFO 1930 — [(1)-192.168.0.3] o.s.web.servlet.DispatcherServlet : Completed initialization in 6 ms2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Disable delta property : false2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Single vip registry refresh property : null2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Force full registry fetch : false2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Application is null : false2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Registered Applications size is zero : true2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Application version is -1: true2019-03-09 21:00:57.662 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : Getting all instance registry info from the eureka server2019-03-09 21:00:57.745 WARN 1930 — [nio-8080-exec-1] c.n.e.registry.AbstractInstanceRegistry : DS: Registry: lease doesn’t exist, registering resource: UNKNOWN - 192.168.0.32019-03-09 21:00:57.745 WARN 1930 — [nio-8080-exec-1] c.n.eureka.resources.InstanceResource : Not Found (Renew): UNKNOWN - 192.168.0.32019-03-09 21:00:57.763 INFO 1930 — [tbeatExecutor-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_UNKNOWN/192.168.0.3 - Re-registering apps/UNKNOWN2019-03-09 21:00:57.763 INFO 1930 — [tbeatExecutor-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_UNKNOWN/192.168.0.3: registering service…2019-03-09 21:00:57.770 INFO 1930 — [freshExecutor-0] com.netflix.discovery.DiscoveryClient : The response status is 2002019-03-09 21:00:57.807 INFO 1930 — [nio-8080-exec-3] c.n.e.registry.AbstractInstanceRegistry : Registered instance UNKNOWN/192.168.0.3 with status UP (replication=false)2019-03-09 21:00:57.809 INFO 1930 — [tbeatExecutor-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_UNKNOWN/192.168.0.3 - registration status: 2042019-03-09 21:00:58.329 INFO 1930 — [nio-8080-exec-4] c.n.e.registry.AbstractInstanceRegistry : Registered instance UNKNOWN/192.168.0.3 with status UP (replication=true)我们看项目这时已经不会抛出异常了,并且通过观察发现,每隔一段时间就会有日志输出,这也就是上面介绍的Eureka的服务端和Eureka的客户端的心跳机制。下面我们继续访问http://127.0.0.1:8080地址来看看此时的注册中心和刚刚相比,是否有不一样的地方。修改默认项目名我们看这时的注册中心已经检测到了有服务注册了,只不过这个服务就是Eureka的服务端自己,并且名字为UNKNOWN。如果有强迫者的人如果看到UNKNOWN那一定会感觉不舒服,不了解了Eureka组件的还以为注册中心出错了呢。下面我们修改一下项目参数,将UNKNOWN改成我们指定的名字。具体配置如下:eureka: client: service-url: defaultZone: http://127.0.0.1:8080/eureka/spring: application: name: jilinwula-springcloud-eureka-server我们在看一下注册中的的变化,看看还是不是已经成功的将UNKNOWN修改为我们指定的项目名字了。register-with-eureka配置我们看注册中心已经成功的显示我们配置文件中的项目名字了。在实际的开发中,我们基本不会让注册中心显示Eureka的服务端自己的服务,这样可能会导致和Eureka的客户端相混淆。所以通常的做法是让注册中心将Eureka的服务端屏蔽掉,说是屏蔽实际上是让Eureka的服务端不向注册中心注册。具体配置如下:eureka: client: service-url: defaultZone: http://127.0.0.1:8080/eureka/ register-with-eureka: falsespring: application: name: jilinwula-springcloud-eureka-server我们在观察一下注册中心看看还是否可以检测到Eureka服务端自己。我们发现这时注册中心已经检测到不任何服务了。下面我们将Eureka服务端的端口设置为默认的端口8761,因为8080端口可能会被占用。具体配置如下:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/ register-with-eureka: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 8761创建Eureka客户端步骤这时我们Eureka服务端的基本配置就介绍完了,下面我们介绍一下Eureka组件的客户端。下面我们还是向Eureka服务端一样从创建项目开始。具体步骤如下:我们还是在IDEA中选择Spring Initializr选项。也就是如下图所示:设置项目的相关参数,和Eureka服务端没有任何区别。这一步非常关键,因为它和Eureka服务端和SpringBoot都是不一样的。备注:为了保证Eureka服务端和客户端可以注册成功,我们要特别注意保证两个项目中SpringBoot及其SpringCloud的版本一致。由于剩下的步骤和Eureka服务端一样,我们就不做过多的介绍了。下面我们还是和Eureka服务端一样,配置注册中心的服务地址。具体配置如下:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/spring: application: name: jilinwula-springcloud-eureka-clientserver: port: 8081@EnableEurekaClient注解如果只修改上面的配置,注册中心是不会检测到Eureka客户端的,因为我们还没有在该项目的启动类上添加Eureka客户端的注解。具体配置如下:package com.jilinwula.jilinwulaspringcloudeurekaclient;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication@EnableEurekaClientpublic class JilinwulaSpringcloudEurekaClientApplication { public static void main(String[] args) { SpringApplication.run(JilinwulaSpringcloudEurekaClientApplication.class, args); }}@EnableEurekaClient注解与@EnableDiscoveryClient注解区别也就是添加@EnableEurekaClient注解。实际上除了该注解外,我们还可以用@EnableDiscoveryClient注解来达到同样的作用。它们两个注解的区别是注册中心的实现方式有很多种,如果是采用的是Eureka服务的话,那客户端直接使用@EnableEurekaClient注解和@EnableDiscoveryClient注解都可以。如果注册中心采用的是zookeeper或者其它服务时,那我们注册中心客户端就不能采用@EnableEurekaClient注解了,而是要使用@EnableDiscoveryClient注解。所以在实际的开发中我们推荐使用@EnableDiscoveryClient注解,这样当我们更换注册中心实现时,就不用修改代码了。上述代码中我们为了和Eureka服务端一致,所以我们采用@EnableDiscoveryClient注解。下面我们启动一下项目看看注册中心是否可以成功的检测Eureka客户端的存在。当我们按照上面的配置启动Eureka客户端时,我们发现日志居然报错了,并且项目自动停止运行了。具体日志如下:2019-03-09 23:00:55.844 INFO 2082 — [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_JILINWULA-SPRINGCLOUD-EUREKA-CLIENT/192.168.0.3:jilinwula-springcloud-eureka-client:8081: registering service…2019-03-09 23:00:55.852 INFO 2082 — [ main] inwulaSpringcloudEurekaClientApplication : Started JilinwulaSpringcloudEurekaClientApplication in 2.115 seconds (JVM running for 2.567)2019-03-09 23:00:55.866 INFO 2082 — [ Thread-8] o.s.c.n.e.s.EurekaServiceRegistry : Unregistering application JILINWULA-SPRINGCLOUD-EUREKA-CLIENT with eureka with status DOWN2019-03-09 23:00:55.866 WARN 2082 — [ Thread-8] com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1552143655866, current=DOWN, previous=UP]2019-03-09 23:00:55.869 INFO 2082 — [ Thread-8] com.netflix.discovery.DiscoveryClient : Shutting down DiscoveryClient …2019-03-09 23:00:55.883 INFO 2082 — [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_JILINWULA-SPRINGCLOUD-EUREKA-CLIENT/192.168.0.3:jilinwula-springcloud-eureka-client:8081 - registration status: 2042019-03-09 23:00:55.883 INFO 2082 — [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_JILINWULA-SPRINGCLOUD-EUREKA-CLIENT/192.168.0.3:jilinwula-springcloud-eureka-client:8081: registering service…2019-03-09 23:00:55.888 INFO 2082 — [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_JILINWULA-SPRINGCLOUD-EUREKA-CLIENT/192.168.0.3:jilinwula-springcloud-eureka-client:8081 - registration status: 2042019-03-09 23:00:55.889 INFO 2082 — [ Thread-8] com.netflix.discovery.DiscoveryClient : Unregistering …2019-03-09 23:00:55.893 INFO 2082 — [ Thread-8] com.netflix.discovery.DiscoveryClient : DiscoveryClient_JILINWULA-SPRINGCLOUD-EUREKA-CLIENT/192.168.0.3:jilinwula-springcloud-eureka-client:8081 - deregister status: 2002019-03-09 23:00:55.902 INFO 2082 — [ Thread-8] com.netflix.discovery.DiscoveryClient : Completed shut down of DiscoveryClient这是为什么呢?这个问题的原因是因为版本不同导致的,也就是有的Eureka版本有BUG导致的,少了一个依赖我们只要把缺少的依赖添加上即可。缺少的依赖如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>下面我们继续启动Eureka客户端,然后看看注册中心是否可以检测到Eureka客户端。这时我们发现注册中心已经成功的检测到了Eureka客户端的服务了。除此之外,我们发现此时的注册中心和以往相比有了其它的不同,我们发现注册中心显示警告信息了。这是为什么呢?这是因为注册中心有预警机制,因为我为了掩饰项目,会频繁的启动重启项目,这样注册中心的心跳就会时常检测不到Eureka客户端的心跳,所以就会认为该服务已下线。所以Eureka注册中心当服务下线少于一定比率时,就会显示警告信息,以此来表示有的服务运行不稳定。当然我们还是可以通过配置参数来消除上面的警告。具体参数如下:register-with-eureka配置eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 8761该参数的是意思是默认认为服务均在线,并且还有一点要注意,该参数是在Eureka服务端配置的。我们重新启动完Eureka服务端后,在看一下注册中心中的变化。Eureka服务端双注册中心配置这时我们发现警告信息又变了,这说明我们的配置参数启作用了。那为什么还会提示警告呢?这是因为我们配置了改参数,所以注册中心就不会准确的检查服务上下线状态了。所以提示了另一个警告。下面我们将Eureka服务端配置成多节点,在实际的项目开发中,我们知道一个节点可能会出现问题,如果Eureka服务端出现了问题,那么就相当于整个服务都不能调用了,所以为了保证高可用,通常会将Eureka服务端配置成多个节点,下面我们先尝试将Eureka服务端配置成双节点。既然是双节点,那当然是有两个Eureka服务端项目了,由于创建Eureka服务端的步骤,我们已经很熟悉了,所以我们只介绍它们配置文件的不同。首先我们先看一下第一个Eureka服务端注册中心配置:eureka: client: service-url: defaultZone: http://127.0.0.1:8762/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 8761第二个Eureka服务端注册中心配置:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 8762我们发现这两个配置基本相同,唯一的不同就是配置注册中地方,它们彼此配置的是对方的服务地址。也就是让两个Eureka服务端彼此注册,这样就只要我们Eureka客户端注册任意一个注册中心,这两个注册中心都可以检测到Eureka客户端的存在,因为底层它们会进行数据同步。下面我们看一下现在的注册中心的变化。8761注册中心:8762注册中心:我们看我们的Eureka客户端只配置了一个注册中心,但两个注册中心都检测到了Eureka客户端的存在。这就是刚刚提到过的当两个注册中心彼此注册时,就会进行数据通信,所以8762注册中心也检测到了该Eureka客户端的存在。下面我们将8761注册中心停止服务,然后在观察一下8762的注册中心,看看是否有何变化。8762注册中心:我们发现虽然我们将8761注册中心停止了服务,但8762注册中心依然检测到了Eureka客户端的存在。下面我们重新启动一下Eureka客户端然后在看一下8762注册中心还是否可以检测到Eureka客户端的存在。这时我们发现8762注册中心已经检测不到Eureka客户端的服务了。那应该怎么办呢?解决的办法很简单,那就是让我们的Eureka客户端注册两个注册中心。具体配置如下:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/,http://127.0.0.1:8762/eureka/spring: application: name: jilinwula-springcloud-eureka-clientserver: port: 8081这时我们在访问一下注册中心,看一下服务是否可以检测到。Eureka服务端三注册中心配置这时我们的注册中心已经成功的检测到了Eureka客户端了。下面我们介绍一下怎么部署Eureka服务端3节点。既然2节点我们已经知道了要彼此注册,那么3节点,我们应该已经猜到了,那就是让每一个节点都注册另外两个节点的服务。具体配置如下:8761注册中心:eureka: client: service-url: defaultZone: http://127.0.0.1:8762/eureka/,http://127.0.0.1:8763/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 87618762注册中心:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/,http://127.0.0.1:8763/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 87628763注册中心:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/,http://127.0.0.1:8762/eureka/ register-with-eureka: false server: enable-self-preservation: falsespring: application: name: jilinwula-springcloud-eureka-serverserver: port: 8763Eureka客户端:eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka/,http://127.0.0.1:8762/eureka/,http://127.0.0.1:8763/eureka/spring: application: name: jilinwula-springcloud-eureka-clientserver: port: 8081下面我们看一下访问任何一个注册中心,来看一下注册中心是否可以检测到Eureka客户端的服务,及其它注册中心的存在。我们看注册中心已经成功的检测到了Eureka客户端的服务了,并且红色标识的地方已经检测到了其它两个注册中心的地址了,所以我们在访问注册中心时,就可以通过下面红色标识的地方,来了解项目中的Eureka服务端有几个注册中心。上述内容就是SpringClould中Eureka组件的详细介绍,如有不正确或者需要交流的欢迎留言,下一篇我们将介绍怎么在SpringClould中进行不同的服务与服务之间的调用。谢谢。 项目源码https://github.com/jilinwula/jilinwula-springcloud-eureka.git原文链接http://jilinwula.com/article/24342 ...

March 10, 2019 · 4 min · jiezi

SpringBoot 实战 (十八) | 整合 MongoDB

微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。前言如题,今天介绍下 SpringBoot 是如何整合 MongoDB 的。MongoDB 简介MongoDB 是由 C++ 编写的非关系型数据库,是一个基于分布式文件存储的开源数据库系统,它将数据存储为一个文档,数据结构由键值 (key=>value) 对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组,非常灵活。存储结构如下:{ “studentId”: “201311611405”, “age”:24, “gender”:“男”, “name”:“一个优秀的废人”}准备工作SpringBoot 2.1.3 RELEASEMongnDB 2.1.3 RELEASEMongoDB 4.0IDEAJDK8创建一个名为 test 的数据库,不会建的。参考菜鸟教程:http://www.runoob.com/mongodb…配置数据源spring: data: mongodb: uri: mongodb://localhost:27017/test以上是无密码写法,如果 MongoDB 设置了密码应这样设置:spring: data: mongodb: uri: mongodb://name:password@localhost:27017/testpom 依赖配置<!– mongodb 依赖 –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId></dependency><!– web 依赖 –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><!– lombok 依赖 –><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional></dependency><!– test 依赖(没用到) –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope></dependency>实体类@Datapublic class Student { @Id private String id; @NotNull private String studentId; private Integer age; private String name; private String gender;}dao 层和 JPA 一样,SpringBoot 同样为开发者准备了一套 Repository ,只需要继承 MongoRepository 传入实体类型以及主键类型即可。@Repositorypublic interface StudentRepository extends MongoRepository<Student, String> {}service 层public interface StudentService { Student addStudent(Student student); void deleteStudent(String id); Student updateStudent(Student student); Student findStudentById(String id); List<Student> findAllStudent();}实现类:@Servicepublic class StudentServiceImpl implements StudentService { @Autowired private StudentRepository studentRepository; /** * 添加学生信息 * @param student * @return / @Override @Transactional(rollbackFor = Exception.class) public Student addStudent(Student student) { return studentRepository.save(student); } /* * 根据 id 删除学生信息 * @param id / @Override public void deleteStudent(String id) { studentRepository.deleteById(id); } /* * 更新学生信息 * @param student * @return / @Override @Transactional(rollbackFor = Exception.class) public Student updateStudent(Student student) { Student oldStudent = this.findStudentById(student.getId()); if (oldStudent != null){ oldStudent.setStudentId(student.getStudentId()); oldStudent.setAge(student.getAge()); oldStudent.setName(student.getName()); oldStudent.setGender(student.getGender()); return studentRepository.save(oldStudent); } else { return null; } } /* * 根据 id 查询学生信息 * @param id * @return / @Override public Student findStudentById(String id) { return studentRepository.findById(id).get(); } /* * 查询学生信息列表 * @return */ @Override public List<Student> findAllStudent() { return studentRepository.findAll(); }}controller 层@RestController@RequestMapping("/student")public class StudentController { @Autowired private StudentService studentService; @PostMapping("/add") public Student addStudent(@RequestBody Student student){ return studentService.addStudent(student); } @PutMapping("/update") public Student updateStudent(@RequestBody Student student){ return studentService.updateStudent(student); } @GetMapping("/{id}") public Student findStudentById(@PathVariable(“id”) String id){ return studentService.findStudentById(id); } @DeleteMapping("/{id}") public void deleteStudentById(@PathVariable(“id”) String id){ studentService.deleteStudent(id); } @GetMapping("/list") public List<Student> findAllStudent(){ return studentService.findAllStudent(); }}测试结果Postman 测试已经全部通过,这里仅展示了保存操作。这里推荐一个数据库可视化工具 Robo 3T 。下载地址:https://robomongo.org/download完整代码https://github.com/turoDog/De…如果觉得对你有帮助,请给个 Star 再走呗,非常感谢。后语如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。另外,关注之后在发送 1024 可领取免费学习资料。资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享 ...

March 9, 2019 · 2 min · jiezi

拥有者权限验证

问题描述在做权限验证的时候,我们经常会遇到这样的情况:教师拥有多个学生,但是在处理学生信息的时候,教师只能操作自己班级的学生。所以,我们要做的就是,当教师尝试处理别的班的学生的时候,抛出异常。实体关系用户1:1教师,教师m:n班级,班级1:n学生实现思路以findById为例。因为从整体上看,用户和学生是m:n的关系,所以在调用这个接口的时候,获取该学生的所有用户,然后跟当前登录用户进行对比,如果不在其中,抛出异常。利用切面,我们可以在findById、update、delete方法上进行验证。注解我们会在方法上添加注解,以表示对该方法进行权限验证。@Target(ElementType.METHOD) // 注解使用在方法上@Retention(RetentionPolicy.RUNTIME) // 运行时生效public @interface AuthorityAnnotation { /** * 仓库名 / @Required Class repository();}因为我们需要获取出学生,但是并不限于学生,所以就要将仓库repository作为一个参数传入。实体上面我们说过,需要获取学生中的用户,所以我们可以在实体中定义一个方法,获取所有有权限的用户:getBelongUsers()但是,我们知道,学生和用户没用直接的关系,而且为了复用,在对其他实体进行验证的时候也能使用,可以考虑创建一个接口,让需要验证的实体去实现他。这样,我们可以在让每个实体都集成这个接口,然后形成链式调用,这样就解决了上面你的两个问题。public interface BaseEntity { List<User> getBelongToUsers();}教师:@Entitypublic class Teacher implements YunzhiEntity, BaseEntity { … @Override public List<User> getBelongToUsers() { List<User> userList = new ArrayList<>(); userList.add(this.getUser()); return userList; }}班级:@Entitypublic class Klass implements BaseEntity { … @Override public List<User> getBelongToUsers() { List<User> userList = new ArrayList<>(); for (Teacher teacher: this.getTeacherList()) { userList.addAll(teacher.getBelongToUsers()); } return userList; }}学生:@Entitypublic class Student implements BaseEntity { … @Override public List<User> getBelongToUsers() { return this.getKlass().getBelongToUsers(); }}切面有了实体后,我们就可以建立切面实现验证功能了。@Aspect@Componentpublic class OwnerAuthorityAspect { private static final Logger logger = LoggerFactory.getLogger(OwnerAuthorityAspect.class.getName()); /* * 使用注解,并第一个参数为id / @Pointcut("@annotation(com.yunzhiclub.alice.annotation.AuthorityAnnotation) && args(id,..) && @annotation(authorityAnnotation)") public void doAccessCheck(Long id, AuthorityAnnotation authorityAnnotation) { } @Before(“doAccessCheck(id, authorityAnnotation)”) public void before(Long id, AuthorityAnnotation authorityAnnotation) { }首先,我们要获取到待操作对象。但是在获取对象之前,我们必须获取到repository。这里我们利用applicationContext来获取仓库bean,然后再利用获取到的bean,生成repository对象。@Aspect@Componentpublic class OwnerAuthorityAspect implements ApplicationContextAware { private ApplicationContext applicationContext = null; // 初始化上下文 …… @Before(“doAccessCheck(id, authorityAnnotation)”) public void before(Long id, AuthorityAnnotation authorityAnnotation) { logger.debug(“获取注解上的repository, 并通过applicationContext来获取bean”); Class<?> repositoryClass = authorityAnnotation.repository(); Object object = applicationContext.getBean(repositoryClass); logger.debug(“将Bean转换为CrudRepository”); CrudRepository<BaseEntity, Object> crudRepository = (CrudRepository<BaseEntity, Object>)object; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }}该类实现了ApplicationContextAware接口,通过setApplicationContext函数获取到了applicationContext。接下来,就是利用repository获取对象,然后获取他的所属用户,再与当前登录用户进行比较。@Before(“doAccessCheck(id, authorityAnnotation)")public void before(Long id, AuthorityAnnotation authorityAnnotation) { logger.debug(“获取注解上的repository, 并通过applicationContext来获取bean”); Class<?> repositoryClass = authorityAnnotation.repository(); Object object = applicationContext.getBean(repositoryClass); logger.debug(“将Bean转换为CrudRepository”); CrudRepository<BaseEntity, Object> crudRepository = (CrudRepository<BaseEntity, Object>)object; logger.debug(“获取实体对象”); Optional<BaseEntity> baseEntityOptional = crudRepository.findById(id); if(!baseEntityOptional.isPresent()) { throw new RuntimeException(“对不起,未找到相关的记录”); } BaseEntity baseEntity = baseEntityOptional.get(); logger.debug(“获取登录用户以及拥有者,并进行比对”); List<User> belongToTUsers = baseEntity.getBelongToUsers(); User currentLoginUser = userService.getCurrentLoginUser(); Boolean havePermission = false; if (currentLoginUser != null && belongToTUsers.size() != 0) { for (User user: belongToTUsers) { if (user.getId().equals(currentLoginUser.getId())) { havePermission = true; break; } } if (!havePermission) { throw new RuntimeException(“权限不允许”); } }}使用在控制器的方法上使用注解:@AuthorityAnnotation,传入repository。@RestController@RequestMapping("/student”)public class StudentController { private final StudentService studentService; // 学生 @Autowired public StudentController(StudentService studentService) { this.studentService = studentService; } /* * 通过id获取学生 * * @param id * @return */ @AuthorityAnnotation(repository = StudentRepository.class) @GetMapping("/{id}") @JsonView(StudentJsonView.get.class) public Student findById(@PathVariable Long id) { return studentService.findById(id); }}出现的问题实现之后,进行单元测试的过程中出现了问题。@Testpublic void update() throws Exception { logger.info(“获取一个保存学生”); Student student = studentService.getOneSaveStudent(); Long id = student.getId(); logger.info(“获取一个更新学生”); Student newStudent = studentService.getOneUnSaveStudent(); String jsonString = JSONObject.toJSONString(newStudent); logger.info(“发送更新请求”); this.mockMvc .perform(put(baseUrl + “/” + id) .cookie(this.cookie) .content(jsonString) .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk());}400的错误,说明参数错误,参数传的是实体,看下传了什么:我们看到,这个字段并不是我们实体中的字段,但是为什么序列化的时候出现了这个字段呢?原因是这样的,我们在实体中定义了一个getBelongToUsers函数,然后JSONobject在进行序列化的时候会根据实体中的getter方法,获取get后面的为key,也就是将belongToUsers看做了字段。所以就出现了上面传实体字段多出的情况,从而引发了400的错误。解决我们不想JSONobject在序列化的时候处理getBelongToUsers,就需要声明一下,这里用到了注解:@JsonIgnore。这样在序列化的时候就会忽略它。@Entitypublic class Student implements BaseEntity { …… @JsonIgnore @Override public List<User> getBelongToUsers() { return this.getKlass().getBelongToUsers(); }}修改后的学生实体如上,其他实现了getBelongToUsers方法的,都需要做相同处理。总结在解决这个问题的时候,开始就是自己埋头写,很多细节都没有处理好。然后偶然google到了潘老师之前写过的一篇文章,就对前面写的进行了完善。虽然自己解决问题的过程还是有很多收获的,但是如果开始直接参考这篇文章,会省不少事。其实是这样的,我们写博客,一方面是让自己有所提升,另一方面也是为了团队中的其他成员少走一些弯路。看来这次我是没有好好利用资源了。相关参考:https://my.oschina.net/dashan…https://segmentfault.com/a/11… ...

March 9, 2019 · 2 min · jiezi

SpringBoot使用prometheus监控

本文介绍SpringBoot如何使用Prometheus配合Grafana监控。1.关于PrometheusPrometheus是一个根据应用的metrics来进行监控的开源工具。相信很多工程都在使用它来进行监控,有关详细介绍可以查看官网:https://prometheus.io/docs/in…。2.有关GrafanaGrafana是一个开源监控利器,如图所示。从图中就可以看出来,使用Grafana监控很高大上,提供了很多可视化的图标。官网地址:https://grafana.com/3.SpringBoot使用Prometheus3.1 依赖内容在SpringBoot中使用Prometheus其实很简单,不需要配置太多的东西,在pom文件中加入依赖,完整内容如下所示。<?xml version=“1.0” encoding=“UTF-8”?><project xmlns=“http://maven.apache.org/POM/4.0.0" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!– lookup parent from repository –></parent><groupId>com.dalaoyang</groupId><artifactId>springboot2_prometheus</artifactId><version>0.0.1-SNAPSHOT</version><name>springboot2_prometheus</name><description>springboot2_prometheus</description><properties> <java.version>1.8</java.version></properties><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>1.1.3</version> </dependency></dependencies><build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins></build></project>3.2 配置文件配置文件中加入配置,这里就只进行一些简单配置,management.metrics.tags.application属性是本文配合Grafana的Dashboard设置的,如下所示:spring.application.name=springboot_prometheusmanagement.endpoints.web.exposure.include=*management.metrics.tags.application=${spring.application.name}3.3 设置application修改启动类,如下所示.@SpringBootApplicationpublic class Springboot2PrometheusApplication {public static void main(String[] args) { SpringApplication.run(Springboot2PrometheusApplication.class, args);}@BeanMeterRegistryCustomizer<MeterRegistry> configurer( @Value("${spring.application.name}”) String applicationName) { return (registry) -> registry.config().commonTags(“application”, applicationName);}}SpringBoot项目到这里就配置完成了,启动项目,访问http://localhost:8080/actuator/prometheus,如图所示,可以看到一些度量指标。4.Prometheus配置4.1 配置应用在prometheus配置监控我们的SpringBoot应用,完整配置如下所示。my global configglobal: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s).Alertmanager configurationalerting: alertmanagers:static_configs:targets: # - alertmanager:9093Load rules once and periodically evaluate them according to the global ’evaluation_interval’.rule_files: # - “first_rules.yml” # - “second_rules.yml"A scrape configuration containing exactly one endpoint to scrape:Here it’s Prometheus itself.scrape_configs:job_name: ‘prometheus’ static_configs:targets: [‘127.0.0.1:9090’]以下内容为SpringBoot应用配置job_name: ‘springboot_prometheus’ scrape_interval: 5s metrics_path: ‘/actuator/prometheus’ static_configs:- targets: [‘127.0.0.1:8080’]4.2 启动Prometheus启动Prometheus,浏览器访问,查看Prometheus页面,如图所示。点击如图所示位置,可以查看Prometheus监控的应用。列表中UP的页面为存活的实例,如图所示。也可以查看很多指数,如下所示。5.Grafana配置启动Grafana,配置Prometheus数据源,这里以ID是4701的Doshboard为例(地址:https://grafana.com/dashboard…)如图。在Grafana内点击如图所示import按钮在如图所示位置填写4701,然后点击load。接下来导入Doshboard。导入后就可以看到我们的SpringBoot项目对应的指标图表了,如图。6.源码源码地址:https://gitee.com/dalaoyang/s…本文作者:dalaoyang阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 8, 2019 · 1 min · jiezi

删除Druid下方的阿里云广告

目前druid-1.1.14的web控制台依然存在阿里云的广告,本文通过过滤器将广告文本拦截。定位问题产生广告的JS文件在 druid-1.1.14.jar/support/http/resources/js/common.js。查看源码可知是buildFooter方法进行植入,由init方法调用。解决问题/** * Druid的配置类 * * @author BBF /@Configuration@AutoConfigureAfter(DruidDataSourceAutoConfigure.class)public class DruidConfig { /* * 带有广告的common.js全路径,druid-1.1.14 / private static final String FILE_PATH = “support/http/resources/js/common.js”; /* * 原始脚本,触发构建广告的语句 / private static final String ORIGIN_JS = “this.buildFooter();”; /* * 替换后的脚本 / private static final String NEW_JS = “//this.buildFooter();”; /* * 去除Druid监控页面的广告 * * @param properties DruidStatProperties属性集合 * @return {@link org.springframework.boot.web.servlet.FilterRegistrationBean} / @Bean @ConditionalOnWebApplication @ConditionalOnProperty(name = “spring.datasource.druid.stat-view-servlet.enabled”, havingValue = “true”) public FilterRegistrationBean<RemoveAdFilter> removeDruidAdFilter( DruidStatProperties properties) throws IOException { // 获取web监控页面的参数 DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); // 提取common.js的配置路径 String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : “/druid/”; String commonJsPattern = pattern.replaceAll("\*", “js/common.js”); // 获取common.js String text = Utils.readFromResource(FILE_PATH); // 屏蔽 this.buildFooter(); 不构建广告 final String newJs = text.replace(ORIGIN_JS, NEW_JS); FilterRegistrationBean<RemoveAdFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new RemoveAdFilter(newJs)); registration.addUrlPatterns(commonJsPattern); return registration; } /** * 删除druid的广告过滤器 * * @author BBF */ private class RemoveAdFilter implements Filter { private final String newJs; public RemoveAdFilter(String newJS) { this.newJs = newJS; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(request, response); // 重置缓冲区,响应头不会被重置 response.resetBuffer(); response.getWriter().write(newJs); } }} ...

March 6, 2019 · 1 min · jiezi

使用SpringSecurity处理CSRF攻击

CSRF漏洞现状CSRF(Cross-site request forgery)跨站请求伪造,也被称为One Click Attack或者Session Riding,通常缩写为CSRF或XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。 CSRF是一种依赖web浏览器的、被混淆过的代理人攻击(deputy attack)。POM依赖<!– 模板引擎 freemarker –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId></dependency><!– Security (只使用CSRF部分) –><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId></dependency>配置过滤器@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } /** * 配置CSRF过滤器 * * @return {@link org.springframework.boot.web.servlet.FilterRegistrationBean} / @Bean public FilterRegistrationBean<CsrfFilter> csrfFilter() { FilterRegistrationBean<CsrfFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(new CsrfFilter(new HttpSessionCsrfTokenRepository())); registration.addUrlPatterns("/"); registration.setName(“csrfFilter”); return registration; }}在form请求中添加CSRF的隐藏字段<input name="${(_csrf.parameterName)!}" value="${(_csrf.token)!}" type=“hidden” />在AJAX请求中添加header头xhr.setRequestHeader("${_csrf.headerName}", “${_csrf.token}”);jQuery的Ajax全局配置jQuery.ajaxSetup({ “beforeSend”: function (request) { request.setRequestHeader("${_csrf.headerName}", “${_csrf.token}”); }});

March 6, 2019 · 1 min · jiezi

Laravel 和 Spring Boot 两个框架比较创业篇(一:开发效率)

我个人是比较不喜欢去正儿八经的比较两个框架的,这样没有意义,不过欲善其事先利其器!技术是相通的,但是在某个特定的领域的某个阶段肯定有相对最适合的一个工具!这里比较不是从技术角度比较,而是从公司技术选型考虑的,特别是初创的互联网创业公司。没办法,谁让互联网公司离不开软件呢!哈哈哈。首先是双方选手出场介绍:LaravelLaravel框架号称是Web艺术家的框架,富有生产力,代表了最优雅最流行的PHP框架,经过一段时间的使用,也上了一个项目,感觉特点如下:比较规范(PHP的框架中),适合团队分工协作开发速度快(社区生态和脚手架加持)部署方便(PHP的部署就那样吧,Git一套推拉下来就搞定了)功能模块比较全面架构较复杂(在PHP框架中,O(∩∩)O哈哈~)全栈,前后端一个IDE搞定其他文中再说Spring BootSpring Boot准确来说并不是一个完整的框架,而是为了使 Spring 全家桶更方便使用、更亲民而产生的一个整合框架。所以Spring Boot 的背后是 Spring 近乎无敌的生态和解决方案。先简单说一下特点吧:背靠 Java 这个老家伙,还有 Spring 这个J2EE 的标准背书,生态非常强大开发速度快(在Java系列中。。。),约定大于配置基于JVM,执行效率有保障需要掌握Spring的那一套,对于本身不是 J2EE 的童鞋学习成本有点高有Cloud 加持,微服务在召唤智能到令人发指的Spring Data JPA其他稍后文中再说好啦,介绍完选手,就开始来分析一下该用哪个啦,这里我们设定一个情境:假设 小红 是一位有一个自认为价值 20亿 的Idea,并且打算付诸实践的小BOSS(即将成为),稍懂软件架构和开发技术,没错,是很菜的那种(如果很厉害那随便怎么用框架了,没所谓),且启动资金只有 30万。我也不想假设的这么惨的,现实中这种情况很多,那我们就以这种情景展开分析。小红要以最低成本、最快速度推出 1.0 版本,投放市场,收集反馈,持续迭代。这是一个系统工程,讲其他因素剔除,只考虑技术问题,可以总结成以下几点:成本(开发效率和人工成本)响应(迭代和部署效率)安全(稳定性和 BUG解决速度)协作(团队协作和扩展性)1.开发效率开发这个过程,我们将它定义为需求和原型都已经确定,并且已经简单建模完毕,嗯,就是猿们到岗后拿着需求文档打开电脑(Windows)的时候开始,到 1.0 版本发布这段时间,是谁跑得快!O(∩∩)O哈哈~首先是 Laravel 框架,步骤是这样的:配置本地环境:包括PHP-CLI、Vagrant 、VirtualBox、HomeStead Box、Composer、nodejs(Mix要用到)、Python、Virtual Studio、Node-gyp(Node-Sass要用到)、PHPStorm、Git,一切就绪后composer create-project laravel/laravel xxx开发:定义migration、model,然后transformer和repository,再写service和passport啥的,再写controller,view视图,然后完善 Event、Notification、推送啥的,期间伴随着单元测试部署:Git push、Git Clone 、Pull,env整一个,上线对 Laravel 的开发流程熟悉的人呢,开发速度是很快的。我们再来看看Spring Boot:业务不复杂就不要折腾微服务啦,不要像某人一样明明只有一台机器,硬是要开几十个端口,然后跑几十个Spring Boot的小服务,还用Cloud全家桶串起来了。我竟无言以对单体应用撸起来,步骤如下:配置开发环境:IntelliJ IDEA下一个、JDK装一个、其他要用到的Redis啥的装上,分分钟就搞定可以开撸了。开发:定义JAP Entity,Repository、Service,配置Spring Security(包括Oauth2),定义Validation,开撸Controller、异常处理,视图层啥的,单元测试也少不了部署:打出Jar包,扔到服务器上执行吧,nginx映射一下,搞定我个人觉得Spring Boot的开发效率要比 Laravel 框架高些!为什么呢? 因为如果对 Spring 的机制熟悉,也了解 Security、JPA、Thymeleaf模板、RabbitMQ 等等功能模块的使用,Spring Boot 的封装是比 Laravel 要好的,但前提是对Spring 那一套熟悉,不然从何入手都弄不清楚。Spring 有些组件是非常复杂的,例如 Spring SecurityLaravel 框架借鉴了很多 Java Spring 的思想,比如容器,依赖注入、切面,这方面明显 Spring Boot 是正宗,注解啥的6得飞起!Java 语言非常严谨,在开发过程中的体验比较好,至少像我这样天马行空的猿,还非得要 Java 这个老头来管着,不然分分钟要跑偏。回到开发效率这个问题上,如果对两个框架都比较熟悉的情况下,Spring Boot 是开发比较快的,但 Laravel在某些方面是完胜Spring Boot,如下:Laravel 框架的 ORM 构建需要经历两个步骤,migration 和 model ,而且改动 migration 需要调整 model,无法向 JPA 一样Entity 即数据库结构;Laravel 框架需要手动实现一些注入绑定,通常是$app->bind,尽管这不消耗多少时间,但是比起Spring强大的注解还是慢不少,而且主流IDE对 Spring 的 Bean 提供了导航查看功能,牛逼哄哄啊;如果要做网页渲染,Laravel的动态脚本语言特性加上Blade模板基本是秒杀Spring Boot 的;要让层次更分明一些的话,Laravel 需要手动实现Repository 模式,反正我是受不了Model 直接定义业务逻辑的,放在Controller里也受不了,不但难看,还不好扩展;在授权这方面,Laravel 自带的和Spring Security 都很强大,可以说是开箱即用,打平;Laravel框架开发反馈调试方面是完胜Spring Boot的,这方面可以说所有非编译型的语言都很爽!尽管Spring Boot 也有DevTool,但是架不住 PHP 根本就不需要重新启动呀。Laravel框架的代码提示远远比不上Spring Boot,而且还需要第三方包Ide-Helper的加持,不然代码追踪都不行,可是就算用了第三方包还是看不了 容器内长啥样啊;像 Laravel 这样靠面向对象体现优雅的框架,却遇到了PHP 这门面向对象不太完全的语言,以致于在 Java 体系内很容易实现的一个功能,到了PHP体系却无能为力;Route 路由这方面 Laravel 非常强大,而且直观,比Spring Boot 灵活,所以定义路由的时候效率完爆Spring Boot;异常处理两者都非常方便,提供了统一处理的方式,难分伯仲;Api Json数据定制这方面,Laravel 比 Spring Boot 要强大,这是因为PHP的数组操作非常灵活,对于 Java 来说需要定义工具类和实体类来专门处理;i18n国际化,Laravel 比Spring Boot 方便;前端资源处理,就这个功能本身来说,Laravel的Mix配合Blade模板完爆Spring Boot,但是话说回来,只要不是全栈,这不算什么优势。设想一下如果是前端做好页面,拿到后端套模板,那Thymeleaf 完爆 Blade,因为Thymeleaf 可以保留预览数据,渲染实际数据,Blade 做不到这一点。总结:在技能掌握充足的情况下,个人感觉 Spring Boot 开发效率要略高于Laravel。个人掌握情况不一样,请勿喷,可以参考文中的几个维度,自己思考一下。最后想提一下,顺便求证:Laravel 不念 “拉瓦” Laravel 不念 “拉瓦” Laravel 不念 “拉瓦”时候不早了,有点困。今天就写到这,明天再写人工成本的考量。大家晚安!谢谢 ...

March 6, 2019 · 1 min · jiezi